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
+280
View File
@@ -0,0 +1,280 @@
# HLD — High-Level Design Diagrams
**Scope:** LynkedUpPro CRM — Full System
**Renders in:** GitHub, VS Code (Mermaid extension), GitLab, Notion
---
## Diagram 1 — System Context
Who uses the system, what it does, and what external services it depends on.
```mermaid
graph TB
subgraph Users["👤 User Roles"]
OWN([Owner])
ADM([Admin])
FA([Field Agent])
CON([Contractor])
SUB([Subcontractor])
VEN([Vendor])
CUS([Customer])
end
subgraph LynkedUpPro["🏢 LynkedUpPro System"]
FE["React 19 SPA\n(Vercel)"]
BE["FastAPI Backend\n(Railway / Render)"]
DB[(PostgreSQL 16)]
end
subgraph External["☁️ External Services"]
GROQ["Groq API\n(AI / LLM)"]
S3["AWS S3\n(File Storage)"]
SMTP["SendGrid\n(Email)"]
end
OWN & ADM & FA & CON & SUB & VEN & CUS -->|"HTTPS browser"| FE
FE -->|"REST /api/v1/ + JWT"| BE
BE -->|"SQLAlchemy async"| DB
BE -->|"httpx proxy"| GROQ
BE -->|"boto3 SDK"| S3
BE -->|"SMTP / SendGrid SDK"| SMTP
```
---
## Diagram 2 — System Components
Internal components of each layer and how they connect.
```mermaid
graph LR
subgraph Frontend["Frontend — React SPA (Vite)"]
direction TB
AC["AuthContext\n(login / logout)"]
MS["mockStore → ApiProvider\n(data layer)"]
GC["GamificationContext\n(XP / levels)"]
PAGES["Pages\n(Owner / Admin / Agent\nContractor / Vendor / Customer)"]
CHAT["Chatbot Component\n(AI assistant)"]
MAP["Maps Component\n(Leaflet + Polygons)"]
end
subgraph Backend["Backend — FastAPI"]
direction TB
MW["Middleware\n(CORS · Rate Limit · Logging)"]
AUTH_R["Auth Router\n(/api/v1/auth)"]
ROUTERS["Feature Routers\n(properties · meetings · vendors\nprojects · invoices · documents\nusers · chatbot)"]
SERVICES["Service Layer\n(business logic)"]
DEPS["Dependencies\nget_db · get_current_user\nrequire_role()"]
end
subgraph Data["Data Layer"]
PG[(PostgreSQL\n16 tables)]
S3_B["AWS S3\n(documents / COIs)"]
end
subgraph Ext["External APIs"]
GROQ_A["Groq API\nqwen-32b"]
SG["SendGrid\n(alerts)"]
end
Frontend -->|"JWT Bearer / httpOnly cookie"| MW
MW --> AUTH_R
MW --> ROUTERS
AUTH_R --> DEPS
ROUTERS --> DEPS
DEPS --> SERVICES
SERVICES --> PG
SERVICES --> S3_B
SERVICES -->|"RBAC-gated proxy"| GROQ_A
SERVICES -->|"async trigger"| SG
```
---
## Diagram 3 — Role Access Zones
Which roles can access which portals and routes. Each role logs in through a single `/login` page and is routed to their portal based on JWT claims.
```mermaid
graph TD
LOGIN["/login\nSingle Entry Point"]
LOGIN -->|"role = OWNER"| OWNER_ZONE
LOGIN -->|"role = ADMIN"| ADMIN_ZONE
LOGIN -->|"role = FIELD_AGENT"| FA_ZONE
LOGIN -->|"role = CONTRACTOR"| CON_ZONE
LOGIN -->|"role = VENDOR"| VEN_ZONE
LOGIN -->|"role = SUBCONTRACTOR"| SUB_ZONE
LOGIN -->|"role = CUSTOMER"| CUS_ZONE
subgraph OWNER_ZONE["👑 Owner Portal"]
O1["/owner/snapshot\nBusiness Health Dashboard"]
O2["/owner/projects\nProject Command Center"]
O3["/owner/vendors\nVendor Directory"]
O4["/owner/people\nPeople Directory"]
O5["/owner/documents\nDocument Management"]
O6["/owner/maps\nTerritory Map"]
O7["/owner/pro-canvas\nTeam Leaderboard"]
O8["/admin/*\nFull Admin Access"]
end
subgraph ADMIN_ZONE["🏢 Admin Portal"]
A1["/admin/dashboard\nCommand Center"]
A2["/admin/schedule\nTeam Schedule"]
A3["/admin/leaderboard\nSales Leaderboard"]
A4["/admin/maps\nTerritory Map"]
A5["/admin/pro-canvas\nTeam Performance"]
end
subgraph FA_ZONE["🚶 Field Agent Portal"]
FA1["/emp/fa/dashboard\nMy Dashboard"]
FA2["/emp/fa/maps\nTerritory Map"]
FA3["/emp/fa/pro-canvas\nMy Gamification Hub"]
end
subgraph CON_ZONE["🔨 Contractor Portal"]
C1["/contractor/dashboard\nMy Projects"]
C2["/contractor/projects\nProject List"]
end
subgraph VEN_ZONE["📦 Vendor Portal"]
V1["/vendor/dashboard\nMy Orders & KPIs"]
V2["/vendor/orders\nOrder Management"]
end
subgraph SUB_ZONE["⚙️ Subcontractor Portal"]
S1["/subcontractor/dashboard\nMy Work"]
S2["/subcontractor/projects\nAssigned Projects"]
end
subgraph CUS_ZONE["🏠 Customer Portal"]
CU1["/portal/profile\nMy Property & Meetings"]
end
SHARED["/chat-assistant\nAI Assistant\n(all roles)"]
LOGIN --> SHARED
```
---
## Diagram 4 — High-Level Data Flow
How a user action travels from browser → API → database and back.
```mermaid
sequenceDiagram
actor User as 👤 User (any role)
participant FE as React SPA
participant AC as AuthContext
participant API as FastAPI /api/v1
participant DB as PostgreSQL
Note over User,DB: ── Initial Login ──
User->>FE: Enter credentials
FE->>AC: login(identifier, password, type)
AC->>API: POST /auth/login
API->>DB: SELECT user WHERE email/empId = ?
DB-->>API: User row
API-->>AC: { access_token, refresh_token, user }
AC-->>FE: Set cookie + update user state
FE-->>User: Redirect to role portal
Note over User,DB: ── Authenticated Data Request ──
User->>FE: Navigate to page
FE->>API: GET /properties?status=Hot+Lead\nAuthorization: Bearer <token>
API->>API: Validate JWT → extract role
API->>API: require_role(FIELD_AGENT, ADMIN, OWNER)
API->>DB: SELECT * FROM properties WHERE deleted_at IS NULL
DB-->>API: Rows
API-->>FE: { data: [...], meta: { total, page } }
FE-->>User: Render page with data
Note over User,DB: ── Write Action (e.g. assign agent) ──
User->>FE: Click "Assign Agent"
FE->>API: PATCH /properties/42\n{ assigned_agent_id: "uuid" }
API->>API: require_role(ADMIN, OWNER)
API->>DB: UPDATE properties SET assigned_agent_id = ?, updated_at = NOW()
API->>DB: INSERT INTO audit_logs (actor_id, action, ...)
DB-->>API: Updated row
API-->>FE: { data: { ...updatedProperty } }
FE-->>User: Toast "Agent assigned ✓"
```
---
## Diagram 5 — Deployment Architecture
How the pieces are hosted and connected in production.
```mermaid
graph TB
subgraph Internet["🌐 Internet"]
BROWSER["User Browser"]
end
subgraph Vercel["Vercel (Frontend CDN)"]
CDN["Static Assets\nReact SPA Bundle"]
end
subgraph Railway["Railway / Render (Backend)"]
UVICORN["uvicorn workers\nFastAPI app"]
PG_DB[("PostgreSQL 16\nManaged DB")]
end
subgraph AWS["AWS"]
S3_BUCKET["S3 Bucket\nlynkeduppro-docs\n(COIs, W9s, Contracts)"]
end
subgraph ThirdParty["Third-Party APIs"]
GROQ_API["Groq API\nLLM inference"]
SENDGRID_API["SendGrid\nTransactional email"]
end
BROWSER -->|"HTTPS :443"| CDN
CDN -->|"HTTPS /api/v1/*\nJWT in cookie"| UVICORN
UVICORN <-->|"TCP :5432\nasyncpg"| PG_DB
UVICORN -->|"HTTPS\npre-signed URLs"| S3_BUCKET
UVICORN -->|"HTTPS\nAPI key"| GROQ_API
UVICORN -->|"HTTPS\nAPI key"| SENDGRID_API
BROWSER -->|"pre-signed URL\ndirect upload"| S3_BUCKET
```
---
## Diagram 6 — Role Permission Overview (RBAC Summary)
A simplified view of the data each role can read and write.
```mermaid
graph LR
subgraph ReadAll["📖 Read All Data"]
OWNER_R["OWNER\nAll tables, all records"]
ADMIN_R["ADMIN\nAll tables, all records"]
end
subgraph ReadScoped["📖 Read Scoped Data"]
FA_R["FIELD_AGENT\nOwn meetings + assigned properties"]
CON_R["CONTRACTOR\nAssigned projects + own invoices"]
SUB_R["SUBCONTRACTOR\nAssigned tasks + project scope"]
VEN_R["VENDOR\nOwn orders + own compliance docs"]
CUS_R["CUSTOMER\nOwn property + own meetings"]
end
subgraph WriteAll["✏️ Write All Data"]
OWNER_W["OWNER\nProjects · Vendors · Documents\nInvoice approval · Change orders"]
ADMIN_W["ADMIN\nProperties · Meetings · Assignments"]
end
subgraph WriteSelf["✏️ Write Own Data"]
FA_W["FIELD_AGENT\nMeeting outcomes · Lead status"]
VEN_W["VENDOR\nOrder acknowledgement"]
CUS_W["CUSTOMER\nProfile info"]
end
OWNER_R --- OWNER_W
ADMIN_R --- ADMIN_W
FA_R --- FA_W
VEN_R --- VEN_W
CUS_R --- CUS_W
```
+388
View File
@@ -0,0 +1,388 @@
# LLD — Architecture (B0)
**Scope:** FastAPI backend — internal structure, request lifecycle, auth flows, module dependencies
**Companion doc:** `docs/backend/00_project_overview.md`
---
## Diagram 1 — FastAPI Application Layer Structure
How the files inside `app/` relate to each other. Arrows show import/dependency direction.
```mermaid
graph TD
subgraph Entry["Entry Point"]
MAIN["main.py\nFastAPI() instance\nMiddleware registration\nRouter registration"]
end
subgraph Config["Configuration"]
CFG["config.py\nSettings (pydantic-settings)\nReads .env"]
DB_MOD["database.py\nasync engine\nAsyncSession factory\nget_db()"]
end
subgraph Deps["Shared Dependencies"]
DEPMOD["dependencies.py\nget_db()\nget_current_user()\nrequire_role(*roles)"]
end
subgraph Routers["Routers — /api/v1/"]
R_AUTH["auth.py\n/auth/login\n/auth/refresh\n/auth/logout"]
R_USERS["users.py\n/users/me\n/users/{id}"]
R_PROP["properties.py\n/properties\n/properties/{id}"]
R_MEET["meetings.py\n/meetings\n/meetings/{id}"]
R_VEND["vendors.py\n/vendors\n/vendors/{id}"]
R_PROJ["projects.py\n/projects\n/projects/{id}/tasks"]
R_INV["invoices.py\n/invoices\n/invoices/{id}/approve"]
R_DOC["documents.py\n/documents\n/documents/{id}"]
R_CHAT["chatbot.py\n/chatbot/message"]
end
subgraph Services["Service Layer (Business Logic)"]
S_AUTH["auth_service.py\nverify_password()\ncreate_tokens()\nrotate_refresh_token()"]
S_PROP["property_service.py\nget_properties()\nassign_agent()\nmark_contacted()"]
S_MEET["meeting_service.py\nschedule()\nchange_status()\nrequest_change()"]
S_VEND["vendor_service.py\ncheck_compliance()\nupdate_coi()"]
S_NOTIF["notification_service.py\nsend_email()\ncreate_notification()"]
S_CHAT["chatbot_service.py\nbuild_context(role)\nproxy_to_groq()"]
end
subgraph Models["ORM Models (SQLAlchemy)"]
M_ALL["user.py · property.py\nmeeting.py · vendor.py\nproject.py · invoice.py\ndocument.py · audit_log.py"]
end
subgraph Schemas["Pydantic Schemas (Request/Response)"]
SCH_ALL["user.py · property.py\nmeeting.py · vendor.py\nauth.py · invoice.py"]
end
subgraph Utils["Utilities"]
U_SEC["security.py\njwt_encode / jwt_decode\nbcrypt helpers"]
U_PERM["permissions.py\nrole_can_access()"]
U_MASK["mask.py\nmask_ssn() · mask_bank()"]
end
MAIN --> CFG
MAIN --> DB_MOD
MAIN --> DEPMOD
MAIN --> R_AUTH & R_USERS & R_PROP & R_MEET & R_VEND & R_PROJ & R_INV & R_DOC & R_CHAT
R_AUTH --> DEPMOD --> S_AUTH
R_PROP --> DEPMOD --> S_PROP
R_MEET --> DEPMOD --> S_MEET
R_VEND --> DEPMOD --> S_VEND
R_CHAT --> DEPMOD --> S_CHAT
S_NOTIF --> S_PROP & S_MEET & S_VEND
S_AUTH & S_PROP & S_MEET & S_VEND & S_PROJ --> M_ALL
R_AUTH & R_PROP & R_MEET --> SCH_ALL
S_AUTH --> U_SEC
DEPMOD --> U_PERM
R_USERS --> U_MASK
```
---
## Diagram 2 — HTTP Request Lifecycle
Every request follows this exact path through the backend. Each step is a named file/function.
```mermaid
sequenceDiagram
participant C as Client (React)
participant CORS as CORS Middleware
participant RL as Rate Limiter
participant LOG as Request Logger
participant RT as Router (e.g. properties.py)
participant DEP as Dependencies
participant SVC as Service Layer
participant ORM as SQLAlchemy ORM
participant DB as PostgreSQL
C->>CORS: HTTP Request
Note over CORS: Check Origin header<br/>against ALLOWED_ORIGINS
CORS-->>C: 403 if origin not allowed
CORS->>RL: Pass through
Note over RL: Check rate limit<br/>by IP / user ID
RL-->>C: 429 if exceeded
RL->>LOG: Pass through
Note over LOG: Log: method, path,<br/>request_id, timestamp
LOG->>RT: Route matched
Note over RT,DEP: FastAPI resolves Depends()
RT->>DEP: get_db() → yields AsyncSession
RT->>DEP: get_current_user(token)
Note over DEP: Decode JWT<br/>Validate expiry<br/>Load user from DB
DEP-->>RT: 401 if invalid token
RT->>DEP: require_role("ADMIN","OWNER")
DEP-->>RT: 403 if role mismatch
DEP-->>RT: current_user injected
RT->>SVC: call service function(db, current_user, params)
Note over SVC: Business logic<br/>Permission refinement<br/>Data transformation
SVC->>ORM: db.execute(select(Property)...)
ORM->>DB: SQL query
DB-->>ORM: Rows
ORM-->>SVC: Model instances
SVC-->>RT: Result dict / model
Note over RT: Pydantic response schema<br/>serializes result
RT-->>C: 200 { data: {...}, meta: {...} }
Note over LOG: Log: status_code,<br/>duration_ms, response_size
```
---
## Diagram 3 — Authentication Flow
### 3a — Login
```mermaid
sequenceDiagram
actor U as User
participant FE as React AuthContext
participant API as POST /auth/login
participant SVC as auth_service.py
participant DB as users table
participant SEC as security.py
U->>FE: login(identifier, password, type)
FE->>API: { identifier, password, type }
API->>SVC: authenticate(identifier, password, type)
alt type == "customer"
SVC->>DB: SELECT WHERE username = identifier
else type == "employee" (FIELD_AGENT / ADMIN)
SVC->>DB: SELECT WHERE emp_id = identifier
else type == "employee" (OWNER / CONTRACTOR / VENDOR / SUBCONTRACTOR)
SVC->>DB: SELECT WHERE username = identifier
end
DB-->>SVC: User row (or None)
alt User not found
SVC-->>API: None
API-->>FE: 401 { detail: "Invalid credentials" }
FE-->>U: Toast "Invalid credentials"
else User found
SVC->>SEC: verify_password(plain, hash)
alt Password wrong
SEC-->>SVC: False
SVC-->>API: None
API-->>FE: 401 { detail: "Invalid credentials" }
else Password correct
SEC-->>SVC: True
SVC->>SEC: create_access_token(user.id, user.role)
SVC->>SEC: create_refresh_token(user.id)
SVC->>DB: UPDATE users SET refresh_token_hash = ?, last_login_at = NOW()
SVC-->>API: { access_token, refresh_token, user }
API-->>FE: Set httpOnly cookie (access + refresh)\n200 { user: { id, name, role, ... } }
FE-->>U: Redirect to role portal
end
end
```
### 3b — Authenticated Request
```mermaid
sequenceDiagram
participant FE as React
participant API as Any Protected Endpoint
participant DEP as get_current_user()
participant SEC as security.py
participant DB as users table
FE->>API: GET /properties\nCookie: access_token=<jwt>
API->>DEP: Depends(get_current_user)
DEP->>DEP: Extract token from cookie / Authorization header
alt No token present
DEP-->>API: HTTPException 401
API-->>FE: 401 UNAUTHORIZED
end
DEP->>SEC: jwt_decode(token, SECRET_KEY)
alt Token expired
SEC-->>DEP: JWTError (ExpiredSignatureError)
DEP-->>API: HTTPException 401 "Token expired"
API-->>FE: 401 → FE triggers refresh flow
else Token invalid (tampered)
SEC-->>DEP: JWTError
DEP-->>API: HTTPException 401 "Invalid token"
else Token valid
SEC-->>DEP: { sub: user_id, role: "FIELD_AGENT", exp: ... }
DEP->>DB: SELECT WHERE id = user_id AND deleted_at IS NULL
DB-->>DEP: User row
DEP-->>API: current_user injected
API->>API: require_role check
API-->>FE: 200 with data
end
```
### 3c — Token Refresh
```mermaid
sequenceDiagram
participant FE as React (axios interceptor)
participant API as POST /auth/refresh
participant SVC as auth_service.py
participant DB as users table
participant SEC as security.py
Note over FE: Receives 401 "Token expired"
FE->>API: POST /auth/refresh\nCookie: refresh_token=<jwt>
API->>SVC: refresh_access_token(refresh_token)
SVC->>SEC: jwt_decode(refresh_token)
alt Refresh token invalid/expired
SEC-->>SVC: JWTError
SVC-->>API: Raise 401
API-->>FE: 401 → FE calls logout(), redirect to /login
else Valid
SEC-->>SVC: { sub: user_id }
SVC->>DB: SELECT refresh_token_hash WHERE id = user_id
SVC->>SEC: verify(incoming_token, stored_hash)
alt Hash mismatch (token reuse attack)
SEC-->>SVC: False
SVC->>DB: UPDATE users SET refresh_token_hash = NULL (revoke all)
SVC-->>API: 401 "Refresh token reuse detected"
API-->>FE: 401 → Force logout
else Hash matches
SVC->>SEC: create_access_token(user_id, role)
SVC->>SEC: create_refresh_token(user_id)
SVC->>DB: UPDATE users SET refresh_token_hash = new_hash
SVC-->>API: { new_access_token, new_refresh_token }
API-->>FE: Set new cookies\n200 OK
FE->>FE: Retry original request with new token
end
end
```
---
## Diagram 4 — RBAC Middleware Decision Tree
```mermaid
flowchart TD
REQ["Incoming Request"] --> HAS_TOKEN{Token present\nin cookie\nor header?}
HAS_TOKEN -->|No| R401_A["401 UNAUTHORIZED\nNo authentication provided"]
HAS_TOKEN -->|Yes| DECODE{Decode JWT\nvalid signature?}
DECODE -->|No / tampered| R401_B["401 UNAUTHORIZED\nInvalid token"]
DECODE -->|Yes| EXPIRED{Token\nexpired?}
EXPIRED -->|Yes| R401_C["401 UNAUTHORIZED\nToken expired\n→ client should refresh"]
EXPIRED -->|No| LOAD_USER{User exists\nin DB and\nis_active?}
LOAD_USER -->|No| R401_D["401 UNAUTHORIZED\nUser not found or deactivated"]
LOAD_USER -->|Yes| HAS_ROLE_CHECK{Route has\nrequire_role()?}
HAS_ROLE_CHECK -->|No - public route| PASS["✅ Proceed to handler\ncurrent_user = None"]
HAS_ROLE_CHECK -->|Yes| ROLE_MATCH{user.role\nin allowed_roles?}
ROLE_MATCH -->|No| R403["403 FORBIDDEN\nInsufficient permissions\nfor this resource"]
ROLE_MATCH -->|Yes| HANDLER["✅ Proceed to handler\ncurrent_user = User object"]
HANDLER --> SERVICE["Service layer\nfurther scoping\n(e.g. agent only sees own meetings)"]
```
---
## Diagram 5 — Module Build Dependency Graph
The recommended order to build backend modules. Each module depends on the ones above it.
```mermaid
graph TD
B0["B0\nProject Overview\n& Conventions"]
B2["B2\nDatabase Schema\n(All ORM models)"]
B1["B1\nAuthentication\n(JWT, RBAC, all 6 roles)"]
B3["B3\nProperties Module\n(CRUD, geospatial, leads)"]
B4["B4\nUsers & People\n(profiles, masking)"]
B5["B5\nMeetings & Scheduling\n(status machine, change requests)"]
B6["B6\nVendors & Compliance\n(COI, W9, expiry alerts)"]
B7["B7\nFinancials\n(invoices, AR/AP, payouts)"]
B8["B8\nChatbot & AI\n(RBAC context, Groq proxy)"]
B9["B9\nNotifications\n(email, in-app, alerts)"]
B0 --> B2
B2 --> B1
B1 --> B3
B1 --> B4
B3 --> B5
B4 --> B5
B5 --> B6
B3 --> B7
B6 --> B7
B3 & B4 & B5 & B6 & B7 --> B8
B5 & B6 & B7 --> B9
style B0 fill:#1e3a5f,color:#fff
style B2 fill:#1e3a5f,color:#fff
style B1 fill:#1e5f3a,color:#fff
style B3 fill:#3a1e5f,color:#fff
style B4 fill:#3a1e5f,color:#fff
style B5 fill:#5f3a1e,color:#fff
style B6 fill:#5f3a1e,color:#fff
style B7 fill:#5f1e3a,color:#fff
style B8 fill:#1e5f5f,color:#fff
style B9 fill:#5f5f1e,color:#fff
```
---
## Diagram 6 — Chatbot RBAC Context Builder
How the chatbot service decides what data to inject into the LLM prompt based on the requesting user's role.
```mermaid
flowchart TD
REQ["POST /chatbot/message\n{ message, role }"] --> AUTH["Validate JWT\nExtract role"]
AUTH --> ROLE{user.role?}
ROLE -->|FIELD_AGENT| FA_CTX["Build Agent Context\n• Today's meetings\n• Assigned properties\n• Personal XP / streak\n• Top 5 hot leads in territory"]
ROLE -->|ADMIN| ADM_CTX["Build Admin Context\n• Unassigned leads count\n• Today's schedule (all agents)\n• Pending signatures\n• Revenue MTD"]
ROLE -->|OWNER| OWN_CTX["Build Owner Context\n• Project health scores\n• Overdue invoices\n• Compliance alerts\n• Revenue vs budget"]
ROLE -->|CONTRACTOR| CON_CTX["Build Contractor Context\n• Assigned projects\n• Pending tasks\n• Open change orders"]
ROLE -->|VENDOR| VEN_CTX["Build Vendor Context\n• Open orders\n• Compliance doc expiry\n• Payment status"]
ROLE -->|CUSTOMER| CUS_CTX["Build Customer Context\n• Own property details\n• Upcoming meetings\n• Service history"]
FA_CTX & ADM_CTX & OWN_CTX & CON_CTX & VEN_CTX & CUS_CTX --> PROMPT["Assemble system prompt\nRole context + user message"]
PROMPT --> GROQ["POST Groq API\nqwen-32b\nstream=true"]
GROQ --> AUDIT["Write audit_log\nactor_id, action=chatbot.query\nredacted message"]
AUDIT --> STREAM["Stream response\nback to client"]
```
---
## Diagram 7 — Environment Configuration Flow
How environment variables flow from `.env` into every part of the app.
```mermaid
graph LR
ENV_FILE[".env file\n(never committed)"]
ENV_EXAMPLE[".env.example\n(committed template)"]
ENV_FILE -->|"pydantic-settings\nBaseSettings"| SETTINGS["config.py\nSettings singleton\nsettings.database_url\nsettings.secret_key\nsettings.groq_api_key\n..."]
SETTINGS -->|"settings.database_url"| DB_MOD["database.py\ncreate_async_engine(url)"]
SETTINGS -->|"settings.secret_key"| SEC["security.py\njwt_encode/decode"]
SETTINGS -->|"settings.groq_api_key"| CHAT_SVC["chatbot_service.py\nhttpx.post(headers={api_key})"]
SETTINGS -->|"settings.backend_cors_origins"| CORS_MW["main.py\nCORSMiddleware(allow_origins=...)"]
SETTINGS -->|"settings.aws_*"| S3_SVC["document upload\nboto3.client(s3)"]
SETTINGS -->|"settings.sendgrid_api_key"| NOTIF_SVC["notification_service.py\nSendGrid client"]
ENV_EXAMPLE -.->|"documents expected vars\n(no real values)"| ENV_FILE
```
+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"]
```
+527
View File
@@ -0,0 +1,527 @@
# LLD — Database Schema (B2)
**Scope:** PostgreSQL schema — entity relationships, state machines, query patterns, data lifecycle
**Companion doc:** `docs/backend/02_database_schema.md`
---
## Diagram 1 — Core Entity Relationship Diagram
All 16 tables and their foreign-key relationships. Key fields shown per entity.
```mermaid
erDiagram
USERS {
uuid id PK
varchar legacy_id
varchar email
varchar emp_id
varchar username
varchar password_hash
varchar full_name
user_role role
int xp
int streak_days
jsonb achievements
timestamptz deleted_at
}
PROPERTIES {
serial id PK
varchar property_id
varchar address
decimal latitude
decimal longitude
jsonb polygon
property_type property_type
canvassing_status canvassing_status
int estimated_market_value
uuid assigned_agent_id FK
bool pending_signature
int proposal_value
bool currently_rented
timestamptz deleted_at
}
PROPERTY_PHOTOS {
serial id PK
int property_id FK
varchar url
varchar caption
smallint sort_order
}
MEETINGS {
uuid id PK
varchar legacy_id
uuid agent_id FK
uuid customer_id FK
int property_id FK
date meeting_date
time meeting_time
meeting_status status
int deal_value
text notes
timestamptz deleted_at
}
MEETING_CHANGE_REQUESTS {
uuid id PK
uuid meeting_id FK
uuid requested_by FK
uuid reviewed_by FK
date proposed_date
change_request_status status
}
PROJECTS {
uuid id PK
uuid owner_id FK
int property_id FK
varchar title
project_status status
smallint health_score
int approved_budget_cents
int actual_cost_cents
smallint completion_pct
timestamptz deleted_at
}
PROJECT_TASKS {
uuid id PK
uuid project_id FK
uuid assigned_to FK
varchar title
task_status status
task_priority priority
date due_date
}
CHANGE_ORDERS {
uuid id PK
uuid project_id FK
uuid requested_by FK
uuid approved_by FK
varchar title
int cost_impact_cents
change_order_status status
}
VENDORS {
uuid id PK
varchar company_name
varchar trade_type
vendor_status status
decimal on_time_delivery_rate
date coi_expiry_date
bool is_compliant
int total_spend_cents
timestamptz deleted_at
}
VENDOR_COMPLIANCE_DOCS {
uuid id PK
uuid vendor_id FK
compliance_doc_type doc_type
compliance_doc_status status
date expiry_date
varchar file_url
uuid uploaded_by FK
}
VENDOR_ORDERS {
uuid id PK
uuid vendor_id FK
uuid project_id FK
varchar order_number
int total_cents
order_status status
date expected_date
date delivered_date
timestamptz deleted_at
}
INVOICES {
uuid id PK
varchar invoice_number
invoice_type invoice_type
uuid issued_by FK
uuid billed_to_user FK
uuid vendor_id FK
uuid project_id FK
int total_cents
int amount_paid_cents
int balance_cents
invoice_status status
date due_date
uuid approved_by FK
timestamptz deleted_at
}
DOCUMENTS {
uuid id PK
varchar title
document_category category
document_review_status review_status
uuid project_id FK
uuid vendor_id FK
uuid uploaded_by FK
varchar file_url
date expiry_date
timestamptz deleted_at
}
SALES_HISTORY {
uuid id PK
uuid agent_id FK
int property_id FK
uuid meeting_id FK
date closed_date
int amount_cents
deal_status status
}
NOTIFICATIONS {
uuid id PK
uuid user_id FK
notification_type type
varchar title
bool is_read
varchar deep_link
jsonb metadata
}
AUDIT_LOGS {
bigserial id PK
uuid actor_id FK
varchar action
varchar resource_type
varchar resource_id
jsonb old_value
jsonb new_value
inet ip_address
}
USERS ||--o{ PROPERTIES : "assigned_agent_id"
USERS ||--o{ MEETINGS : "agent_id"
USERS ||--o{ MEETINGS : "customer_id"
USERS ||--o{ PROJECTS : "owner_id"
USERS ||--o{ PROJECT_TASKS : "assigned_to"
USERS ||--o{ CHANGE_ORDERS : "requested_by"
USERS ||--o{ INVOICES : "issued_by"
USERS ||--o{ INVOICES : "billed_to_user"
USERS ||--o{ SALES_HISTORY : "agent_id"
USERS ||--o{ NOTIFICATIONS : "user_id"
USERS ||--o{ AUDIT_LOGS : "actor_id"
USERS ||--o{ VENDOR_COMPLIANCE_DOCS : "uploaded_by"
PROPERTIES ||--o{ PROPERTY_PHOTOS : "property_id"
PROPERTIES ||--o{ MEETINGS : "property_id"
PROPERTIES ||--o{ PROJECTS : "property_id"
PROPERTIES ||--o{ SALES_HISTORY : "property_id"
MEETINGS ||--o{ MEETING_CHANGE_REQUESTS : "meeting_id"
MEETINGS ||--o{ SALES_HISTORY : "meeting_id"
PROJECTS ||--o{ PROJECT_TASKS : "project_id"
PROJECTS ||--o{ CHANGE_ORDERS : "project_id"
PROJECTS ||--o{ VENDOR_ORDERS : "project_id"
PROJECTS ||--o{ INVOICES : "project_id"
PROJECTS ||--o{ DOCUMENTS : "project_id"
VENDORS ||--o{ VENDOR_COMPLIANCE_DOCS : "vendor_id"
VENDORS ||--o{ VENDOR_ORDERS : "vendor_id"
VENDORS ||--o{ INVOICES : "vendor_id"
VENDORS ||--o{ DOCUMENTS : "vendor_id"
```
---
## Diagram 2 — Meeting Status State Machine
A meeting moves through these states. Only ADMIN/OWNER can force-advance to any state. FIELD_AGENTs can only request changes.
```mermaid
stateDiagram-v2
[*] --> Scheduled : POST /meetings\n(ADMIN creates)
Scheduled --> Rescheduled : PATCH status=Rescheduled\n(ADMIN approves change request)
Rescheduled --> Scheduled : PATCH status=Scheduled\n(new date confirmed)
Scheduled --> InProgress : PATCH status=In Progress\n(agent marks on-site)
Rescheduled --> InProgress : PATCH status=In Progress
InProgress --> Completed : PATCH status=Completed\n+ outcome + notes
InProgress --> Cancelled : PATCH status=Cancelled\n+ reason
Completed --> Converted : PATCH status=Converted\n+ deal_value (closes deal)
Scheduled --> Cancelled : PATCH status=Cancelled
Rescheduled --> Cancelled : PATCH status=Cancelled
Converted --> [*] : Terminal state\n→ writes to sales_history
Cancelled --> [*] : Terminal state
note right of Scheduled
FIELD_AGENT submits
MeetingChangeRequest
(cannot directly edit)
end note
note right of Converted
Triggers:
sales_history INSERT
notification to ADMIN
end note
```
---
## Diagram 3 — Invoice Status State Machine
Invoices move through a lifecycle depending on payments, approvals, and time-based triggers.
```mermaid
stateDiagram-v2
[*] --> Draft : POST /invoices\n(ADMIN / OWNER creates)
Draft --> Sent : PATCH status=Sent\n(email triggered to recipient)
Draft --> Void : PATCH status=Void\n(cancel before sending)
Sent --> Viewed : Auto on first\nrecipient GET request
Viewed --> Partial : PATCH\namount_paid_cents > 0\nbut < total_cents
Viewed --> Paid : PATCH\namount_paid_cents = total_cents
Partial --> Paid : PATCH\namount_paid_cents = total_cents
Partial --> Overdue : Scheduler trigger\ndue_date < TODAY
Sent --> Overdue : Scheduler trigger\ndue_date < TODAY
Overdue --> Paid : PATCH\namount_paid_cents = total_cents
Viewed --> Disputed : PATCH status=Disputed\n+ dispute_reason
Partial --> Disputed : PATCH status=Disputed
Overdue --> Disputed : PATCH status=Disputed
Disputed --> Paid : OWNER resolves dispute
Disputed --> Void : OWNER voids invoice
Paid --> [*] : Terminal state
Void --> [*] : Terminal state
note right of Overdue
Triggers:
notification to OWNER + ADMIN
SendGrid email to debtor
end note
```
---
## Diagram 4 — Project Status State Machine
Projects managed through the Owner's Box.
```mermaid
stateDiagram-v2
[*] --> active : POST /projects\n(OWNER creates)
active --> completed : PATCH status=completed\nall tasks done\nfinal invoice paid
active --> delayed : Auto-trigger:\ntarget_end_date passed\ncompletion_pct < 100
active --> on_hold : PATCH status=on_hold\n+ reason (OWNER decision)
on_hold --> active : PATCH status=active\n(OWNER resumes)
active --> disputed : PATCH status=disputed\n+ dispute details\n(OWNER or CONTRACTOR)
disputed --> active : Dispute resolved\n(OWNER approves)
disputed --> cancelled : Escalated dispute\n(OWNER decision)
active --> cancelled : PATCH status=cancelled\n(OWNER decision)
delayed --> active : PATCH status=active\n(new timeline set)
delayed --> cancelled : PATCH status=cancelled
completed --> [*] : Terminal state
cancelled --> [*] : Terminal state
note right of delayed
Triggers:
notification to OWNER
health_score recalculated
end note
note right of disputed
Triggers:
notification to CONTRACTOR
change_orders locked
end note
```
---
## Diagram 5 — Vendor Compliance Status Machine
Compliance documents transition based on expiry dates. A scheduler runs daily to check all docs.
```mermaid
stateDiagram-v2
[*] --> Active : Document uploaded\n(expiry_date in future)
Active --> ExpiringSoon : Scheduler:\nexpiry_date ≤ TODAY + 30 days
Active --> Expired : Scheduler:\nexpiry_date < TODAY
Active --> Missing : Manual flag\nor doc deleted
ExpiringSoon --> Active : New document uploaded\nwith future expiry_date
ExpiringSoon --> Expired : Scheduler:\nexpiry_date < TODAY
Expired --> Active : Replacement doc uploaded
Missing --> Active : Doc uploaded
note right of ExpiringSoon
Triggers at 30, 14, 7 days:
notification to OWNER
email to vendor contact
vendor.is_compliant = false
end note
note right of Expired
Triggers immediately:
urgent notification to OWNER
vendor.is_compliant = false
vendor orders may be blocked
end note
```
---
## Diagram 6 — Soft Delete Pattern
How "deletion" works across all tables. No row is ever hard-deleted in production.
```mermaid
flowchart TD
REQ["DELETE /properties/42\nor\nDELETE /vendors/uuid"] --> AUTH_CHECK["Auth: require_role\nADMIN or OWNER"]
AUTH_CHECK --> SOFT_DEL["UPDATE table\nSET deleted_at = NOW()\nWHERE id = ?"]
SOFT_DEL --> AUDIT["INSERT audit_logs\nactor_id, action=resource.deleted\nold_value = full row snapshot"]
SOFT_DEL --> NOTIFY["Trigger dependent cleanup:\n• Unassign agent from properties\n• Cancel pending meetings\n• Flag open invoices"]
subgraph AllQueries["All Application Queries"]
Q1["SELECT * FROM properties\nWHERE deleted_at IS NULL"]
Q2["SELECT * FROM meetings\nWHERE deleted_at IS NULL"]
Q3["SELECT * FROM vendors\nWHERE deleted_at IS NULL"]
end
SOFT_DEL -.->|"row hidden from"| AllQueries
subgraph AdminOnly["Admin / Recovery Queries"]
Q_ADMIN["SELECT * FROM properties\nWHERE deleted_at IS NOT NULL\n(recovery / audit only)"]
end
```
---
## Diagram 7 — Audit Log Write Pattern
Which operations write to `audit_logs` and what data is captured.
```mermaid
flowchart LR
subgraph Triggers["Write Operations That Trigger Audit Log"]
P1["Property:\nassign_agent\nchange_status\npending_signature"]
P2["Meeting:\nstatus change\nchange_request approved"]
P3["Invoice:\ncreated\napproved\nvoided"]
P4["User:\nrole changed\ndeactivated"]
P5["Project:\ncreated\nstatus changed\nchange_order approved"]
P6["Chatbot:\nquery with sensitive role context"]
end
subgraph AuditLog["audit_logs INSERT"]
AL["{\n actor_id: user.id,\n action: 'property.agent_assigned',\n resource_type: 'property',\n resource_id: '42',\n old_value: { assigned_agent_id: null },\n new_value: { assigned_agent_id: 'uuid' },\n ip_address: '1.2.3.4',\n created_at: NOW()\n}"]
end
P1 & P2 & P3 & P4 & P5 & P6 --> AL
AL --> QUERY["OWNER / ADMIN\nGET /audit-logs?resource_type=property\n&resource_id=42"]
```
---
## Diagram 8 — Key Query Patterns by Role
How the ORM service layer scopes queries differently per role.
```mermaid
flowchart TD
subgraph PropertiesQuery["GET /properties — Role-Scoped Query"]
PQ_START["Request arrives\nwith current_user"]
PQ_START --> PQ_ROLE{current_user.role}
PQ_ROLE -->|"OWNER / ADMIN"| PQ_ALL["SELECT * FROM properties\nWHERE deleted_at IS NULL\n+ any filters"]
PQ_ROLE -->|"FIELD_AGENT"| PQ_AGENT["SELECT * FROM properties\nWHERE assigned_agent_id = current_user.id\nAND deleted_at IS NULL"]
PQ_ROLE -->|"CUSTOMER"| PQ_CUS["SELECT * FROM properties\nWHERE property_id = current_user.property_id\nAND deleted_at IS NULL\nLIMIT 1"]
end
subgraph MeetingsQuery["GET /meetings — Role-Scoped Query"]
MQ_START["Request arrives"]
MQ_START --> MQ_ROLE{current_user.role}
MQ_ROLE -->|"OWNER / ADMIN"| MQ_ALL["SELECT * FROM meetings\nWHERE deleted_at IS NULL"]
MQ_ROLE -->|"FIELD_AGENT"| MQ_AGENT["SELECT * FROM meetings\nWHERE agent_id = current_user.id\nAND deleted_at IS NULL"]
MQ_ROLE -->|"CUSTOMER"| MQ_CUS["SELECT * FROM meetings\nWHERE customer_id = current_user.id\nAND deleted_at IS NULL"]
end
subgraph InvoiceQuery["GET /invoices — Role-Scoped Query"]
IQ_START["Request arrives"]
IQ_START --> IQ_ROLE{current_user.role}
IQ_ROLE -->|"OWNER"| IQ_ALL["SELECT * FROM invoices\nWHERE deleted_at IS NULL\n(full financial view)"]
IQ_ROLE -->|"ADMIN"| IQ_ADM["SELECT * FROM invoices\nWHERE deleted_at IS NULL\nAND invoice_type != 'internal'"]
IQ_ROLE -->|"CONTRACTOR / SUBCONTRACTOR"| IQ_CON["SELECT * FROM invoices\nWHERE billed_to_user = current_user.id\nOR issued_by = current_user.id"]
IQ_ROLE -->|"VENDOR"| IQ_VEN["SELECT * FROM invoices\nWHERE vendor_id IN\n (SELECT id FROM vendors\n WHERE contact_user_id = current_user.id)"]
end
```
---
## Diagram 9 — Legacy ID Migration Path
How the `legacy_id` column bridges the mock frontend IDs (`'e1'`, `'own_001'`) to real UUIDs during the transition period.
```mermaid
sequenceDiagram
participant FE as React Frontend
participant API as FastAPI
participant DB as PostgreSQL
Note over FE,DB: ── Transition Phase (Integration Team replacing mock calls) ──
FE->>API: GET /users/by-legacy-id/e1
API->>DB: SELECT * FROM users WHERE legacy_id = 'e1'
DB-->>API: { id: "3f8a...(uuid)", legacy_id: "e1", ... }
API-->>FE: User object with real UUID
Note over FE: Frontend stores UUID in state<br/>All subsequent calls use UUID
FE->>API: GET /meetings?agent_id=3f8a...(uuid)
API->>DB: SELECT * FROM meetings WHERE agent_id = '3f8a...'
DB-->>API: Meeting rows
API-->>FE: Meetings data
Note over FE,DB: ── Post-Migration Phase (legacy_id no longer needed) ──
Note over DB: legacy_id column remains<br/>for audit trail but<br/>no longer queried by FE
```
+353
View File
@@ -0,0 +1,353 @@
# LLD — I0 Integration Overview
**Scope:** Frontend API layer architecture, 401 interceptor, mock-to-real migration, error handling, environment config
**Companion doc:** `docs/integration/00_integration_overview.md`
---
## Diagram 1 — Frontend API Layer Architecture
How the files in `src/api/` relate to each other, to the React components, and to the backend.
```mermaid
graph TD
subgraph Components["React Components / Pages"]
C1["OwnerSnapshot.jsx"]
C2["AdminDashboard.jsx"]
C3["AgentDashboard.jsx"]
C4["Map.jsx"]
C5["VendorDashboard.jsx"]
C6["Chatbot.jsx"]
end
subgraph Hooks["Custom Hooks (src/hooks/)"]
H1["useProperties()"]
H2["useMeetings()"]
H3["useUsers()"]
H4["useVendors()"]
H5["useInvoices()"]
end
subgraph ApiLayer["API Layer (src/api/)"]
CLIENT["client.js\naxios.create({\n baseURL: VITE_API_URL,\n withCredentials: true\n})"]
INTER["interceptors.js\nResponse interceptor:\n401 → refresh → retry\nfailure → logout event"]
AUTH_API["auth.js\nlogin()\nlogout()\ngetMe()"]
PROP_API["properties.js\ngetProperties()\ngetProperty()\nassignAgent()"]
MEET_API["meetings.js\ngetMeetings()\ncreateMeeting()\nupdateMeeting()"]
VEND_API["vendors.js\ngetVendors()\nuploadComplianceDoc()"]
INV_API["invoices.js\ngetInvoices()\napproveInvoice()"]
CHAT_API["chatbot.js\nsendMessage() ← streaming"]
end
subgraph Config["Config (src/config/)"]
ENV["env.js\nAPI_URL\nFEATURE_REAL_API\nGROQ_API_KEY"]
end
subgraph Backend["FastAPI Backend"]
BE["/api/v1/*"]
end
C1 & C2 & C3 --> H1 & H2 & H3
C4 --> H1
C5 --> H4
C6 --> CHAT_API
H1 --> PROP_API
H2 --> MEET_API
H3 --> AUTH_API
H4 --> VEND_API
H5 --> INV_API
AUTH_API & PROP_API & MEET_API & VEND_API & INV_API & CHAT_API --> CLIENT
INTER -->|"wraps"| CLIENT
CLIENT --> ENV
CLIENT -->|"HTTP + cookies"| BE
style CLIENT fill:#1e3a5f,color:#fff
style INTER fill:#1e5f3a,color:#fff
style ENV fill:#5f3a1e,color:#fff
```
---
## Diagram 2 — 401 Interceptor State Machine
The queue-based pattern that handles concurrent requests all expiring at the same time. Only one refresh call is ever made.
```mermaid
stateDiagram-v2
[*] --> Idle : App initialised\ninterceptors.js imported
Idle --> RequestInFlight : Any API call made
RequestInFlight --> Success200 : Response 2xx
Success200 --> Idle : Return response to caller
RequestInFlight --> Error401 : Response 401
Error401 --> AlreadyRetried : originalRequest._retry == true
AlreadyRetried --> PropagateError : Reject — do not loop
Error401 --> RefreshInProgress : isRefreshing == true\n(another request is already refreshing)
RefreshInProgress --> Queued : Push {resolve, reject}\ninto failedQueue
Error401 --> StartRefresh : isRefreshing == false
StartRefresh --> RefreshCall : Set _retry=true\nSet isRefreshing=true\nPOST /auth/refresh
RefreshCall --> RefreshSuccess : 200 — new cookies set\nby backend
RefreshSuccess --> DrainQueue : processQueue(null)\nresume all queued requests
DrainQueue --> RetryOriginal : apiClient(originalRequest)
RetryOriginal --> Success200
RefreshCall --> RefreshFailed : 401 — refresh token\ninvalid/expired/reused
RefreshFailed --> DrainQueueError : processQueue(error)\nreject all queued requests
DrainQueueError --> DispatchLogout : window.dispatchEvent(\n'auth:logout-required'\n)
DispatchLogout --> ForceLogout : AuthContext listener\ncalls logout()\nredirects /login
ForceLogout --> [*]
note right of Queued
Multiple components race
to refresh. First one wins,
others wait here.
end note
note right of RefreshFailed
Refresh token expired
or reuse attack detected.
User must log in again.
end note
```
---
## Diagram 3 — Mock-to-Real Migration: Module Lifecycle
The three phases every integration module (I1I8) goes through. No module skips phases.
```mermaid
flowchart LR
subgraph Phase1["Phase 1 — Feature Flagged (Default OFF)"]
P1_ENV["VITE_FEATURE_REAL_API=false\nin .env.local"]
P1_HOOK["useProperties() hook created\nwith flag branch:\nif (!FEATURE_REAL_API)\n return mockStore data"]
P1_TEST["All existing tests pass\nApp behaves identically\nto before integration started"]
P1_ENV --> P1_HOOK --> P1_TEST
end
subgraph Phase2["Phase 2 — Real API Active"]
P2_ENV["VITE_FEATURE_REAL_API=true\nin .env.local"]
P2_BACKEND["Backend module running\n(e.g. B3 Properties)"]
P2_TEST["QA: real data renders\nloading/error states work\nrole scoping correct"]
P2_ENV --> P2_BACKEND --> P2_TEST
end
subgraph Phase3["Phase 3 — Mock Removed"]
P3_CLEAN["Delete if (!FEATURE_REAL_API)\nbranch from hook"]
P3_FLAG["Remove VITE_FEATURE_REAL_API\nfrom env files"]
P3_MOCK["MockStoreProvider removal\nonly when ALL I1I8 complete"]
P3_CLEAN --> P3_FLAG --> P3_MOCK
end
Phase1 -->|"backend module\nbecomes available"| Phase2
Phase2 -->|"QA signed off"| Phase3
style Phase1 fill:#1e3a5f,color:#fff
style Phase2 fill:#1e5f3a,color:#fff
style Phase3 fill:#5f1e3a,color:#fff
```
---
## Diagram 4 — Feature Flag Decision Tree (Inside Each Hook)
The exact conditional logic inside every `useXxx()` hook created by I1I8.
```mermaid
flowchart TD
HOOK["useProperties(filters)\nor any useXxx() hook called"]
HOOK --> FLAG{FEATURE_REAL_API\n== true?}
FLAG -->|false — mock path| MOCK_STORE["useMockStore()\nRead from MockStoreProvider\nApply filters in JS"]
MOCK_STORE --> MOCK_SHAPE["Return mock-shaped object:\n{ data: [...], loading: false, error: null }"]
FLAG -->|true — real API path| EFFECT["useEffect(() => {\n fetchData()\n return () => { cancelled = true }\n}, [deps])"]
EFFECT --> LOADING["setLoading(true)\nsetError(null)"]
LOADING --> API_CALL["await getProperties(filters)\nsrc/api/properties.js"]
API_CALL --> OK{"Response\nok?"}
OK -->|yes| SET_DATA["setData(result.data)\nsetLoading(false)"]
OK -->|no| SET_ERROR["setError(\n err.response?.data?.detail\n ?? 'Failed to load'\n)\nsetLoading(false)"]
SET_DATA --> REAL_SHAPE["Return real object:\n{ data: [...], loading: false, error: null }"]
SET_ERROR --> ERROR_SHAPE["Return error object:\n{ data: [], loading: false, error: 'message' }"]
subgraph ShapeContract["Shape Contract — Must Match"]
MOCK_SHAPE2["Both paths return:\n{ data: T[], loading: boolean, error: string | null }\n\nComponents never know which path is active"]
end
```
---
## Diagram 5 — Frontend Request Lifecycle
The complete path from a user action in a component all the way to the backend and back, including what each layer is responsible for.
```mermaid
sequenceDiagram
actor User as 👤 User
participant Comp as React Component
participant Hook as useXxx() Hook
participant ApiFile as src/api/properties.js
participant Client as apiClient (client.js)
participant Inter as interceptors.js
participant BE as FastAPI /api/v1
User->>Comp: Interacts (navigate / click / filter)
Comp->>Hook: useProperties({ status: 'Hot Lead' })
Note over Hook: checks FEATURE_REAL_API flag
Hook->>ApiFile: getProperties({ status: 'Hot Lead' })
ApiFile->>Client: apiClient.get('/properties', { params })
Note over Client: Attaches withCredentials=true\nbrowser sends httpOnly cookie automatically
Client->>Inter: Request passes through interceptor
Note over Inter: Response interceptor registered\n(not triggered yet — on response)
Client->>BE: GET /api/v1/properties?status=Hot+Lead\nCookie: access_token=<jwt>
Note over BE: Middleware: CORS → Rate Limit → Logger\nDependencies: get_current_user() → require_role()\nService: property_service.get_properties(role, filters)
BE-->>Client: 200 { data: [...], meta: { total, page, page_size } }
Client-->>Inter: Response passes through interceptor\n(2xx → pass through unchanged)
Inter-->>ApiFile: { data: [...], meta: {...} }
Note over ApiFile: Optional: transform snake_case → camelCase\nassigned_agent_id → agentId
ApiFile-->>Hook: Transformed data object
Hook-->>Comp: { data: [...], loading: false, error: null }
Comp-->>User: Render updated UI
```
---
## Diagram 6 — HTTP Error Handling Decision Tree
How the frontend responds to each HTTP error code the backend can return.
```mermaid
flowchart TD
RES["API response received\nerror.response.status"]
RES --> S400{400\nBad Request}
S400 --> H400["Validation error\n(bad request body)\nShow field-level errors\nfrom error.response.data.detail"]
RES --> S401{401\nUnauthorized}
S401 --> RETRY{originalRequest\n._retry?}
RETRY -->|false — first 401| REFRESH["Trigger token refresh\nPOST /auth/refresh\n(interceptors.js queue)"]
REFRESH --> RFAIL{refresh\nsucceeded?}
RFAIL -->|yes| RETR["Retry original request\nUser sees nothing"]
RFAIL -->|no| LOGOUT["dispatch('auth:logout-required')\nAuthContext.logout()\nRedirect /login"]
RETRY -->|true — already retried| LOGOUT
RES --> S403{403\nForbidden}
S403 --> H403["User authenticated but wrong role\nShow 'Access denied' toast\nor redirect to own portal\n(do not retry)"]
RES --> S404{404\nNot Found}
S404 --> H404["Resource does not exist\nShow empty state or\n'Not found' message"]
RES --> S422{422\nUnprocessable Entity}
S422 --> H422["FastAPI schema validation failed\nShow detail array from\nerror.response.data.detail[]\nHighlight failing fields"]
RES --> S429{429\nRate Limited}
S429 --> H429["Show toast 'Too many requests'\nBackoff — do not auto-retry\nUser must wait"]
RES --> S500{500\nServer Error}
S500 --> H500["Show generic error toast\n'Something went wrong'\nLog error.response.data to console\n(dev only — never to user)"]
subgraph ErrorShape["All error bodies follow this shape"]
ES["{\n detail: 'Human-readable message',\n code: 'MACHINE_CODE'\n}\n\n422 uses FastAPI default:\n{\n detail: [\n { loc: ['body','field'], msg: '...', type: '...' }\n ]\n}"]
end
```
---
## Diagram 7 — Frontend Environment Variable Flow
How `VITE_API_URL` travels from `.env.local` through the build system into every API call.
```mermaid
graph LR
subgraph Files["Source Files"]
ENV_LOCAL[".env.local\n(never committed)\nVITE_API_URL=http://localhost:8000/api/v1"]
ENV_PROD[".env.production\n(Vercel env vars)\nVITE_API_URL=https://api.lynkeduppro.com/api/v1"]
ENV_EXAMPLE[".env.example\n(committed template)\nVITE_API_URL="]
end
subgraph Build["Vite Build Process"]
VITE["Vite reads .env.local\nDuring vite dev / vite build\nOnly VITE_ prefix exposed to bundle\nAll others stripped"]
end
subgraph Runtime["Browser Runtime"]
META["import.meta.env.VITE_API_URL\n(string | undefined)\nbaked into JS bundle at build time"]
end
subgraph Config["src/config/env.js"]
EXPORT["export const API_URL =\n import.meta.env.VITE_API_URL\n ?? 'http://localhost:8000/api/v1'\n\nexport const FEATURE_REAL_API =\n import.meta.env.VITE_FEATURE_REAL_API === 'true'\n\nexport const GROQ_API_KEY =\n import.meta.env.VITE_GROQ_API_KEY"]
end
subgraph ApiClient["src/api/client.js"]
CLIENT["axios.create({\n baseURL: API_URL,\n withCredentials: true\n})"]
end
ENV_LOCAL -->|"vite dev"| VITE
ENV_PROD -->|"vite build (Vercel)"| VITE
ENV_EXAMPLE -.->|"template for"| ENV_LOCAL
VITE --> META
META --> EXPORT
EXPORT -->|"API_URL"| CLIENT
EXPORT -->|"FEATURE_REAL_API"| HOOKS["All useXxx() hooks\nmock vs real branch"]
EXPORT -->|"GROQ_API_KEY"| CHATBOT["Chatbot.jsx\n(pre-integration direct calls)"]
subgraph Warning["⚠️ Security Note"]
W1["VITE_ vars are PUBLIC\nThey appear in the JS bundle\nNever put SECRET_KEY, DB passwords,\nor admin tokens here\nThose belong in backend .env only"]
end
```
---
## Diagram 8 — Axios `withCredentials` + CORS Handshake
Why `withCredentials: true` is required and what happens at the browser + server level.
```mermaid
sequenceDiagram
participant Browser as Browser
participant Axios as Axios (withCredentials: true)
participant CORS as FastAPI CORS Middleware
participant Handler as Route Handler
Note over Browser,Handler: Preflight (first cross-origin request)
Browser->>CORS: OPTIONS /api/v1/properties\nOrigin: https://lynkeduppro.vercel.app\nAccess-Control-Request-Method: GET\nAccess-Control-Request-Headers: Content-Type
Note over CORS: Check ALLOWED_ORIGINS list:\n['https://lynkeduppro.vercel.app',\n 'http://localhost:5173']
CORS-->>Browser: 200\nAccess-Control-Allow-Origin: https://lynkeduppro.vercel.app\nAccess-Control-Allow-Credentials: true ← REQUIRED\nAccess-Control-Allow-Methods: GET, POST, PATCH, DELETE\nAccess-Control-Allow-Headers: Content-Type
Note over Browser,Handler: Actual request (cookies attached automatically)
Axios->>CORS: GET /api/v1/properties\nOrigin: https://lynkeduppro.vercel.app\nCookie: access_token=<jwt> ← browser attaches because withCredentials=true
CORS->>Handler: Pass through (origin allowed)
Handler-->>CORS: 200 { data: [...] }
CORS-->>Browser: 200 { data: [...] }\nAccess-Control-Allow-Origin: https://lynkeduppro.vercel.app\nAccess-Control-Allow-Credentials: true
Note over Browser: Without withCredentials: true\nthe Cookie header would not be sent\neven for same-site API on different port (localhost:8000)
subgraph BackendConfig["Backend: main.py CORS config"]
BC["CORSMiddleware(\n allow_origins=settings.backend_cors_origins,\n allow_credentials=True, ← REQUIRED for cookies\n allow_methods=['*'],\n allow_headers=['*'],\n)"]
end
```
+381
View File
@@ -0,0 +1,381 @@
# 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
```