Skip to content

Commit deb3744

Browse files
committed
refactor
1 parent 055c1ca commit deb3744

File tree

30 files changed

+827
-505
lines changed

30 files changed

+827
-505
lines changed

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
"Bash(npm run lint)",
3838
"Bash(del \"c:\\\\Users\\\\makara\\\\Desktop\\\\full-stack-fastapi-template\\\\frontend\\\\src\\\\routes\\\\unauthorized.tsx\")",
3939
"Bash(del \"c:\\\\Users\\\\makara\\\\Desktop\\\\full-stack-fastapi-template\\\\frontend\\\\src\\\\components\\\\owasp\\\\SettingsButton.tsx\")",
40-
"Bash(find:*)"
40+
"Bash(find:*)",
41+
"Bash(python3:*)"
4142
],
4243
"deny": [],
4344
"ask": []
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(mkdir:*)",
5+
"Bash(python:*)",
6+
"Bash(.venv/Scripts/python.exe:*)"
7+
]
8+
}
9+
}

backend/app/core/config.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ class Settings(BaseSettings):
3232
)
3333
API_V1_STR: str = "/api/v1"
3434
SECRET_KEY: str = secrets.token_urlsafe(32)
35-
# Access token: short-lived (15 minutes)
36-
ACCESS_TOKEN_EXPIRE_MINUTES: int = 15
37-
# Refresh token: long-lived (7 days)
38-
REFRESH_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7
35+
# Access token: short-lived (5 minutes)
36+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 5
37+
# Refresh token: medium-lived (15 minutes)
38+
REFRESH_TOKEN_EXPIRE_MINUTES: int = 15
3939
FRONTEND_HOST: str = "http://localhost:5173"
4040
ENVIRONMENT: Literal["local", "staging", "production"] = "local"
4141

backend/app/core/csrf.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@
2222

2323
# Endpoints exempt from CSRF validation
2424
CSRF_EXEMPT_PATHS = {
25-
"/api/v1/login/access-token",
26-
"/api/v1/login/google",
27-
"/api/v1/login/google/callback",
28-
"/api/v1/login/google/authorize",
29-
"/api/v1/auth/refresh", # Uses refresh_token cookie for validation
25+
"/api/v1/auth/login",
3026
"/api/v1/auth/logout",
31-
"/api/v1/password-recovery",
32-
"/api/v1/reset-password",
27+
"/api/v1/auth/refresh", # Uses refresh_token cookie for validation
28+
"/api/v1/auth/google/login",
29+
"/api/v1/auth/google/callback",
30+
"/api/v1/auth/google/authorize",
31+
"/api/v1/auth/password/recover",
32+
"/api/v1/auth/password/reset",
3333
"/api/v1/users/signup",
3434
"/docs",
3535
"/redoc",

backend/app/core/db/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Database module
2+
#
3+
# Usage:
4+
# from app.core.db import engine # Always safe
5+
# from app.core.db.init import init_db # Import separately to avoid circular deps
6+
7+
from app.core.db.engine import engine
8+
9+
__all__ = ["engine"]

backend/app/core/db/engine.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from sqlmodel import create_engine
2+
3+
from app.core.config import settings
4+
5+
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,24 @@
11
import logging
22

3-
from sqlmodel import Session, create_engine, select
3+
from sqlmodel import Session, select
44

55
from app.core.config import settings
6+
from app.modules.items.models import Item
7+
from app.modules.rbac.models import Role
8+
from app.modules.rbac.service import UserRoleService
9+
from app.modules.users.models import User
10+
from app.modules.users.repository import UserRepository
11+
from app.modules.users.schemas import UserCreate
612

713
logger = logging.getLogger(__name__)
814

9-
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
10-
11-
12-
# make sure all SQLModel models are imported (app.modules) before initializing DB
13-
# otherwise, SQLModel might fail to initialize relationships properly
14-
# for more details: https://github.com/fastapi/full-stack-fastapi-template/issues/28
15-
1615

1716
def init_db(session: Session) -> None:
18-
# Import models and repository here to avoid circular imports
19-
from app.modules.items.models import Item
20-
from app.modules.rbac.models import Role
21-
from app.modules.rbac.service import UserRoleService
22-
from app.modules.users.models import User
23-
from app.modules.users.repository import UserRepository
24-
from app.modules.users.schemas import UserCreate
25-
2617
# Tables should be created with Alembic migrations
2718
# But if you don't want to use migrations, create
2819
# the tables un-commenting the next lines
2920
# from sqlmodel import SQLModel
21+
# from app.core.db.engine import engine
3022
# SQLModel.metadata.create_all(engine)
3123

3224
user = session.exec(
@@ -85,8 +77,6 @@ def init_db(session: Session) -> None:
8577

8678
def _seed_demo_items(session: Session, admin_user, demo_user) -> None:
8779
"""Seed demo items for OWASP A01 demonstrations."""
88-
from app.modules.items.models import Item
89-
9080
# Check if demo items already exist
9181
existing_items = session.exec(select(Item)).all()
9282
if len(existing_items) >= 6:

backend/app/initial_data.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
from sqlmodel import Session
44

5-
from app.core.db import engine, init_db
5+
from app.core.db import engine
6+
from app.core.db.init import init_db
67

78
logging.basicConfig(level=logging.INFO)
89
logger = logging.getLogger(__name__)

backend/app/modules/auth/routes.py

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
GoogleCallbackRequest,
1212
GoogleLoginRequest,
1313
NewPassword,
14-
Token,
14+
PasswordRecoveryRequest,
1515
TokenResponse,
1616
)
1717
from app.modules.auth.service import AuthService
@@ -23,16 +23,16 @@
2323
)
2424
from app.modules.users.schemas import UserPublic
2525

