Files
backend/docs/architecture.md

182 lines
6.5 KiB
Markdown
Raw Permalink Normal View History

fix(security): resolve audit findings — secrets, env contract, migrations Removes hardcoded fallback secrets and makes a misconfigured deploy fail loudly instead of silently falling back to development defaults. - Remove insecure JWT fallback secrets (messages.module, configuration) - Remove the 'default-secret' fallback for the 2FA TOTP encryption key and allow a dedicated TWO_FACTOR_ENCRYPTION_KEY so rotating JWT_SECRET no longer locks out every 2FA user (see docs/2fa-key-rotation.md) - Require EMAIL_API_URL; drop the hardcoded vendor email endpoint - Drive WebSocket CORS from CORS_ORIGINS instead of origin:'*' - Load .env before any Nest module is imported (src/load-env.ts). Decorator arguments evaluate at import time, so the gateway previously froze its CORS config to the localhost fallback even when CORS_ORIGINS was set - Add boot-time env validation: missing required vars, weak JWT_SECRET, and inverted access/refresh token lifetimes now abort startup - Enable Redis TLS certificate verification - Require ADMIN_EMAIL/ADMIN_PASSWORD for the seed; remove the published default super-admin credentials and stop printing them - Add the initial Prisma migration and stop gitignoring prisma/migrations - Make .env.example an accurate configuration contract (admin bootstrap, REDIS_TLS, S3_ENDPOINT, 2FA key, Firebase path; drop the dead SMTP block) - Add handover documentation: architecture, ER model, sequence and data-flow diagrams, 2FA key rotation runbook Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:30:36 +05:30
# RE:Quest — system architecture
Handover documentation. Diagrams are Mermaid inside Markdown so they are
editable and diffable in version control.
- [Data model / ERD](./data-model.md)
- [Workflow (sequence) diagrams](./flows.md)
- [2FA key rotation runbook](./2fa-key-rotation.md)
---
## 1. Components
| Component | Stack | Repo | Notes |
|---|---|---|---|
| API | NestJS (Node), REST + Socket.IO | `backend` | Global prefix `/api/v1`; Swagger at `/docs` when enabled |
| Web app | Next.js App Router, NextAuth | `frontend` | Public site + user and agent portals |
| Admin panel | Next.js App Router | `adminpanel` | Internal dashboard, no `next/image` remote loading |
| Mobile app | Flutter | `mobile-app` | Talks to the same REST + Socket.IO API |
| Database | PostgreSQL (DigitalOcean Managed) | — | Prisma ORM, 24 entities |
| Cache / pub-sub | Redis / Valkey (DigitalOcean Managed) | — | Presence tracking + Socket.IO adapter for multi-instance fan-out |
| Object storage | S3-compatible (DigitalOcean Spaces) | — | Browser uploads via presigned URLs; DB stores object keys only |
| Payments | Stripe | — | Checkout Sessions + Billing Portal + webhooks |
| Push | Firebase Cloud Messaging | — | Admin SDK server-side, FCM tokens client-side |
| Email | HTTP JSON gateway (`EMAIL_API_URL`) | — | **No SMTP path exists in the code** |
| Auth (social) | Google via NextAuth | — | Frontend verifies, then posts profile to `POST /auth/social` |
### Component diagram
```mermaid
flowchart TB
subgraph Clients
WEB[Web app<br/>Next.js]
ADM[Admin panel<br/>Next.js]
MOB[Mobile app<br/>Flutter]
end
subgraph API["Backend API — NestJS"]
REST[REST controllers<br/>/api/v1]
WS[Socket.IO gateway<br/>MessagesGateway]
EV[Event emitter<br/>in-process]
end
subgraph Data
PG[(PostgreSQL<br/>Prisma)]
RD[(Redis / Valkey<br/>presence + SIO adapter)]
S3[(Object storage<br/>S3-compatible)]
end
subgraph ThirdParty["Third-party services"]
STR[Stripe]
FCM[Firebase FCM]
MAIL[Email gateway<br/>EMAIL_API_URL]
GOO[Google OAuth]
end
WEB --> REST
ADM --> REST
MOB --> REST
WEB <--> WS
ADM <--> WS
MOB <--> WS
WEB -.NextAuth.-> GOO
WEB -- presigned PUT --> S3
MOB -- presigned PUT --> S3
REST --> PG
REST --> RD
REST --> S3
WS --> PG
WS --> RD
REST --> EV
WS --> EV
EV --> MAIL
EV --> FCM
REST --> STR
STR -- webhook --> REST
```
Notes on the diagram:
- **Files never stream through the API.** Clients ask the API for a presigned
URL (`POST /upload/*-presigned-url`) and then PUT directly to object storage.
Only the resulting object key is persisted.
- **Redis serves two distinct purposes**: `RedisPresenceService` (who is
online) and the Socket.IO Redis adapter, which is what lets more than one API
instance broadcast to the same rooms. Running multiple replicas without Redis
would silently break cross-instance message delivery.
- **Side effects are event-driven.** Controllers emit in-process events
(`user.registered`, `notification.message`, …); `EmailListener` and the
notification service subscribe. Email/push failures are logged, not
propagated to the caller.
---
## 2. Runtime topology
```mermaid
flowchart LR
U[Browser / device] --> CDN[re-quest.com<br/>admin.re-quest.com]
CDN --> APIH[prod.api.re-quest.com]
APIH --> N1[API instance 1]
APIH --> N2[API instance N]
N1 <--> RD[(Redis / Valkey)]
N2 <--> RD
N1 --> PG[(PostgreSQL)]
N2 --> PG
```
`CORS_ORIGINS` must list every client origin. It is consumed twice — by the
HTTP CORS middleware **and** by the Socket.IO gateway. If it is unset, both
fall back to `localhost` and every browser request from the live domain is
rejected.
---
## 3. Authentication model
| Concern | Mechanism |
|---|---|
| Credential storage | Argon2id hashes (`User.password`) |
| Session tokens | JWT access + refresh, both signed with `JWT_SECRET` |
| Access token lifetime | `JWT_ACCESS_EXPIRATION` (recommended `15m`) |
| Refresh token lifetime | `JWT_REFRESH_EXPIRATION` (recommended `7d`, **must exceed the access lifetime**) |
| Refresh token storage | `Session` rows in PostgreSQL — revocable via `/auth/logout-all` |
| Client-side token storage | `localStorage` (web) — XSS-exfiltratable; migration to httpOnly cookies is an open item |
| Social login | Google only. NextAuth verifies in the frontend and posts the verified profile to `POST /auth/social` |
| 2FA | TOTP (speakeasy). Secret is AES-256-GCM encrypted at rest with a key derived from `TWO_FACTOR_ENCRYPTION_KEY` (falling back to `JWT_SECRET`) |
| WebSocket auth | JWT passed in `handshake.auth.token`; verified on connect, socket disconnected on failure |
The Facebook and Twitter OAuth variables present in some environments are
**not consumed by the backend**. Google is the only enabled social provider.
---
## 4. Roles and authorisation
`UserRole` is one of `USER`, `AGENT`, `ADMIN`, `SUPER_ADMIN`. Route access is
enforced by guards on the controllers; the admin panel is a client of the same
API and holds no privileges of its own.
Known gap: agent approval does **not** verify an active paid subscription on
the server. Subscription state is displayed to the admin, but approval is not
gated on it in the backend.
---
## 5. Configuration contract
`backend/.env.example` is the authoritative list. Validation runs at boot
(`src/config/env.validation.ts`) and the process **exits** if:
- `DATABASE_URL`, `JWT_SECRET` or `EMAIL_API_URL` is missing
- `NODE_ENV=production` and `CORS_ORIGINS` or `FRONTEND_URL` is missing
- `NODE_ENV=production` and `JWT_SECRET` is shorter than 32 characters
- `JWT_REFRESH_EXPIRATION` is not longer than `JWT_ACCESS_EXPIRATION`
Warnings (logged, non-fatal): access token lifetime over one hour, `REDIS_TLS`
disabled in production, `sslmode=no-verify` in `DATABASE_URL`, `localhost` in a
production `CORS_ORIGINS`.
`.env` files are loaded by `src/load-env.ts`, which must remain the first
import of `main.ts` — see the comment in that file for why.
---
## 6. Database change process
The schema is versioned under `backend/prisma/migrations`. `prisma db push`
must not be used against any shared environment.
An environment whose schema was created with `db push` (as production was)
needs baselining once, before the first `migrate deploy`:
```bash
npx prisma migrate resolve --applied 20260721102313_init
npx prisma migrate deploy
```
Without the baseline, `migrate deploy` fails with "relation already exists".