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

15 KiB
Raw Permalink Blame History

LLD — B1 Authentication Module

Scope: JWT auth, RBAC dependencies, cookie strategy, refresh token rotation, permissions, data masking Companion doc: docs/backend/01_authentication_module.md


Diagram 1 — Auth Module File Dependency Graph

How the four auth files relate to each other and to the rest of the backend.

graph TD
    subgraph AuthFiles["B1 — Files Created"]
        SEC["security.py\nhash_password()\nverify_password()\ncreate_access_token()\ncreate_refresh_token()\ndecode_token()\nhash_token()\nverify_token_hash()"]
        AUTH_SVC["auth_service.py\nauthenticate_user()\ncreate_token_pair()"]
        DEPS["dependencies.py\nget_db()\nget_current_user()\nrequire_role(*roles)"]
        AUTH_RT["routers/auth.py\nPOST /auth/login\nPOST /auth/refresh\nPOST /auth/logout\nGET  /auth/me"]
    end

    subgraph Utils["Utilities"]
        PERM["permissions.py\nROLE_PERMISSIONS\nhas_permission()"]
        MASK["mask.py\nmask_ssn()\nmask_bank_account()\nmask_ein()"]
    end

    subgraph External["External Dependencies"]
        PASSLIB["passlib[bcrypt]\npwd_context"]
        JOSE["python-jose\njwt.encode/decode"]
        CFG["config.py\nsettings.secret_key\nsettings.algorithm\nsettings.access_token_expire_minutes\nsettings.refresh_token_expire_days"]
        USER_MODEL["models/user.py\nUser ORM model"]
    end

    subgraph AllRouters["All Other Routers (B3B8)"]
        R3["properties.py"]
        R4["users.py"]
        R5["meetings.py"]
        R6["vendors.py"]
        R7["invoices.py"]
        R8["chatbot.py"]
    end

    SEC --> PASSLIB
    SEC --> JOSE
    SEC --> CFG
    AUTH_SVC --> SEC
    AUTH_SVC --> USER_MODEL
    DEPS --> SEC
    DEPS --> USER_MODEL
    AUTH_RT --> AUTH_SVC
    AUTH_RT --> DEPS
    AUTH_RT --> PERM

    DEPS -->|"require_role() imported by"| R3 & R4 & R5 & R6 & R7 & R8
    MASK -->|"called by"| R4

    style SEC fill:#1e3a5f,color:#fff
    style AUTH_SVC fill:#1e3a5f,color:#fff
    style DEPS fill:#1e5f3a,color:#fff
    style AUTH_RT fill:#3a1e5f,color:#fff
    style PERM fill:#5f3a1e,color:#fff
    style MASK fill:#5f3a1e,color:#fff

Diagram 2 — Password Hashing Lifecycle

Two separate contexts where password functions are called: user creation / seed and login verification.

flowchart LR
    subgraph Creation["User Creation / Seed Script"]
        PLAIN1["plain: 'password'"]
        HASH_FN["hash_password(plain)\npwd_context.hash()"]
        DB_STORE["users.password_hash\n'$2b$12$...(60 chars)'"]

        PLAIN1 --> HASH_FN --> DB_STORE
    end

    subgraph Login["Login Verification"]
        PLAIN2["plain from request\nrequest.password"]
        DB_READ["users.password_hash\nfrom DB row"]
        VERIFY["verify_password(plain, hashed)\npwd_context.verify()"]
        TRUE["True → proceed"]
        FALSE["False → return None\n→ 401 Invalid credentials"]

        PLAIN2 --> VERIFY
        DB_READ --> VERIFY
        VERIFY -->|match| TRUE
        VERIFY -->|no match| FALSE
    end

    subgraph NeverDo["❌ Never Do This"]
        BAD1["store plain text\nin DB"]
        BAD2["compare strings directly\nplain == stored"]
        BAD3["use MD5 / SHA1\nfor passwords"]
    end

Diagram 3 — JWT Token Anatomy

Side-by-side view of what's inside each token type and where it lives.

