Skip to content

Commit 2cb3ca4

Browse files
Merge branch '77-refactor-to-use-a-more-traditional-crud-module-structure' into 89-use-inheritance-to-define-data-models
2 parents d114948 + 31a4254 commit 2cb3ca4

File tree

14 files changed

+244
-202
lines changed

14 files changed

+244
-202
lines changed

main.py

Lines changed: 28 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,16 @@
55
from fastapi.responses import RedirectResponse
66
from fastapi.staticfiles import StaticFiles
77
from fastapi.templating import Jinja2Templates
8-
from fastapi.exceptions import RequestValidationError, HTTPException, StarletteHTTPException
9-
from sqlmodel import Session
10-
from routers import authentication, organization, role, user
8+
from fastapi.exceptions import RequestValidationError, StarletteHTTPException
9+
from routers import authentication, organization, role, user, dashboard, terms_of_service, privacy_policy, about
1110
from utils.auth import (
12-
HTML_PASSWORD_PATTERN,
13-
get_user_with_relations,
14-
get_optional_user,
1511
NeedsNewTokens,
16-
get_user_from_reset_token,
1712
PasswordValidationError,
18-
AuthenticationError
13+
AuthenticationError,
14+
get_optional_user
1915
)
16+
from utils.db import set_up_db
2017
from utils.models import User
21-
from utils.db import get_session, set_up_db
22-
from utils.images import MAX_FILE_SIZE, MIN_DIMENSION, MAX_DIMENSION, ALLOWED_CONTENT_TYPES
2318

2419
logger = logging.getLogger("uvicorn.error")
2520
logger.setLevel(logging.DEBUG)
@@ -33,23 +28,35 @@ async def lifespan(app: FastAPI):
3328
# Optional shutdown logic
3429

3530

31+
# Initialize the FastAPI app
3632
app: FastAPI = FastAPI(lifespan=lifespan)
3733

38-
# Mount static files (e.g., CSS, JS)
34+
# Mount static files (e.g., CSS, JS) and initialize Jinja2 templates
3935
app.mount("/static", StaticFiles(directory="static"), name="static")
40-
41-
# Initialize Jinja2 templates
4236
templates = Jinja2Templates(directory="templates")
4337

4438

39+
# --- Include Routers ---
40+
41+
42+
app.include_router(authentication.router)
43+
app.include_router(organization.router)
44+
app.include_router(role.router)
45+
app.include_router(user.router)
46+
app.include_router(dashboard.router)
47+
app.include_router(terms_of_service.router)
48+
app.include_router(privacy_policy.router)
49+
app.include_router(about.router)
50+
51+
4552
# --- Exception Handling Middlewares ---
4653

4754

4855
# Handle AuthenticationError by redirecting to login page
4956
@app.exception_handler(AuthenticationError)
5057
async def authentication_error_handler(request: Request, exc: AuthenticationError):
5158
return RedirectResponse(
52-
url="/login",
59+
url=app.url_path_for("read_login"),
5360
status_code=status.HTTP_303_SEE_OTHER
5461
)
5562

@@ -146,160 +153,21 @@ async def general_exception_handler(request: Request, exc: Exception):
146153
)
147154

148155

149-
# --- Unauthenticated Routes ---
150-
151-
152-
# Define a dependency for common parameters
153-
async def common_unauthenticated_parameters(
154-
request: Request,
155-
user: Optional[User] = Depends(get_optional_user),
156-
error_message: Optional[str] = None,
157-
) -> dict:
158-
return {"request": request, "user": user, "error_message": error_message}
156+
# --- Home Page ---
159157

160158

