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
14 KiB
14 KiB
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.
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.
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
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
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
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
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.
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.
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.
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