flowchart LR
    subgraph AccessToken["Access Token — 15 min"]
        direction TB
        AT_HDR["Header\n{ alg: HS256, typ: JWT }"]
        AT_PAY["Payload\n{\n  sub: '3f8a1c2d-...(UUID)',\n  role: 'FIELD_AGENT',\n  legacy_id: 'e1',\n  type: 'access',\n  iat: 1740400000,\n  exp: 1740400900\n}"]
        AT_SIG["Signature\nHMAC-SHA256(\n  base64(header) + '.' + base64(payload),\n  SECRET_KEY\n)"]
        AT_COOKIE["Cookie: access_token\nhttpOnly=true\nsecure=true\nsamesite=strict\npath=/\nmax_age=900s"]

        AT_HDR --> AT_PAY --> AT_SIG --> AT_COOKIE
    end

    subgraph RefreshToken["Refresh Token — 7 days"]
        direction TB
        RT_HDR["Header\n{ alg: HS256, typ: JWT }"]
        RT_PAY["Payload\n{\n  sub: '3f8a1c2d-...(UUID)',\n  type: 'refresh',\n  iat: 1740400000,\n  exp: 1741004800\n}"]
        RT_SIG["Signature\nHMAC-SHA256(...)"]
        RT_COOKIE["Cookie: refresh_token\nhttpOnly=true\nsecure=true\nsamesite=strict\npath=/api/v1/auth ← scoped\nmax_age=604800s"]
        RT_DB["DB: users.refresh_token_hash\nbcrypt hash of token string\n(not the token itself)"]

        RT_HDR --> RT_PAY --> RT_SIG --> RT_COOKIE
        RT_SIG -.->|"hash stored in"| RT_DB
    end

    note1["Access token has role + legacy_id\nso routers don't need extra DB lookup\nfor these common fields"]
    note2["Refresh token path scoped to /api/v1/auth\nso it is NOT sent with every API request —\nreduces exposure surface"]

Diagram 4 — Login Type Decision Tree

How authenticate_user() maps the frontend's loginType to a DB query strategy.

flowchart TD
    START["authenticate_user(\n  db, identifier, password, login_type\n)"]

    START --> LT{login_type?}

    LT -->|"'customer'"| CUS["SELECT FROM users\nWHERE username = identifier\nAND role = 'CUSTOMER'\nAND deleted_at IS NULL\nAND is_active = true"]

    LT -->|"'employee'"| EMP["SELECT FROM users\nWHERE emp_id = identifier\nAND role IN ('FIELD_AGENT','ADMIN')\nAND deleted_at IS NULL\nAND is_active = true"]

    LT -->|"'owner'"| OWN["SELECT FROM users\nWHERE (username = identifier\n  OR email = identifier)\nAND role = 'OWNER'\nAND deleted_at IS NULL\nAND is_active = true"]

    LT -->|"'contractor'"| CON["SELECT FROM users\nWHERE (username = identifier\n  OR email = identifier)\nAND role = 'CONTRACTOR'\nAND deleted_at IS NULL\nAND is_active = true"]

    LT -->|"'subcontractor'"| SUB["SELECT FROM users\nWHERE (username = identifier\n  OR email = identifier)\nAND role = 'SUBCONTRACTOR'\nAND deleted_at IS NULL\nAND is_active = true"]

    LT -->|"'vendor'"| VEN["SELECT FROM users\nWHERE (username = identifier\n  OR email = identifier)\nAND role = 'VENDOR'\nAND deleted_at IS NULL\nAND is_active = true"]

    LT -->|"anything else"| NONE["return None\n→ 401"]

    CUS & EMP & OWN & CON & SUB & VEN --> FOUND{user row\nreturned?}

    FOUND -->|No| NONE2["return None → 401\n'Invalid credentials'"]
    FOUND -->|Yes| PWD["verify_password(\n  request.password,\n  user.password_hash\n)"]

    PWD -->|False| NONE3["return None → 401\n'Invalid credentials'\n(same message — never\nreveal which field failed)"]
    PWD -->|True| RETURN["return User object\n→ create_token_pair()"]

    subgraph SeedRef["Seed credentials reference"]
        S1["CUSTOMER: username='alice'"]
        S2["FIELD_AGENT: emp_id='FA001''FA005'"]
        S3["ADMIN: emp_id='ADM01''ADM03'"]
        S4["OWNER: username='justin'/'diana'"]
        S5["CONTRACTOR: username='mike'"]
        S6["SUBCONTRACTOR: username='carlos'"]
        S7["VENDOR: username='abc_supply'"]
    end

Diagram 5 — Refresh Token Rotation + Reuse Attack Detection

The full refresh flow, including the security branch when a reused refresh token is detected.

