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>
This commit is contained in:
181
docs/architecture.md
Normal file
181
docs/architecture.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# 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".
|
||||
Reference in New Issue
Block a user