# LLD — I1 Auth Integration **Scope:** Replacing mock AuthContext with real JWT flow — state machine, session restore, login/logout sequences, ProtectedRoute guard **Companion doc:** `docs/integration/01_auth_integration.md` --- ## Diagram 1 — Before vs After: AuthContext Architecture A side-by-side view of what changes and what stays the same. ```mermaid flowchart LR subgraph Before["BEFORE — Mock AuthContext"] direction TB B_MOCK["mockStore.users\n(17 in-memory users)"] B_LOGIN["login(id, pass, type)\nSynchronous\nusers.find() in JS"] B_STATE["useState:\nuser = null\nisAuthenticated = false\n(no isLoading)\n(no session restore)"] B_LOGOUT["logout()\nSynchronous\nsetUser(null)"] B_EXPORT["useAuth() exports:\nuser, isAuthenticated\nlogin, logout"] B_MOCK --> B_LOGIN B_LOGIN --> B_STATE B_STATE --> B_LOGOUT B_LOGOUT --> B_EXPORT end subgraph After["AFTER — Real JWT AuthContext"] direction TB A_API["src/api/auth.js\nloginApi()\ngetMeApi()\nlogoutApi()"] A_LOGIN["login(id, pass, type)\nAsync\nPOST /auth/login"] A_STATE["useState:\nuser = null\nisAuthenticated = false\nisLoading = true ← NEW"] A_MOUNT["useEffect (mount)\nGET /auth/me\n→ session restore"] A_FORCE["useEffect\nwindow 'auth:logout-required'\n← from interceptors.js"] A_LOGOUT["logout()\nAsync\nPOST /auth/logout\nthen setUser(null)"] A_EXPORT["useAuth() exports:\nuser, isAuthenticated\nisLoading ← NEW\nlogin, logout"] A_API --> A_LOGIN A_LOGIN --> A_STATE A_STATE --> A_MOUNT A_STATE --> A_FORCE A_STATE --> A_LOGOUT A_LOGOUT --> A_EXPORT end Before -->|"I1 migration"| After subgraph Unchanged["Components — NO changes needed"] U1["All pages reading\nuseAuth().user\nuseAuth().isAuthenticated"] U2["fillDemo() buttons\nin Login.jsx"] U3["ROLES constant\n(same 7 values)"] end ``` --- ## Diagram 2 — AuthContext State Machine The three states `AuthProvider` can be in, and the events that cause transitions. ```mermaid stateDiagram-v2 [*] --> Loading : AuthProvider mounts\nisLoading = true Loading --> Authenticated : GET /auth/me → 200\nOR refresh succeeded\nsetUser(data)\nsetIsAuthenticated(true)\nsetIsLoading(false) Loading --> Unauthenticated : GET /auth/me → 401\nAND refresh failed\nOR no cookie present\nsetUser(null)\nsetIsAuthenticated(false)\nsetIsLoading(false) Unauthenticated --> Authenticated : login() called\nPOST /auth/login → 200\nsetUser(data.user)\nsetIsAuthenticated(true) Authenticated --> Unauthenticated : logout() called\nPOST /auth/logout\nsetUser(null)\nsetIsAuthenticated(false) Authenticated --> Unauthenticated : 'auth:logout-required' event\ndispatched by interceptors.js\nwhen refresh token expired/invalid\nsetUser(null)\nsetIsAuthenticated(false) note right of Loading ProtectedRoute renders a spinner while in this state — never redirects to /login until Loading exits. end note note right of Authenticated user object available. Cookie present in browser. All API calls succeed. end note note right of Unauthenticated user = null. No cookies. ProtectedRoute redirects to /login. end note ``` --- ## Diagram 3 — Session Restore on Mount The exact sequence when the app loads and finds an existing session cookie. ```mermaid sequenceDiagram participant Browser as Browser participant App as App.jsx (React mount) participant Auth as AuthProvider (useEffect) participant API as GET /auth/me participant Inter as interceptors.js participant BE as FastAPI Browser->>App: Page load / refresh App->>Auth: AuthProvider mounts\nisLoading = true Auth->>API: getMeApi()\napiClient.get('/auth/me') Note over API: withCredentials=true\nbrowser attaches access_token cookie API->>BE: GET /api/v1/auth/me\nCookie: access_token= alt Access token valid BE-->>API: 200 UserPublic { id, role, full_name, ... } API-->>Auth: data = UserPublic Auth->>Auth: setUser(data)\nsetIsAuthenticated(true)\nsetIsLoading(false) Note over App: ProtectedRoute:\nisLoading=false, isAuthenticated=true\n→ render page else Access token expired (401) BE-->>API: 401 API-->>Inter: Response interceptor triggered Inter->>BE: POST /auth/refresh\nCookie: refresh_token= alt Refresh succeeds BE-->>Inter: 200 new access_token cookie set Inter->>BE: Retry GET /auth/me (with new cookie) BE-->>Inter: 200 UserPublic Inter-->>Auth: data = UserPublic Auth->>Auth: setUser(data)\nsetIsAuthenticated(true)\nsetIsLoading(false) else Refresh also fails BE-->>Inter: 401 Inter->>Inter: dispatch('auth:logout-required') Inter-->>Auth: Promise rejected Auth->>Auth: catch block:\nsetUser(null)\nsetIsAuthenticated(false)\nsetIsLoading(false) Note over App: ProtectedRoute:\nisLoading=false, isAuthenticated=false\n→ Navigate /login end else No cookie at all (first visit / after logout) BE-->>API: 401 (no cookie) Note over Inter: Interceptor checks _retry flag\nalready retried or no refresh cookie\n→ propagates error API-->>Auth: catch block:\nsetUser(null)\nsetIsAuthenticated(false)\nsetIsLoading(false) Note over App: ProtectedRoute → Navigate /login end ``` --- ## Diagram 4 — Login Flow (Async) The new async login sequence from form submit to page redirect. ```mermaid sequenceDiagram actor User as 👤 User participant Form as Login.jsx form participant AuthCtx as AuthContext.login() participant AuthAPI as src/api/auth.js loginApi() participant Client as apiClient participant BE as POST /auth/login User->>Form: Fill identifier + password\nselect loginType tab\nclick Sign In Form->>Form: setIsSubmitting(true)\ndisable button Form->>AuthCtx: await login(identifier, password, loginType) AuthCtx->>AuthAPI: loginApi(identifier, password, type) AuthAPI->>Client: apiClient.post('/auth/login',\n{ identifier, password, type }) Client->>BE: POST /api/v1/auth/login\n{ identifier, password, type } alt Credentials valid BE-->>Client: 200 { user: UserPublic, message: "Login successful" }\nSet-Cookie: access_token=; httpOnly\nSet-Cookie: refresh_token=; httpOnly; path=/api/v1/auth Client-->>AuthAPI: { data: { user, message } } AuthAPI-->>AuthCtx: data AuthCtx->>AuthCtx: setUser(data.user)\nsetIsAuthenticated(true) AuthCtx->>AuthCtx: toast.success('Welcome back, full_name!') AuthCtx-->>Form: { success: true, role: 'FIELD_AGENT' } Form->>Form: setIsSubmitting(false) Form->>Form: switch(result.role)\nnavigate('/emp/fa/dashboard') else Invalid credentials BE-->>Client: 401 { detail: "Invalid credentials" } Client-->>AuthAPI: AxiosError (401) AuthAPI-->>AuthCtx: throws AuthCtx->>AuthCtx: message = error.response.data.detail\ntost.error('Invalid credentials') AuthCtx-->>Form: { success: false, message: "Invalid credentials" } Form->>Form: setIsSubmitting(false)\nsetError(result.message) Form-->>User: Error banner shown end ``` --- ## Diagram 5 — Role Redirect Decision Tree (Login.jsx) What happens after a successful login response based on `result.role`. ```mermaid flowchart TD SUCCESS["login() returns\n{ success: true, role }"] SUCCESS --> SW{result.role} SW -->|CUSTOMER| R1["navigate('/portal/profile')\n← FIXED from '/' (bug)"] SW -->|OWNER| R2["navigate('/owner/snapshot')"] SW -->|ADMIN| R3["navigate('/admin/dashboard')\n← FIXED from default (bug)"] SW -->|CONTRACTOR| R4["navigate('/contractor/dashboard')"] SW -->|VENDOR| R5["navigate('/vendor/dashboard')"] SW -->|SUBCONTRACTOR| R6["navigate('/subcontractor/dashboard')"] SW -->|default FIELD_AGENT| R7["navigate('/emp/fa/dashboard')"] subgraph Bugs["Two redirect bugs fixed by I1"] B1["CUSTOMER: '/' → Landing page\nShould be '/portal/profile'"] B2["ADMIN: fell to default → /emp/fa/dashboard\nShould be '/admin/dashboard'"] end subgraph FillDemo["fillDemo() — unchanged"] FD["fillDemo('customer') sets loginType + identifier + password\nForm submits normally → handleLogin runs\nSame redirect logic applies"] end ``` --- ## Diagram 6 — Logout Flow The new async logout — API call first, state clear regardless of outcome. ```mermaid sequenceDiagram actor User as 👤 User participant UI as Any component calling logout() participant AuthCtx as AuthContext.logout() participant AuthAPI as src/api/auth.js logoutApi() participant BE as POST /auth/logout User->>UI: Click logout button UI->>AuthCtx: await logout() AuthCtx->>AuthAPI: logoutApi() AuthAPI->>BE: POST /api/v1/auth/logout\nCookie: access_token= alt Logout API succeeds BE-->>AuthAPI: 200 { message: "Logged out successfully" }\nSet-Cookie: access_token=; max_age=0 (cleared)\nSet-Cookie: refresh_token=; max_age=0 (cleared) Note over BE: Also sets users.refresh_token_hash = NULL in DB AuthAPI-->>AuthCtx: success else Logout API fails (token already expired) BE-->>AuthAPI: 401 AuthAPI-->>AuthCtx: error caught — logged as warning Note over AuthCtx: Still clears local state below end AuthCtx->>AuthCtx: finally block:\nsetUser(null)\nsetIsAuthenticated(false)\ntoast.info('Logged out successfully') AuthCtx-->>UI: resolved Note over UI: ProtectedRoute detects\nisAuthenticated=false\n→ Navigate to /login ``` --- ## Diagram 7 — Forced Logout Event Flow When the refresh token expires or a reuse attack is detected, the interceptor forces a logout without any user action. ```mermaid sequenceDiagram participant Comp as Any React Component participant Client as apiClient participant Inter as interceptors.js participant BE_API as Any API endpoint participant BE_REF as POST /auth/refresh participant Auth as AuthContext Comp->>Client: apiClient.get('/properties') Client->>BE_API: GET /api/v1/properties\n(access_token expired) BE_API-->>Client: 401 Client->>Inter: Response interceptor triggered Note over Inter: _retry = false → attempt refresh Inter->>BE_REF: POST /auth/refresh\nCookie: refresh_token= alt Refresh token also expired BE_REF-->>Inter: 401 "Invalid or expired refresh token" else Reuse attack detected BE_REF-->>Inter: 401 "Refresh token reuse detected" Note over BE_REF: DB: users.refresh_token_hash = NULL\nAll sessions revoked end Inter->>Inter: processQueue(error)\nfailure on all queued requests Inter->>Inter: window.dispatchEvent(\n new CustomEvent('auth:logout-required')\n) Inter-->>Comp: Promise.reject(refreshError) Note over Auth: 'auth:logout-required' event listener\n(registered in AuthProvider useEffect) Auth->>Auth: handleForceLogout()\nsetUser(null)\nsetIsAuthenticated(false)\ntoast.error('Session expired') Note over Comp: ProtectedRoute detects\nisAuthenticated=false\n→ Navigate /login ``` --- ## Diagram 8 — `ProtectedRoute` Decision Tree (With `isLoading`) The updated guard logic in `App.jsx`. The `isLoading` check is the only change. ```mermaid flowchart TD MOUNT["ProtectedRoute renders\n(on every navigation + page refresh)"] MOUNT --> LOADING{isLoading?} LOADING -->|true| SPINNER["Render full-screen spinner\n(session check in progress)\nDo NOT redirect yet"] LOADING -->|false| AUTH{isAuthenticated?} AUTH -->|false| REDIRECT_LOGIN[""] AUTH -->|true| ROLES{allowedRoles\nprovided?} ROLES -->|No — public-ish protected route| RENDER["Render children"] ROLES -->|Yes| MATCH{user.role\nin allowedRoles?} MATCH -->|Yes| RENDER2["Render children"] MATCH -->|No — wrong role| REDIRECT_HOME["\n(redirects to Landing page)"] subgraph WhyIsLoadingMatters["Why isLoading matters"] W1["Without isLoading guard:\nPage refresh → isAuthenticated=false (initial state)\n→ ProtectedRoute immediately redirects to /login\n→ Session restore completes 200ms later\n→ User sees login flash before their page loads"] W2["With isLoading guard:\nPage refresh → spinner shown\n→ Session restore completes\n→ isLoading=false, isAuthenticated=true\n→ Protected page renders normally"] end ``` --- ## Diagram 9 — User Object Shape Transformation How the mock user fields map to real API fields, and which components are affected. ```mermaid flowchart LR subgraph MockShape["Mock Store User Shape (camelCase)"] M1["id: 'e1' (string, not UUID)"] M2["name: 'Alice Johnson'"] M3["role: 'CUSTOMER'"] M4["empId: 'FA001'"] M5["type: 'customer'"] M6["xp: 3400"] M7["streak: 7"] M8["company: 'ABC Corp'"] M9["password: 'password' ← in memory!"] end subgraph RealShape["Real API UserPublic Shape (snake_case)"] R1["id: '3f8a1c2d-...' (UUID string)"] R2["full_name: 'Alice Johnson'"] R3["role: 'CUSTOMER'"] R4["emp_id: null (CUSTOMER has no emp_id)"] R5["legacy_id: 'cus_001'"] R6["xp: 3400"] R7["streak_days: 7"] R8["company_name: 'ABC Corp'"] R9["(password never returned)"] end subgraph I1Fixes["Fixed in I1"] F1["AuthContext toast:\nuser.name → user.full_name"] F2["Layout.jsx sidebar:\nuser.name → user.full_name"] F3["CustomerProfile.jsx header:\nuser.name → user.full_name"] F4["Chatbot.jsx greeting:\nuser.name → user.full_name"] end subgraph LaterFixes["Fixed in later modules"] L1["user.empId → user.emp_id\n(I4 — Users integration)"] L2["user.streak → user.streak_days\n(I4 — Users integration)"] L3["user.id from 'e1' → UUID\n(via legacy_id bridge — I2)"] end MockShape -->|"I1 replaces"| RealShape RealShape --> I1Fixes RealShape --> LaterFixes ```