7694788387
- 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
389 lines
14 KiB
Markdown
389 lines
14 KiB
Markdown
# 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
|
|
```
|