26-
router = APIRouter(tags=["login"])
26+
router = APIRouter(tags=["auth"])
2727

2828

2929
def generate_csrf_token() -> str:
3030
"""Generate a cryptographically secure CSRF token."""
3131
return secrets.token_urlsafe(32)
3232

3333

34-
@router.post("/login/access-token")
35-
def login_access_token(
34+
@router.post("/auth/login")
35+
def auth_login(
3636
response: Response,
3737
session: SessionDep,
3838
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
@@ -67,7 +67,7 @@ def login_access_token(
6767

6868

6969
@router.post("/auth/refresh")
70-
def refresh_token(
70+
def auth_refresh(
7171
response: Response,
7272
session: SessionDep,
7373
refresh_token: Annotated[str | None, Cookie()] = None,
@@ -104,7 +104,7 @@ def refresh_token(
104104

105105

106106
@router.post("/auth/logout")
107-
def logout(response: Response) -> Message:
107+
def auth_logout(response: Response) -> Message:
108108
"""
109109
Logout user by clearing authentication cookies.
110110
"""
@@ -117,45 +117,45 @@ def logout(response: Response) -> Message:
117117
return Message(message="Successfully logged out")
118118

119119

120-
@router.post("/login/test-token", response_model=UserPublic)
121-
def test_token(current_user: CurrentUser) -> Any:
122-
"""Test access token."""
120+
@router.post("/auth/verify", response_model=UserPublic)
121+
def auth_verify(current_user: CurrentUser) -> Any:
122+
"""Verify access token and return current user."""
123123
return current_user
124124

125125

126-
@router.post("/password-recovery/{email}")
127-
def recover_password(email: str, session: SessionDep) -> Message:
128-
"""Password recovery."""
126+
@router.post("/auth/password/recover")
127+
def auth_password_recover(session: SessionDep, body: PasswordRecoveryRequest) -> Message:
128+
"""Request password recovery email."""
129129
service = AuthService(session)
130-
service.recover_password(email=email)
130+
service.recover_password(email=body.email)
131131
return Message(message="Password recovery email sent")
132132

133133

134-
@router.post("/reset-password/")
135-
def reset_password(session: SessionDep, body: NewPassword) -> Message:
136-
"""Reset password."""
134+
@router.post("/auth/password/reset")
135+
def auth_password_reset(session: SessionDep, body: NewPassword) -> Message:
136+
"""Reset password with token."""
137137
service = AuthService(session)
138138
service.reset_password(token=body.token, new_password=body.new_password)
139139
return Message(message="Password updated successfully")
140140

141141

142142
@router.post(
143-
"/password-recovery-html-content/{email}",
143+
"/auth/password/recover-html",
144144
dependencies=[Depends(get_current_active_superuser)],
145145
response_class=HTMLResponse,
146146
)
147-
def recover_password_html_content(email: str, session: SessionDep) -> Any:
148-
"""HTML content for password recovery."""
147+
def auth_password_recover_html(session: SessionDep, body: PasswordRecoveryRequest) -> Any:
148+
"""HTML content for password recovery (admin only)."""
149149
service = AuthService(session)
150-
html_content, subject = service.get_password_recovery_html(email=email)
150+
html_content, subject = service.get_password_recovery_html(email=body.email)
151151
return HTMLResponse(content=html_content, headers={"subject:": subject})
152152

153153

154-
@router.post("/login/google")
155-
def login_google(
154+
@router.post("/auth/google/login")
155+
def auth_google_login(
156156
response: Response, session: SessionDep, body: GoogleLoginRequest
157157
) -> TokenResponse:
158-
"""Google OAuth login with HttpOnly cookie tokens."""
158+
"""Google OAuth login with ID token. Sets HttpOnly cookie tokens."""
159159
try:
160160
service = AuthService(session)
161161
user, access_token, refresh_token = service.login_google(id_token=body.id_token)
@@ -185,8 +185,8 @@ def login_google(
185185
)
186186

187187

188-
@router.get("/login/google/authorize")
189-
def google_authorize() -> RedirectResponse:
188+
@router.get("/auth/google/authorize")
189+
def auth_google_authorize() -> RedirectResponse:
190190
"""Initiate Google OAuth flow by redirecting to Google's authorization page."""
191191
if not settings.GOOGLE_CLIENT_ID:
192192
raise HTTPException(
@@ -207,11 +207,11 @@ def google_authorize() -> RedirectResponse:
207207
return RedirectResponse(url=auth_url)
208208

209209

210-
@router.post("/login/google/callback")
211-
def google_callback(
210+
@router.post("/auth/google/callback")
211+
def auth_google_callback(
212212
response: Response, session: SessionDep, body: GoogleCallbackRequest
213213
) -> TokenResponse:
214-
"""Handle Google OAuth callback with HttpOnly cookie tokens."""
214+
"""Handle Google OAuth callback with authorization code. Sets HttpOnly cookie tokens."""
215215
try:
216216
service = AuthService(session)
217217
user, access_token, refresh_token = service.google_callback(code=body.code)

backend/app/modules/auth/schemas.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ class NewPassword(SQLModel):
2929
new_password: str = Field(min_length=8, max_length=128)
3030

3131

32+
class PasswordRecoveryRequest(SQLModel):
33+
"""Password recovery request."""
34+
35+
email: str
36+
37+
3238
class GoogleLoginRequest(SQLModel):
3339
"""Google login with ID token request."""
3440

0 commit comments

Comments
 (0)