Skip to content

Commit 49b4468

Browse files
Merge pull request #48 from Promptly-Technologies-LLC/11-fix-delete-account-flow
11 fix delete account flow
2 parents 4ab25bb + b165e0a commit 49b4468

File tree

16 files changed

+281
-127
lines changed

16 files changed

+281
-127
lines changed

docs/customization.qmd

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ The following fixtures, defined in `tests/conftest.py`, are available in the tes
2525
- `set_up_database`: Sets up the test database before running the test suite by dropping all tables and recreating them to ensure a clean state.
2626
- `session`: Provides a session for database operations in tests.
2727
- `clean_db`: Cleans up the database tables before each test by deleting all entries in the `PasswordResetToken` and `User` tables.
28-
- `client`: Provides a `TestClient` instance with the session fixture, overriding the `get_session` dependency to use the test session.
28+
- `auth_client`: Provides a `TestClient` instance with access and refresh token cookies set, overriding the `get_session` dependency to use the `session` fixture.
29+
- `unauth_client`: Provides a `TestClient` instance without authentication cookies set, overriding the `get_session` dependency to use the `session` fixture.
2930
- `test_user`: Creates a test user in the database with a predefined name, email, and hashed password.
3031

3132
To run the tests, use these commands:
@@ -43,7 +44,7 @@ The project uses type annotations and mypy for static type checking. To run mypy
4344
mypy
4445
```
4546

46-
We find that mypy is an enormous time-saver, catching many errors early and greatly reducing time spent debugging unit tests. However, note that mypy requires you type annotate every variable, function, and method in your code base, so taking advantage of it is a lifestyle change!
47+
We find that mypy is an enormous time-saver, catching many errors early and greatly reducing time spent debugging unit tests. However, note that mypy requires you type annotate every variable, function, and method in your code base, so taking advantage of it requires a lifestyle change!
4748

4849
### Developing with LLMs
4950

@@ -84,15 +85,19 @@ We also create POST endpoints, which accept form submissions so the user can cre
8485

8586
#### Routing patterns in this template
8687

87-
In this template, GET routes are defined in the main entry point for the application, `main.py`. POST routes are organized into separate modules within the `routers/` directory. We name our GET routes using the convention `read_<name>`, where `<name>` is the name of the page, to indicate that they are read-only endpoints that do not modify the database.
88+
In this template, GET routes are defined in the main entry point for the application, `main.py`. POST routes are organized into separate modules within the `routers/` directory.
89+
90+
We name our GET routes using the convention `read_<name>`, where `<name>` is the name of the page, to indicate that they are read-only endpoints that do not modify the database.
8891

8992
We divide our GET routes into authenticated and unauthenticated routes, using commented section headers in our code that look like this:
9093

9194
```python
9295
# -- Authenticated Routes --
9396
```
9497

95-
Some of our routes take request parameters, which we pass as keyword arguments to the route handler. These parameters should be type annotated for validation purposes. Some parameters are shared across all authenticated or unauthenticated routes, so we define them in the `common_authenticated_parameters` and `common_unauthenticated_parameters` dependencies defined in `main.py`.
98+
Some of our routes take request parameters, which we pass as keyword arguments to the route handler. These parameters should be type annotated for validation purposes.
99+
100+
Some parameters are shared across all authenticated or unauthenticated routes, so we define them in the `common_authenticated_parameters` and `common_unauthenticated_parameters` dependencies defined in `main.py`.
96101

97102
### HTML templating with Jinja2
98103

@@ -113,7 +118,7 @@ async def welcome(request: Request):
113118
)
114119
```
115120

116-
In this example, the `welcome.html` template will receive two pieces of context: the user's `request`, which is always passed automatically by FastAPI, and a `username` variable, which we specify as "Alice". We can then use the `{{ username }}` syntax in the `welcome.html` template (or any of its parent or child templates) to insert the value into the HTML.
121+
In this example, the `welcome.html` template will receive two pieces of context: the user's `request`, which is always passed automatically by FastAPI, and a `username` variable, which we specify as "Alice". We can then use the `{{{ username }}}` syntax in the `welcome.html` template (or any of its parent or child templates) to insert the value into the HTML.
117122

118123
#### Form validation strategy
119124

docs/installation.qmd

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ If you use VSCode with Docker to develop in a container, the following VSCode De
2020

2121
Simply create a `.devcontainer` folder in the root of the project and add a `devcontainer.json` file in the folder with the above content. VSCode may prompt you to install the Dev Container extension if you haven't already, and/or to open the project in a container. If not, you can manually select "Dev Containers: Reopen in Container" from View > Command Palette.
2222

23+
*IMPORTANT: If using this dev container configuration, you will need to set the `DB_HOST` environment variable to "host.docker.internal" in the `.env` file.*
24+
2325
## Install development dependencies manually
2426

