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
This commit is contained in:
Satyam
2026-02-26 01:36:36 +05:30
parent 3d215f6db3
commit 7694788387
14 changed files with 6795 additions and 931 deletions
+385
View File
@@ -0,0 +1,385 @@
# 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"]
```