161159
@app.get("/")
162160
async def read_home(
163-
params: dict = Depends(common_unauthenticated_parameters)
164-
):
165-
if params["user"]:
166-
return RedirectResponse(url="/dashboard", status_code=302)
167-
return templates.TemplateResponse(params["request"], "index.html", params)
168-
169-
170-
@app.get("/login")
171-
async def read_login(
172-
params: dict = Depends(common_unauthenticated_parameters),
173-
email_updated: Optional[str] = "false"
174-
):
175-
if params["user"]:
176-
return RedirectResponse(url="/dashboard", status_code=302)
177-
params["email_updated"] = email_updated
178-
return templates.TemplateResponse(params["request"], "authentication/login.html", params)
179-
180-
181-
@app.get("/register")
182-
async def read_register(
183-
params: dict = Depends(common_unauthenticated_parameters)
184-
):
185-
if params["user"]:
186-
return RedirectResponse(url="/dashboard", status_code=302)
187-
188-
params["password_pattern"] = HTML_PASSWORD_PATTERN
189-
return templates.TemplateResponse(params["request"], "authentication/register.html", params)
190-
191-
192-
@app.get("/forgot_password")
193-
async def read_forgot_password(
194-
params: dict = Depends(common_unauthenticated_parameters),
195-
show_form: Optional[str] = "true",
196-
):
197-
params["show_form"] = show_form == "true"
198-
199-
return templates.TemplateResponse(params["request"], "authentication/forgot_password.html", params)
200-
201-
202-
@app.get("/about")
203-
async def read_about(params: dict = Depends(common_unauthenticated_parameters)):
204-
return templates.TemplateResponse(params["request"], "about.html", params)
205-
206-
207-
@app.get("/privacy_policy")
208-
async def read_privacy_policy(params: dict = Depends(common_unauthenticated_parameters)):
209-
return templates.TemplateResponse(params["request"], "privacy_policy.html", params)
210-
211-
212-
@app.get("/terms_of_service")
213-
async def read_terms_of_service(params: dict = Depends(common_unauthenticated_parameters)):
214-
return templates.TemplateResponse(params["request"], "terms_of_service.html", params)
215-
216-
217-
@app.get("/auth/reset_password")
218-
async def read_reset_password(
219-
email: str,
220-
token: str,
221-
params: dict = Depends(common_unauthenticated_parameters),
222-
session: Session = Depends(get_session)
223-
):
224-
authorized_user, _ = get_user_from_reset_token(email, token, session)
225-
226-
# Raise informative error to let user know the token is invalid and may have expired
227-
if not authorized_user:
228-
raise HTTPException(status_code=400, detail="Invalid or expired token")
229-
230-
params["email"] = email
231-
params["token"] = token
232-
params["password_pattern"] = HTML_PASSWORD_PATTERN
233-
234-
return templates.TemplateResponse(params["request"], "authentication/reset_password.html", params)
235-
236-
237-
# --- Authenticated Routes ---
238-
239-
240-
# Define a dependency for common parameters
241-
async def common_authenticated_parameters(
242161
request: Request,
243-
user: User = Depends(get_user_with_relations),
244-
error_message: Optional[str] = None
245-
) -> dict:
246-
return {"request": request, "user": user, "error_message": error_message}
247-
248-
249-
# Redirect to home if user is not authenticated
250-
@app.get("/dashboard")
251-
async def read_dashboard(
252-
params: dict = Depends(common_authenticated_parameters)
162+
user: Optional[User] = Depends(get_optional_user)
253163
):
254-
return templates.TemplateResponse(params["request"], "dashboard/index.html", params)
255-
256-
257-
@app.get("/profile")
258-
async def read_profile(
259-
params: dict = Depends(common_authenticated_parameters),
260-
email_update_requested: Optional[str] = "false",
261-
email_updated: Optional[str] = "false"
262-
):
263-
# Add image constraints to the template context
264-
params.update({
265-
"max_file_size_mb": MAX_FILE_SIZE / (1024 * 1024), # Convert bytes to MB
266-
"min_dimension": MIN_DIMENSION,
267-
"max_dimension": MAX_DIMENSION,
268-
"allowed_formats": list(ALLOWED_CONTENT_TYPES.keys()),
269-
"email_update_requested": email_update_requested,
270-
"email_updated": email_updated
271-
})
272-
return templates.TemplateResponse(params["request"], "users/profile.html", params)
273-
274-
275-
@app.get("/organizations/{org_id}")
276-
async def read_organization(
277-
org_id: int,
278-
params: dict = Depends(common_authenticated_parameters)
279-
):
280-
# Get the organization only if the user is a member of it
281-
org = next(
282-
(org for org in params["user"].organizations if org.id == org_id),
283-
None
164+
if user:
165+
return RedirectResponse(url="/dashboard", status_code=302)
166+
return templates.TemplateResponse(
167+
"index.html",
168+
{"request": request, "user": user}
284169
)
285-
if not org:
286-
raise organization.OrganizationNotFoundError()
287-
288-
# Eagerly load roles and users
289-
org.roles
290-
org.users
291-
params["organization"] = org
292-
293-
return templates.TemplateResponse(params["request"], "users/organization.html", params)
294-
295170

296-
# --- Include Routers ---
297-
298-
299-
app.include_router(authentication.router)
300-
app.include_router(organization.router)
301-
app.include_router(role.router)
302-
app.include_router(user.router)
303171

304172
if __name__ == "__main__":
305173
import uvicorn

routers/about.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from typing import Optional
2+
from fastapi import APIRouter, Depends, Request
3+
from fastapi.templating import Jinja2Templates
4+
from utils.auth import get_optional_user
5+
from utils.models import User
6+
7+
router = APIRouter(prefix="/about", tags=["about"])
8+
templates = Jinja2Templates(directory="templates")
9+
10+
@router.get("/")
11+
async def read_about(
12+
request: Request,
13+
user: Optional[User] = Depends(get_optional_user)
14+
):
15+
return templates.TemplateResponse(
16+
"about.html",
17+
{"request": request, "user": user}
18+
)

routers/account.py

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
from datetime import datetime
66
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, Form, Request
77
from fastapi.responses import RedirectResponse
8+
from fastapi.templating import Jinja2Templates
89
from pydantic import BaseModel, EmailStr, ConfigDict
910
from sqlmodel import Session, select
10-
from utils.models import AccountBase, Account, DataIntegrityError, User
11+
from utils.models import User, Account, DataIntegrityError, User
12+
from utils.db import get_session
1113
from utils.auth import (
12-
get_session,
14+
HTML_PASSWORD_PATTERN,
1315
get_user_from_reset_token,
1416
create_password_validator,
1517
create_passwords_match_validator,
@@ -23,12 +25,14 @@
2325
send_email_update_confirmation,
2426
get_user_from_email_update_token,
2527
get_authenticated_user,
26-
PasswordValidationError
28+
PasswordValidationError,
29+
get_optional_user
2730
)
2831

2932
logger = getLogger("uvicorn.error")
3033

3134
router = APIRouter(prefix="/account", tags=["account"])
35+
templates = Jinja2Templates(directory="templates")
3236

3337
# --- Custom Exceptions ---
3438

@@ -52,7 +56,7 @@ def __init__(self, message: str = "Invalid credentials"):
5256
# --- Server Request and Response Models ---
5357

5458

55-
class DeleteAccount(AccountBase):
59+
class DeleteAccount(Account):
5660
@classmethod
5761
async def as_form(
5862
cls,
@@ -62,7 +66,7 @@ async def as_form(
6266
return cls(email=email, password=password)
6367

6468

65-
class CreateAccount(AccountBase):
69+
class CreateAccount(Account):
6670
name: str
6771
password: str
6872
confirm_password: str
@@ -187,6 +191,67 @@ async def delete_account(
187191

188192
# Log out the user
189193
return RedirectResponse(url="/auth/logout", status_code=303)
194+
@router.get("/login")
195+
async def read_login(
196+
request: Request,
197+
user: Optional[User] = Depends(get_optional_user),
198+
email_updated: Optional[str] = "false"
199+
):
200+
if user:
201+
return RedirectResponse(url="/dashboard", status_code=302)
202+
return templates.TemplateResponse(
203+
"authentication/login.html",
204+
{"request": request, "user": user, "email_updated": email_updated}
205+
)
206+
207+
208+
@router.get("/register")
209+
async def read_register(
210+
request: Request,
211+
user: Optional[User] = Depends(get_optional_user)
212+
):
213+
if user:
214+
return RedirectResponse(url="/dashboard", status_code=302)
215+
216+
return templates.TemplateResponse(
217+
"authentication/register.html",
218+
{"request": request, "user": user}
219+
)
220+
221+
222+
@router.get("/forgot_password")
223+
async def read_forgot_password(
224+
request: Request,
225+
user: Optional[User] = Depends(get_optional_user),
226+
show_form: Optional[str] = "true",
227+
):
228+
if user:
229+
return RedirectResponse(url="/dashboard", status_code=302)
230+
231+
return templates.TemplateResponse(
232+
"authentication/forgot_password.html",
233+
{"request": request, "user": user, "show_form": show_form == "true"}
234+
)
235+
236+
237+
@router.get("/reset_password")
238+
async def read_reset_password(
239+
request: Request,
240+
email: str,
241+
token: str,
242+
user: Optional[User] = Depends(get_optional_user),
243+
session: Session = Depends(get_session)
244+
):
245+
authorized_user, _ = get_user_from_reset_token(email, token, session)
246+
247+
# Raise informative error to let user know the token is invalid and may have expired
248+
if not authorized_user:
249+
raise HTTPException(status_code=400, detail="Invalid or expired token")
250+
251+
return templates.TemplateResponse(
252+
"authentication/reset_password.html",
253+
{"request": request, "user": user, "email": email, "token": token, "password_pattern": HTML_PASSWORD_PATTERN}
254+
)
190255

191256

192257
# TODO: Use custom error message in the case where the user is already registered

routers/dashboard.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from typing import Optional
2+
from fastapi import APIRouter, Depends, Request
3+
from fastapi.templating import Jinja2Templates
4+
from utils.auth import get_user_with_relations
5+
from utils.models import User
6+
7+
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
8+
templates = Jinja2Templates(directory="templates")
9+
10+
11+
# --- Authenticated Routes ---
12+
13+
14+
@router.get("/")
15+
async def read_dashboard(
16+
request: Request,
17+
user: Optional[User] = Depends(get_user_with_relations)
18+
):
19+
return templates.TemplateResponse(
20+
"dashboard/index.html",
21+
{"request": request, "user": user}
22+
)

0 commit comments

Comments
 (0)