samar m commited on
Commit
ec8498d
Β·
1 Parent(s): 7910b95

docs: add Phase 1 auth system design spec

Browse files
docs/superpowers/specs/2026-03-28-phase-1-auth-design.md ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Phase 1 β€” Auth System Design
2
+
3
+ **Date:** 2026-03-28
4
+ **Status:** Approved
5
+
6
+ ## Goal
7
+
8
+ Implement full JWT-based authentication: backend routes, DB queries, password hashing, token management, and frontend login/signup UI with Zustand state.
9
+
10
+ ## Backend
11
+
12
+ ### Files
13
+ - `backend/auth/password.py` β€” bcrypt hash (cost=12) and verify
14
+ - `backend/auth/jwt.py` β€” sign and verify HS256 access token (15min); generate opaque refresh token string
15
+ - `backend/db/connection.py` β€” asyncpg connection pool, initialised via `init_db_pool()` on startup
16
+ - `backend/db/queries.py` β€” all SQL: user insert, user lookup by email, refresh token insert/lookup/delete
17
+ - `backend/routers/auth.py` β€” four routes with request/response Pydantic models
18
+ - `backend/main.py` β€” wire routers, call `init_db_pool()` in lifespan
19
+
20
+ ### Auth Flow
21
+
22
+ ```
23
+ SIGNUP POST /api/auth/signup
24
+ Body: { full_name, email, password, role, class_code? }
25
+ β†’ if role=student: lookup batch by class_code, get batch_id (400 if not found)
26
+ β†’ bcrypt hash password (cost=12)
27
+ β†’ INSERT user
28
+ β†’ sign access token { user_id, role, batch_id, email, exp }
29
+ β†’ generate refresh token string β†’ hash β†’ INSERT refresh_tokens
30
+ β†’ Set-Cookie: refresh_token=<raw> (httpOnly, SameSite=Lax)
31
+ β†’ return { access_token, user: { id, role, full_name, batch_id } }
32
+
33
+ LOGIN POST /api/auth/login
34
+ Body: { email, password }
35
+ β†’ lookup user by email (401 if not found)
36
+ β†’ bcrypt verify (401 if wrong)
37
+ β†’ same token flow as signup
38
+
39
+ REFRESH POST /api/auth/refresh
40
+ β†’ read refresh_token cookie (401 if missing)
41
+ β†’ hash cookie value β†’ lookup in refresh_tokens (401 if not found or expired)
42
+ β†’ sign new access token
43
+ β†’ return { access_token }
44
+
45
+ LOGOUT POST /api/auth/logout
46
+ β†’ read refresh_token cookie
47
+ β†’ DELETE from refresh_tokens where token_hash matches
48
+ β†’ clear cookie
49
+ β†’ return { ok: true }
50
+ ```
51
+
52
+ ### Route Protection (added to main.py, used in Phase 2+)
53
+
54
+ ```python
55
+ async def get_current_user(token: str = Depends(oauth2_scheme)) -> dict:
56
+ # verify JWT, return payload
57
+ # raises 401 on failure
58
+
59
+ async def require_instructor(user = Depends(get_current_user)) -> dict: ...
60
+ async def require_student(user = Depends(get_current_user)) -> dict: ...
61
+ ```
62
+
63
+ ## Frontend
64
+
65
+ ### Files
66
+ - `frontend/src/api/client.ts` β€” base `apiFetch`: injects Bearer token, retries once on 401 after refresh
67
+ - `frontend/src/api/auth.ts` β€” `signup()`, `login()`, `logout()`, `refreshToken()`
68
+ - `frontend/src/store/authStore.ts` β€” Zustand store, persisted to localStorage
69
+ - `frontend/src/App.tsx` β€” React Router v6, role-based redirect, ProtectedRoute
70
+ - `frontend/src/pages/Login.tsx` β€” dark card form, error state, loading button
71
+ - `frontend/src/pages/Signup.tsx` β€” same style + role toggle + conditional class code field
72
+
73
+ ### Zustand Store Shape
74
+
75
+ ```typescript
76
+ interface AuthStore {
77
+ accessToken: string | null
78
+ user: { id: string; role: string; full_name: string; batch_id: string | null } | null
79
+ setAuth: (token: string, user: User) => void
80
+ clearAuth: () => void
81
+ }
82
+ // persisted to localStorage via zustand/middleware persist
83
+ ```
84
+
85
+ ### apiFetch Behaviour
86
+
87
+ 1. Get `accessToken` from `authStore`
88
+ 2. Fetch with `Authorization: Bearer <token>` + `credentials: 'include'`
89
+ 3. If response is 401 β†’ call `refreshToken()` β†’ retry original request once
90
+ 4. If retry also 401 β†’ `clearAuth()` + redirect to `/login`
91
+
92
+ ### Routing (App.tsx)
93
+
94
+ ```
95
+ /login β†’ Login (redirect to dashboard if already authed)
96
+ /signup β†’ Signup (redirect to dashboard if already authed)
97
+ /student/* β†’ ProtectedRoute (role=student)
98
+ /instructor/* β†’ ProtectedRoute (role=instructor)
99
+ / β†’ redirect based on role
100
+ ```
101
+
102
+ ### UI Spec (Login + Signup)
103
+
104
+ - Dark card: `bg-gray-900 border border-gray-800 rounded-xl p-8 w-full max-w-md`
105
+ - Page bg: `min-h-screen bg-gray-950 flex items-center justify-center`
106
+ - Input: `bg-gray-800 border border-gray-700 rounded-lg px-4 py-2 text-white w-full`
107
+ - Button: `bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg px-4 py-2 w-full`
108
+ - Loading: button disabled + "..." text
109
+ - Error: red text below form `text-red-400 text-sm`
110
+ - Signup role toggle: two buttons (Student / Instructor), active = indigo fill
111
+ - Class code field: visible only when role = student
112
+
113
+ ## Key Decisions
114
+
115
+ - Refresh token stored as raw string in cookie; only the bcrypt hash goes to DB β€” cookie theft alone is not enough
116
+ - `apiFetch` handles token refresh transparently β€” pages never deal with 401 manually
117
+ - Zustand `persist` middleware saves auth to localStorage so page refresh doesn't log users out
118
+ - `class_code` lookup happens at signup time; `batch_id` is embedded in the JWT so subsequent requests don't need a DB lookup for basic auth