sequenceDiagram
    participant FE as React (interceptors.js)
    participant API as POST /auth/refresh
    participant SVC as auth_service.py
    participant SEC as security.py
    participant DB as users table

    Note over FE: Received 401 on any request
    Note over FE: isRefreshing flag set to true
    Note over FE: Other 401s queued in failedQueue

    FE->>API: POST /auth/refresh\nCookie: refresh_token=<jwt>

    API->>SEC: decode_token(refresh_token)

    alt Token missing or expired
        SEC-->>API: JWTError
        API-->>FE: 401 "Invalid or expired refresh token"
        FE->>FE: dispatch('auth:logout-required')
        FE->>FE: redirect to /login
    else Token valid
        SEC-->>API: { sub: user_id, type: "refresh" }
        API->>DB: SELECT * FROM users WHERE id = user_id

        alt User not found / deactivated / soft-deleted
            DB-->>API: None
            API-->>FE: 401
        else User found
            DB-->>API: User row (includes refresh_token_hash)

            API->>SEC: verify_token_hash(\n  incoming_token,\n  user.refresh_token_hash\n)

            alt Hash mismatch ← REUSE ATTACK
                Note over API,DB: Token was already rotated.\nSomeone is replaying an old token.
                API->>DB: UPDATE users\nSET refresh_token_hash = NULL\n(revoke ALL sessions)
                API-->>FE: 401 "Refresh token reuse detected"
                FE->>FE: dispatch('auth:logout-required')
                FE->>FE: force logout + redirect /login
            else Hash matches — normal rotation
                API->>SEC: create_access_token(user_id, role, legacy_id)
                API->>SEC: create_refresh_token(user_id)
                SEC-->>API: new_access, new_refresh
                API->>SEC: hash_token(new_refresh)
                SEC-->>API: new_hash
                API->>DB: UPDATE users\nSET refresh_token_hash = new_hash\n    last_login_at = NOW()
                API-->>FE: 200 Set-Cookie: access_token=new\nSet-Cookie: refresh_token=new
                Note over FE: processQueue(null) — release all queued requests
                FE->>FE: Retry original request(s)
            end
        end
    end

How the two cookies differ in path, scope, and lifetime, and why.

flowchart TD
    subgraph AccessCookie["access_token Cookie"]
        AC1["key: 'access_token'"]
        AC2["httponly: True\n← JS cannot read it"]
        AC3["secure: True\n← HTTPS only"]
        AC4["samesite: 'strict'\n← blocks CSRF"]
        AC5["max_age: 900s (15 min)"]
        AC6["path: '/'\n← sent with ALL requests to backend"]
        AC1 --> AC2 --> AC3 --> AC4 --> AC5 --> AC6
    end

    subgraph RefreshCookie["refresh_token Cookie"]
        RC1["key: 'refresh_token'"]
        RC2["httponly: True"]
        RC3["secure: True"]
        RC4["samesite: 'strict'"]
        RC5["max_age: 604800s (7 days)"]
        RC6["path: '/api/v1/auth'\n← only sent to /auth/* endpoints\nnot leaked with every API call"]
        RC1 --> RC2 --> RC3 --> RC4 --> RC5 --> RC6
    end

    subgraph XSSProtection["Why httpOnly?"]
        XSS1["XSS attack injects script:\ndocument.cookie → blocked\nlocalStorage.getItem() → blocked\nfetch with credentials → blocked by samesite"]
    end

    subgraph CSRFProtection["Why samesite=strict?"]
        CSRF1["Attacker tricks victim\ninto clicking evil.com link\nwhich sends request to api.lynkeduppro.com\nsamesite=strict: cookie NOT attached\nto cross-site navigation"]
    end

Diagram 7 — Permission Matrix Decision Flow

How has_permission() is used inside service functions (not routers) for fine-grained access control.

