Files
LynkedUpPro_CRM/docs/diagrams/lld_i1_auth_integration.md
T
Satyam 7694788387 feat(ProCanvas): retro game overhaul with log actions, challenges & achievements modals
- Complete ProCanvas redesign with retro sports game aesthetic
- Card-game style photo frame with tilt, shimmer, corner star accents
- Daily Missions card with weekly challenge + individual quest progress bars
- Log Action card (Door Knocked, Lead Gained, Appointment Set, Client Meeting)
- Each log action opens themed modal with relevant input fields
- Challenges modal with 6 active challenges and progress tracking
- Achievements modal with all badges, unlock status, descriptions
- Nav bar tabs (Leaderboard, Challenges, Achievements) wired to modals
- Rewards & Checkpoints with named stages (Daily Grind to Legend Run)
- Smoother Hot Streak pulse animation (2.5s float + 3s pulse rings)
- Hit The Map button with smooth pulsing glow animation
- Grid pattern overlay in Leaderboard card
- Light mode support via dark: Tailwind variants throughout
- Top 3 badges displayed on profile card
- Fixed dropdown option visibility in dark mode
2026-02-26 01:36:36 +05:30

382 lines
14 KiB
Markdown

# 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=<jwt>
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=<jwt>
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=<jwt>; httpOnly\nSet-Cookie: refresh_token=<jwt>; 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=<jwt>
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=<jwt>
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["<Navigate to='/login'\nstate={{ from: location }}\nreplace />"]
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["<Navigate to='/' replace />\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
```