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

386 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
```mermaid
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**.
```mermaid
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.
```mermaid
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.
```mermaid
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.
```mermaid
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
```
---
## Diagram 6 — Cookie Security Configuration
How the two cookies differ in path, scope, and lifetime, and why.
```mermaid
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.
```mermaid
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.
```mermaid
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.
```mermaid
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"]
```