flowchart TD
    subgraph RolePermTable["ROLE_PERMISSIONS — Full Matrix"]
        direction LR
        HDR["Permission → Role grants"]

        VP["VIEW_ALL_PROJECTS\n→ OWNER ✅ ADMIN ✅"]
        VPE["VIEW_ALL_PERSONNEL\n→ OWNER ✅ ADMIN ✅"]
        VF["VIEW_FINANCIALS\n→ OWNER ✅ ADMIN ✅"]
        VS["VIEW_SENSITIVE_DATA\n→ OWNER ✅ only"]
        MU["MANAGE_USERS\n→ OWNER ✅ ADMIN ✅"]
        AD["APPROVE_DOCUMENTS\n→ OWNER ✅ ADMIN ✅"]
        VAP["VIEW_ASSIGNED_PROJECTS\n→ OWNER ✅ ADMIN ✅ CONTRACTOR ✅ SUBCONTRACTOR ✅"]
        UD["UPLOAD_DOCUMENTS\n→ OWNER ✅ ADMIN ✅ CONTRACTOR ✅ VENDOR ✅"]
        SI["SUBMIT_INVOICES\n→ OWNER ✅ ADMIN ✅ CONTRACTOR ✅ VENDOR ✅"]
        MC["MANAGE_OWN_CREW\n→ CONTRACTOR ✅ only"]
        VAT["VIEW_ASSIGNED_TASKS\n→ OWNER ✅ ADMIN ✅ CONTRACTOR ✅ SUBCONTRACTOR ✅"]
        UTS["UPDATE_TASK_STATUS\n→ OWNER ✅ ADMIN ✅ CONTRACTOR ✅ SUBCONTRACTOR ✅"]
    end

    subgraph Usage["Service Layer Usage Pattern"]
        SVC["service function receives\ncurrent_user: User"]
        CHECK["has_permission(\n  current_user.role,\n  'VIEW_SENSITIVE_DATA'\n)"]
        GRANT["Return full data\nincluding SSN, EIN,\nbank accounts"]
        DENY["Return masked data\nmask_ssn(user.ssn)\nmask_ein(user.ein)\nmask_bank_account(user.bank)"]

        SVC --> CHECK
        CHECK -->|True — OWNER only| GRANT
        CHECK -->|False — all other roles| DENY
    end

    subgraph RouterVsService["Router vs Service Responsibility"]
        ROUTER_CHECK["Router:\nrequire_role('ADMIN','OWNER')\n← coarse: is this role allowed at all?"]
        SERVICE_CHECK["Service:\nhas_permission(role, 'VIEW_FINANCIALS')\n← fine: which fields/operations?"]
        ROUTER_CHECK -->|"if pass"| SERVICE_CHECK
    end

Diagram 8 — Sensitive Data Masking Flow

When mask.py is invoked per endpoint and per role.

flowchart TD
    REQ["GET /users/{id}\nor GET /users (list)"]
    AUTH["get_current_user() → User\nrequire_role('ADMIN','OWNER','CONTRACTOR',..."]

    AUTH --> ROLE{current_user.role?}

    ROLE -->|OWNER| FULL["Return FULL data\nssn: '123-45-6789'\nein: '12-3456789'\nbank_account: '9876543210'"]

    ROLE -->|ADMIN\nCONTRACTOR\nFIELD_AGENT\nVENDOR\nSUBCONTRACTOR| MASKED["Apply mask.py\nssn → mask_ssn() → '***-**-6789'\nein → mask_ein() → '**-***6789'\nbank → mask_bank_account() → '******3210'"]

    ROLE -->|CUSTOMER| OWN_ONLY["CUSTOMER sees only\ntheir own record\nwith masked sensitive fields"]

    subgraph MaskFunctions["mask.py — Functions"]
        M1["mask_ssn('123-45-6789')\n→ '***-**-6789'\nlast 4 digits visible"]
        M2["mask_bank_account('9876543210')\n→ '******3210'\nlast 4 digits visible"]
        M3["mask_ein('12-3456789')\n→ '**-***6789'\nlast 4 digits visible"]
    end

    MASKED --> MaskFunctions
    OWN_ONLY --> MaskFunctions

Diagram 9 — get_current_user() Dependency Resolution

The exact path FastAPI takes to resolve the get_current_user dependency on every protected request.

flowchart TD
    RT["Protected route handler called\nDepends(get_current_user)"]

    RT --> COOKIE["Extract cookie:\naccess_token: str | None = Cookie(default=None)"]

    COOKIE --> HAS{access_token\npresent?}
    HAS -->|No| E401A["raise HTTPException 401\n'Not authenticated'"]

    HAS -->|Yes| DECODE["decode_token(access_token)\njwt.decode(token, SECRET_KEY, algorithms)"]

    DECODE --> VALID{Valid\nsignature?}
    VALID -->|No / tampered| E401B["raise HTTPException 401\n'Not authenticated'"]

    VALID -->|Yes| EXPIRED{Token\nexpired?}
    EXPIRED -->|Yes| E401C["raise HTTPException 401\n'Not authenticated'\n→ FE interceptor triggers /auth/refresh"]

    EXPIRED -->|No| TYPE{"payload['type']\n== 'access'?"}
    TYPE -->|No (refresh token sent by mistake)| E401D["raise HTTPException 401"]

    TYPE -->|Yes| SUB["user_id = payload['sub']\nrole = payload['role']"]

    SUB --> DB["db.get(User, user_id)"]
    DB --> EXISTS{User found?\ndeleted_at IS NULL?\nis_active = True?}

    EXISTS -->|No| E401E["raise HTTPException 401\n'Not authenticated'"]
    EXISTS -->|Yes| INJECT["return User object\n→ injected as current_user\ninto route handler"]

    INJECT --> ROLE_CHECK["require_role() checker:\nif current_user.role not in allowed_roles\n→ 403 FORBIDDEN\nelse → proceed to handler"]