Files
LynkedUpPro_CRM/docs/diagrams/lld_i1_auth_integration.md
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

14 KiB

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.

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.

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.

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.

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.

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.

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.

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.

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.

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