2527
### Python and Docker
@@ -103,15 +105,32 @@ Set your desired database name, username, and password in the .env file.
103105

104106
To use password recovery, register a [Resend](https://resend.com/) account, verify a domain, get an API key, and paste the API key into the .env file.
105107

108+
If using the dev container configuration, you will need to set the `DB_HOST` environment variable to "host.docker.internal" in the .env file. Otherwise, set `DB_HOST` to "localhost" for local development. (In production, `DB_HOST` will be set to the hostname of the database server.)
109+
106110
## Start development database
107111

112+
To start the development database, run the following command in your terminal from the root directory:
113+
108114
``` bash
109115
docker compose up -d
110116
```
111117

118+
If at any point you change the environment variables in the .env file, you will need to stop the database service *and tear down the volume*:
119+
120+
``` bash
121+
# Don't forget the -v flag to tear down the volume!
122+
docker compose down -v
123+
```
124+
125+
You may also need to restart the terminal session to pick up the new environment variables. You can also add the `--force-recreate` and `--build` flags to the startup command to ensure the container is rebuilt:
126+
127+
``` bash
128+
docker compose up -d --force-recreate --build
129+
```
130+
112131
## Run the development server
113132

114-
Make sure the development database is running and tables and default permissions/roles are created first.
133+
Before running the development server, make sure the development database is running and tables and default permissions/roles are created first. Then run the following command in your terminal from the root directory:
115134

116135
``` bash
117136
uvicorn main:app --host 0.0.0.0 --port 8000 --reload

index.qmd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ To use password recovery, register a [Resend](https://resend.com/) account, veri
107107

108108
### Start development database
109109

110+
To start the development database, run the following command in your terminal from the root directory:
111+
110112
``` bash
111113
docker compose up -d
112114
```

main.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from fastapi.exceptions import RequestValidationError, HTTPException, StarletteHTTPException
99
from sqlmodel import Session
1010
from routers import authentication, organization, role, user
11-
from utils.auth import get_authenticated_user, get_optional_user, NeedsNewTokens, get_user_from_reset_token, PasswordValidationError
11+
from utils.auth import get_authenticated_user, get_optional_user, NeedsNewTokens, get_user_from_reset_token, PasswordValidationError, AuthenticationError
1212
from utils.models import User
1313
from utils.db import get_session, set_up_db
1414

@@ -37,6 +37,15 @@ async def lifespan(app: FastAPI):
3737
# -- Exception Handling Middlewares --
3838

3939

40+
# Handle AuthenticationError by redirecting to login page
41+
@app.exception_handler(AuthenticationError)
42+
async def authentication_error_handler(request: Request, exc: AuthenticationError):
43+
return RedirectResponse(
44+
url="/login",
45+
status_code=status.HTTP_303_SEE_OTHER
46+
)
47+
48+
4049
# Handle NeedsNewTokens by setting new tokens and redirecting to same page
4150
@app.exception_handler(NeedsNewTokens)
4251
async def needs_new_tokens_handler(request: Request, exc: NeedsNewTokens):
@@ -104,10 +113,6 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
104113
# Handle StarletteHTTPException (including 404, 405, etc.) by rendering the error page
105114
@app.exception_handler(StarletteHTTPException)
106115
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
107-
# Don't handle redirects
108-
if exc.status_code in [301, 302, 303, 307, 308]:
109-
raise exc
110-
111116
return templates.TemplateResponse(
112117
request,
113118
"errors/error.html",

routers/authentication.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ class UserRead(BaseModel):
114114
organization_id: Optional[int]
115115
created_at: datetime
116116
updated_at: datetime
117-
deleted: bool
118117

119118

120119
# -- Routes --

routers/organization.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ class OrganizationRead(BaseModel):
2727
name: str
2828
created_at: datetime
2929
updated_at: datetime
30-
deleted: bool
3130

3231

3332
class OrganizationUpdate(BaseModel):
@@ -113,9 +112,7 @@ def delete_organization(
113112
if not db_org:
114113
raise HTTPException(status_code=404, detail="Organization not found")
115114

116-
db_org.deleted = True
117-
db_org.updated_at = datetime.utcnow()
118-
session.add(db_org)
115+
session.delete(db_org)
119116
session.commit()
120117

121118
return RedirectResponse(url="/organizations", status_code=303)

routers/role.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ class RoleRead(BaseModel):
3131
name: str
3232
created_at: datetime
3333
updated_at: datetime
34-
deleted: bool
3534
permissions: List[ValidPermissions]
3635

3736

@@ -74,7 +73,7 @@ def create_role(
7473
@router.get("/{role_id}", response_model=RoleRead)
7574
def read_role(role_id: int, session: Session = Depends(get_session)):
7675
db_role: Role | None = session.get(Role, role_id)
77-
if not db_role or not db_role.id or db_role.deleted:
76+
if not db_role or not db_role.id:
7877
raise HTTPException(status_code=404, detail="Role not found")
7978

8079
permissions = [
@@ -88,7 +87,6 @@ def read_role(role_id: int, session: Session = Depends(get_session)):
8887
name=db_role.name,
8988
created_at=db_role.created_at,
9089
updated_at=db_role.updated_at,
91-
deleted=db_role.deleted,
9290
permissions=permissions
9391
)
9492

@@ -99,7 +97,7 @@ def update_role(
9997
session: Session = Depends(get_session)
10098
) -> RedirectResponse:
10199
db_role: Role | None = session.get(Role, role.id)
102-
if not db_role or not db_role.id or db_role.deleted:
100+
if not db_role or not db_role.id:
103101
raise HTTPException(status_code=404, detail="Role not found")
104102
role_data = role.model_dump(exclude_unset=True)
105103
for key, value in role_data.items():
@@ -131,8 +129,6 @@ def delete_role(
131129
db_role = session.get(Role, role_id)
132130
if not db_role:
133131
raise HTTPException(status_code=404, detail="Role not found")
134-
db_role.deleted = True
135-
db_role.updated_at = utc_time()
136-
session.add(db_role)
132+
session.delete(db_role)
137133
session.commit()
138134
return RedirectResponse(url="/roles", status_code=303)

routers/user.py

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
# -- Server Request and Response Models --
1212

1313

14-
class UserProfile(BaseModel):
14+
class UpdateProfile(BaseModel):
15+
"""Request model for updating user profile information"""
1516
name: str
1617
email: EmailStr
1718
avatar_url: str
@@ -40,41 +41,40 @@ async def as_form(
4041
# -- Routes --
4142

4243

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(...),
44+
@router.post("/update_profile", response_class=RedirectResponse)
45+
async def update_profile(
46+
user_profile: UpdateProfile = Depends(UpdateProfile.as_form),
5647
current_user: User = Depends(get_authenticated_user),
5748
session: Session = Depends(get_session)
5849
):
5950
# Update user details
60-
current_user.name = name
61-
current_user.email = email
62-
current_user.avatar_url = avatar_url
51+
current_user.name = user_profile.name
52+
current_user.email = user_profile.email
53+
current_user.avatar_url = user_profile.avatar_url
6354
session.commit()
6455
session.refresh(current_user)
6556
return RedirectResponse(url="/profile", status_code=303)
6657

6758

6859
@router.post("/delete_account", response_class=RedirectResponse)
6960
async def delete_account(
70-
confirm_delete_password: str = Form(...),
61+
user_delete_account: UserDeleteAccount = Depends(
62+
UserDeleteAccount.as_form),
7163
current_user: User = Depends(get_authenticated_user),
7264
session: Session = Depends(get_session)
7365
):
74-
if not verify_password(confirm_delete_password, current_user.hashed_password):
75-
raise HTTPException(status_code=400, detail="Password is incorrect")
66+
if not verify_password(
67+
user_delete_account.confirm_delete_password,
68+
current_user.hashed_password
69+
):
70+
raise HTTPException(
71+
status_code=400,
72+
detail="Password is incorrect"
73+
)
7674

77-
# Mark the user as deleted
78-
current_user.deleted = True
75+
# Delete the user
76+
session.delete(current_user)
7977
session.commit()
80-
return RedirectResponse(url="/", status_code=303)
78+
79+
# Log out the user
80+
return RedirectResponse(url="/auth/logout", status_code=303)

templates/authentication/register.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<div class="mb-3">
2626
<label for="password" class="form-label">Password</label>
2727
<input type="password" class="form-control" id="password" name="password"
28-
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@$!%*?&\{\}\<\>\.\,\\\\'#\-_=\+\(\)\[\]:;\|~])[A-Za-z\d@$!%*?&\{\}\<\>\.\,\\\\'#\-_=\+\(\)\[\]:;\|~]{8,}"
28+
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@$!%*?&\{\}\<\>\.\,\\\\'#\-_=\+\(\)\[\]:;\|~\/])[A-Za-z\d@$!%*?&\{\}\<\>\.\,\\\\'#\-_=\+\(\)\[\]:;\|~\/]{8,}"
2929
title="Must contain at least one number, one uppercase and lowercase letter, one special character, and at least 8 or more characters"
3030
placeholder="Enter your password" required
3131
autocomplete="new-password">

templates/users/profile.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ <h1 class="mb-4">User Profile</h1>
3434
Edit Profile
3535
</div>
3636
<div class="card-body">
37-
<form action="{{ url_for('edit_profile') }}" method="post">
37+
<form action="{{ url_for('update_profile') }}" method="post">
3838
<div class="mb-3">
3939
<label for="name" class="form-label">Name</label>
4040
<input type="text" class="form-control" id="name" name="name" value="{{ user.name }}">

0 commit comments

Comments
 (0)