Skip to content

Commit 8466273

Browse files
2 parents 3fe8290 + 81d8621 commit 8466273

File tree

8 files changed

+182
-8
lines changed

8 files changed

+182
-8
lines changed

routers/authentication.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,6 @@ async def forgot_password(
241241
db_user = session.exec(select(User).where(
242242
User.email == user.email)).first()
243243

244-
# TODO: Handle this in background task so we don't leak information via timing attacks
245244
if db_user:
246245
background_tasks.add_task(send_reset_email, user.email, session)
247246

routers/user.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,80 @@
1-
from fastapi import APIRouter
1+
from fastapi import APIRouter, Depends, HTTPException, Form
2+
from fastapi.responses import RedirectResponse
3+
from pydantic import BaseModel, EmailStr
4+
from sqlmodel import Session
5+
from utils.db import User
6+
from utils.auth import get_session, get_authenticated_user, verify_password
27

38
router = APIRouter(prefix="/user", tags=["user"])
9+
10+
11+
# -- Server Request and Response Models --
12+
13+
14+
class UserProfile(BaseModel):
15+
name: str
16+
email: EmailStr
17+
avatar_url: str
18+
19+
@classmethod
20+
async def as_form(
21+
cls,
22+
name: str = Form(...),
23+
email: EmailStr = Form(...),
24+
avatar_url: str = Form(...),
25+
):
26+
return cls(name=name, email=email, avatar_url=avatar_url)
27+
28+
29+
class UserDeleteAccount(BaseModel):
30+
confirm_delete_password: str
31+
32+
@classmethod
33+
async def as_form(
34+
cls,
35+
confirm_delete_password: str = Form(...),
36+
):
37+
return cls(confirm_delete_password=confirm_delete_password)
38+
39+
40+
# -- Routes --
41+
42+
43+
@router.get("/profile", response_class=RedirectResponse)
44+
async def view_profile(
45+
current_user: User = Depends(get_authenticated_user)
46+
):
47+
# Render the profile page with the current user's data
48+
return {"user": current_user}
49+
50+
51+
@router.post("/edit_profile", response_class=RedirectResponse)
52+
async def edit_profile(
53+
name: str = Form(...),
54+
email: str = Form(...),
55+
avatar_url: str = Form(...),
56+
current_user: User = Depends(get_authenticated_user),
57+
session: Session = Depends(get_session)
58+
):
59+
# Update user details
60+
current_user.name = name
61+
current_user.email = email
62+
current_user.avatar_url = avatar_url
63+
session.commit()
64+
session.refresh(current_user)
65+
return RedirectResponse(url="/profile", status_code=303)
66+
67+
68+
@router.post("/delete_account", response_class=RedirectResponse)
69+
async def delete_account(
70+
confirm_delete_password: str = Form(...),
71+
current_user: User = Depends(get_authenticated_user),
72+
session: Session = Depends(get_session)
73+
):
74+
if not verify_password(confirm_delete_password, current_user.hashed_password):
75+
raise HTTPException(status_code=400, detail="Password is incorrect")
76+
77+
# Mark the user as deleted
78+
current_user.deleted = True
79+
session.commit()
80+
return RedirectResponse(url="/", status_code=303)

templates/components/header.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
{% if user %}
2424
<li class="nav-item dropdown">
2525
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
26-
{{ render_silhouette() }}
26+
<button class="profile-button btn p-0 border-0 bg-transparent">
27+
{{ render_silhouette() }}
28+
</button>
2729
</a>
2830
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
2931
<li><a class="dropdown-item" href="{{ url_for('read_profile') }}">Profile</a></li>

templates/components/silhouette.html

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
{% macro render_silhouette(width=30, height=30, classes="d-inline-block align-top") %}
2-
<button class="profile-button btn p-0 border-0 bg-transparent">
32
<svg width="{{ width }}" height="{{ height }}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="{{ classes }}">
43
<circle cx="12" cy="12" r="12" fill="#E0E0E0"/>
54
<path d="M12 12C14.2091 12 16 10.2091 16 8C16 5.79086 14.2091 4 12 4C9.79086 4 8 5.79086 8 8C8 10.2091 9.79086 12 12 12Z" fill="#BDBDBD"/>
65
<path d="M4 20C4 16.6863 7.58172 14 12 14C16.4183 14 20 16.6863 20 20H4Z" fill="#BDBDBD"/>
76
</svg>
8-
</button>
97
{% endmacro %}

templates/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ <h5 class="card-title">PostgreSQL</h5>
4444

4545
<div class="row">
4646
<div class="col-md-6 offset-md-3 text-center">
47-
<a href="{{ url_for('read_register') }}" class="btn btn-primary btn-lg mx-3">Sign Up</a>
48-
<a href="{{ url_for('read_login') }}" class="btn btn-outline-secondary btn-lg">Log In</a>
47+
<a href="{{ url_for('read_register') }}" class="btn btn-primary btn-lg mx-3 mb-3">Sign Up</a>
48+
<a href="{{ url_for('read_login') }}" class="btn btn-outline-secondary btn-lg mb-3">Log In</a>
4949
</div>
5050
</div>
5151
</div>

templates/users/change_password.html

Whitespace-only changes.

templates/users/edit_profile.html

Whitespace-only changes.

templates/users/profile.html

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,103 @@
11
{% extends "base.html" %}
2+
{% from 'components/silhouette.html' import render_silhouette %}
3+
4+
{% block title %}Profile{% endblock %}
25

36
{% block content %}
4-
<p>Welcome, {{ user.name }}!</p>
7+
<div class="container mt-5">
8+
<h1 class="mb-4">User Profile</h1>
9+
10+
<!-- Basic Information -->
11+
<div class="card mb-4" id="basic-info">
12+
<div class="card-header">
13+
Basic Information
14+
</div>
15+
<div class="card-body">
16+
<p><strong>Name:</strong> {{ user.name }}</p>
17+
<p><strong>Email:</strong> {{ user.email }}</p>
18+
<!-- Display user avatar or silhouette if no avatar is available -->
19+
<div class="mb-3">
20+
{% if user.avatar_url %}
21+
<img src="{{ user.avatar_url }}" alt="User Avatar" class="img-thumbnail" width="150">
22+
{% else %}
23+
{{ render_silhouette(width=150, height=150) }}
24+
{% endif %}
25+
</div>
26+
<!-- Edit button placed below the image -->
27+
<button class="btn btn-primary mt-3" onclick="toggleEditProfile()">Edit</button>
28+
</div>
29+
</div>
30+
31+
<!-- Edit Profile -->
32+
<div class="card mb-4" id="edit-profile" style="display: none;">
33+
<div class="card-header">
34+
Edit Profile
35+
</div>
36+
<div class="card-body">
37+
<form action="{{ url_for('edit_profile') }}" method="post">
38+
<div class="mb-3">
39+
<label for="name" class="form-label">Name</label>
40+
<input type="text" class="form-control" id="name" name="name" value="{{ user.name }}">
41+
</div>
42+
<div class="mb-3">
43+
<label for="email" class="form-label">Email</label>
44+
<input type="email" class="form-control" id="email" name="email" value="{{ user.email }}">
45+
</div>
46+
<div class="mb-3">
47+
<label for="avatar_url" class="form-label">Avatar URL</label>
48+
<input type="url" class="form-control" id="avatar_url" name="avatar_url" value="{{ user.avatar_url }}">
49+
</div>
50+
<button type="submit" class="btn btn-primary">Save Changes</button>
51+
</form>
52+
</div>
53+
</div>
54+
55+
<!-- Change Password -->
56+
<div class="card mb-4">
57+
<div class="card-header">
58+
Change Password
59+
</div>
60+
<div class="card-body">
61+
<!-- Trigger password reset via email confirmation -->
62+
<form action="{{ url_for('forgot_password') }}" method="post">
63+
<input type="hidden" name="email" value="{{ user.email }}">
64+
<p>To change your password, please confirm your email. A password reset link will be sent to your email address.</p>
65+
<button type="submit" class="btn btn-primary">Send Password Reset Email</button>
66+
</form>
67+
</div>
68+
</div>
69+
70+
<!-- Delete Account -->
71+
<div class="card mb-4">
72+
<div class="card-header">
73+
Delete Account
74+
</div>
75+
<div class="card-body">
76+
<form action="{{ url_for('delete_account') }}" method="post">
77+
<p class="text-danger">This action cannot be undone. Please confirm your password to delete your account.</p>
78+
<div class="mb-3">
79+
<label for="confirm_delete_password" class="form-label">Password</label>
80+
<input type="password" class="form-control" id="confirm_delete_password" name="confirm_delete_password">
81+
</div>
82+
<button type="submit" class="btn btn-danger">Delete Account</button>
83+
</form>
84+
</div>
85+
</div>
86+
</div>
87+
88+
<script>
89+
// Function to toggle visibility of Basic Information and Edit Profile sections
90+
function toggleEditProfile() {
91+
var basicInfo = document.getElementById('basic-info');
92+
var editProfile = document.getElementById('edit-profile');
93+
94+
if (basicInfo.style.display === 'none') {
95+
basicInfo.style.display = 'block';
96+
editProfile.style.display = 'none';
97+
} else {
98+
basicInfo.style.display = 'none';
99+
editProfile.style.display = 'block';
100+
}
101+
}
102+
</script>
5103
{% endblock %}

0 commit comments

Comments
 (0)