diff --git a/README.md b/README.md index c0df0e3..5a1d17a 100644 --- a/README.md +++ b/README.md @@ -17,18 +17,19 @@ Monorepo Nx listo para producciΓ³n que incluye autenticaciΓ³n JWT con rotaciΓ³n ### 🎯 **Stack TecnolΓ³gico** - **Frontend**: Angular 21 con standalone components, Signals API y control flow nativo (`@if` / `@for` / `@switch`) -- **Gateway**: Express 5 + `http-proxy-middleware` β€” ΓΊnico servicio expuesto a Internet, gestiona tokens y CORS -- **API**: Express 5 + Sequelize, vive en red privada, expone CRUDs y endpoints `/internal/*` para el gateway +- **Nginx (front)**: sirve la SPA y es la puerta pΓΊblica; hace reverse-proxy de `/api/*` al gateway (mismo origen) +- **Gateway**: Express 5 + `http-proxy-middleware` β€” privado (`internal-network`), detrΓ‘s de nginx; gestiona tokens y CORS +- **API**: Express 5 + Sequelize, privado (`internal-network`), expone CRUDs y endpoints `/internal/*` para el gateway - **Base de datos**: PostgreSQL 16 - **Monorepo**: Nx 22 para gestiΓ³n eficiente - **Build System**: esbuild (backend) + Vite (frontend) -- **ContainerizaciΓ³n**: Docker + Docker Compose con redes `edge-network` e `internal-network` +- **ContainerizaciΓ³n**: Docker + Docker Compose; sΓ³lo `front` (nginx) se expone, el resto vive en `internal-network` (`internal: true`) - **UI**: Bootstrap 5 + NgBootstrap - **i18n**: Transloco (EspaΓ±ol/Valenciano/InglΓ©s) ### πŸ” **AutenticaciΓ³n & Seguridad** β€” ver [`docs/SECURITY.md`](docs/SECURITY.md) -- Arquitectura de microservicios: **gateway** pΓΊblico + **api** privado. El api nunca habla con el cliente directamente; el gateway firma un JWT interno EdDSA antes de proxiar +- Arquitectura de microservicios: **nginx** (front) como puerta pΓΊblica β†’ **gateway** (auth) β†’ **api** privado. El api nunca habla con el cliente directamente; el gateway firma un JWT interno EdDSA antes de proxiar - JWT del cliente con **dos secretos separados** (`JWT_ACCESS_SECRET`, `JWT_REFRESH_SECRET`), claims `typ` y `jti` - **RotaciΓ³n de refresh con detecciΓ³n de reuso**: tabla `refresh_token_family` revoca la familia completa si una cookie ya rotada se vuelve a presentar - **JWT interno Ed25519** entre gateway y api: el gateway tiene la clave privada (firma), el api sΓ³lo la pΓΊblica (verifica). Privilegio separado: un api comprometido no puede emitir tokens @@ -47,14 +48,30 @@ Monorepo Nx listo para producciΓ³n que incluye autenticaciΓ³n JWT con rotaciΓ³n ### πŸ—οΈ **Arquitectura** -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” cookie+Authorization β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” X-Internal-Auth (EdDSA) β”Œβ”€β”€β”€β”€β”€β” -β”‚ Cliente β”‚ ─────────────────────────▢│ Gateway β”‚ ──────────────────────────▢ β”‚ API β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ - (pΓΊblico) (privada) - :3100 :3200 +```mermaid +flowchart LR + Browser(["🌐 Navegador"]) + + subgraph priv ["πŸ”’ internal-network Β· internal: true (sin salida a Internet)"] + direction LR + Nginx["Nginx Β· front
SPA Angular + proxy /api/*"] + Gateway["Gateway Β· :3100
auth JWT cliente
firma JWT interno EdDSA"] + API["API Β· :3200
Express 5 + Sequelize"] + DB[("PostgreSQL 16")] + + Nginx -->|"proxy_pass /api/"| Gateway + Gateway -->|"X-Internal-Auth Β· EdDSA"| API + API --> DB + end + + Browser ==>|"HTTPS Β· cookie + Authorization
ΓΊnica puerta pΓΊblica"| Nginx + + classDef public fill:#1f6feb,stroke:#0b3d91,color:#fff; + class Nginx public; ``` +- **Nginx (contenedor `front`)** es la **ΓΊnica puerta pΓΊblica**: sirve la SPA y hace reverse-proxy de `/api/*` al gateway (mismo origen β†’ las cookies viajan sin CORS). El navegador nunca habla con el gateway directamente. +- **Gateway**, **API** y **PostgreSQL** viven todos en `internal-network` (`internal: true`), sin entrada desde Internet. El gateway autentica el JWT del cliente, firma el JWT interno EdDSA y proxia al api privado. - PatrΓ³n Controller-Service-Repository en el api - DTOs compartidos entre frontend y backend en `libs/rest-dto` - Contrato interno gateway↔api en `libs/internal-auth` (Ed25519 + scopes) @@ -95,7 +112,7 @@ npm run dev ### Acceso a la AplicaciΓ³n - **Frontend**: http://localhost:4200 -- **Gateway (pΓΊblico)**: http://localhost:3100/api/v1/ +- **Gateway (API del cliente)**: http://localhost:3100/api/v1/ (en dev el front la consume vΓ­a proxy de Vite; en docker, tras nginx) - **API (privado)**: http://localhost:3200 (sΓ³lo accesible vΓ­a gateway en docker) - **Base de datos**: localhost:5432 @@ -170,7 +187,7 @@ nx-fullstack-starter/ β”‚ β”‚ β”‚ β”œβ”€β”€ libs/auth/ # MΓ³dulo de autenticaciΓ³n (service, guards) β”‚ β”‚ β”‚ └── services/ # Servicios de negocio β”‚ β”‚ └── src/assets/i18n/ # Archivos de traducciΓ³n -β”‚ β”œβ”€β”€ gateway/ # Servicio pΓΊblico (Express + http-proxy-middleware) +β”‚ β”œβ”€β”€ gateway/ # Servicio de auth + proxy, privado tras nginx (Express + http-proxy-middleware) β”‚ β”‚ └── src/ β”‚ β”‚ β”œβ”€β”€ controllers/ # auth.controller (login/logout) β”‚ β”‚ β”œβ”€β”€ middleware/ # hasPermission, refresh rotation @@ -245,7 +262,7 @@ Especialista en diseΓ±o de esquemas PostgreSQL y MongoDB, migraciones sin downti #### πŸ”§ Backend Developer -Especialista en Express + Sequelize siguiendo arquitectura de 4 capas: Routes β†’ Controllers β†’ Services β†’ Models. Trabaja sobre `apps/api` (lΓ³gica de negocio) y `apps/gateway` (auth pΓΊblico, proxy). +Especialista en Express + Sequelize siguiendo arquitectura de 4 capas: Routes β†’ Controllers β†’ Services β†’ Models. Trabaja sobre `apps/api` (lΓ³gica de negocio) y `apps/gateway` (auth de cliente, proxy). - Patrones `AbstractCrudService` / `AbstractCrudController` para minimizar boilerplate - Todas las respuestas HTTP a travΓ©s de `HttpResponser` (nunca `res.json()` directo) @@ -419,7 +436,7 @@ npm run build docker compose --env-file .env up -d ``` -> SΓ³lo `gateway` y `front` exponen puertos al exterior. `api` y `postgresdb` viven en `internal-network` con `internal: true`. +> SΓ³lo `front` (nginx) se expone al exterior. `gateway`, `api` y `postgresdb` viven en `internal-network` con `internal: true` y no son accesibles desde Internet. ### 🌍 **InternacionalizaciΓ³n** diff --git a/apps/gateway/AGENTS.md b/apps/gateway/AGENTS.md index 93e2f1d..bf89628 100644 --- a/apps/gateway/AGENTS.md +++ b/apps/gateway/AGENTS.md @@ -3,8 +3,10 @@ > Layer-specific rules for the gateway. See the root `AGENTS.md` for global > conventions and path aliases. -The gateway is the **only service exposed to the public**. It owns the public-facing -auth surface, then proxies authorized requests to the internal API. +The gateway owns the **client-facing auth surface**, but it is **not exposed to the +Internet**: it lives on `internal-network` (`internal: true`) and sits behind Nginx (the +`front` container reverse-proxies `/api/*` to it). It then proxies authorized requests to +the internal API on the same private network. ## Responsibilities @@ -21,11 +23,11 @@ auth surface, then proxies authorized requests to the internal API. ## Request flow (must stay intact) ``` -client ──access token──▢ gateway - β”‚ hasPermission() β†’ res.locals.user - β”‚ signUserContext() β†’ internal EdDSA JWT - β–Ό - API /v1/* (verifies internal token, not the public one) +browser ──/api/*──▢ nginx ──proxy_pass──▢ gateway + β”‚ hasPermission() β†’ res.locals.user + β”‚ signUserContext() β†’ internal EdDSA JWT + β–Ό + API /v1/* (verifies internal token, not the public one) ``` ## Hard rules diff --git a/compose.yaml b/compose.yaml index 768459d..ac45cb3 100644 --- a/compose.yaml +++ b/compose.yaml @@ -64,10 +64,7 @@ services: - INTERNAL_JWT_PRIVATE_KEY=${INTERNAL_JWT_PRIVATE_KEY} - CORS_ORIGIN=${CORS_ORIGIN} - SERVICE_FQDN_GATEWAY=${SERVICE_FQDN_GATEWAY} - ports: - - '${GATEWAY_PORT:-3100}' networks: - - edge-network - internal-network front: @@ -84,6 +81,7 @@ services: - '${FRONT_PORT:-80}' networks: - edge-network + - internal-network volumes: starter-postgresdb: diff --git a/docs/README_eng.md b/docs/README_eng.md index 86ffea3..3c30dd7 100644 --- a/docs/README_eng.md +++ b/docs/README_eng.md @@ -17,18 +17,19 @@ Production-ready Nx monorepo including JWT authentication with refresh rotation, ### 🎯 **Tech Stack** - **Frontend**: Angular 21 with standalone components, Signals API and native control flow (`@if` / `@for` / `@switch`) -- **Gateway**: Express 5 + `http-proxy-middleware` β€” the only service exposed to the Internet, handles tokens and CORS -- **API**: Express 5 + Sequelize, lives in a private network, exposes CRUDs and `/internal/*` endpoints used by the gateway +- **Nginx (front)**: serves the SPA and is the public door; reverse-proxies `/api/*` to the gateway (same origin) +- **Gateway**: Express 5 + `http-proxy-middleware` β€” private (`internal-network`), behind nginx; handles tokens and CORS +- **API**: Express 5 + Sequelize, private (`internal-network`), exposes CRUDs and `/internal/*` endpoints used by the gateway - **Database**: PostgreSQL 16 - **Monorepo**: Nx 22 for efficient management - **Build System**: esbuild (backend) + Vite (frontend) -- **Containerisation**: Docker + Docker Compose with split `edge-network` and `internal-network` +- **Containerisation**: Docker + Docker Compose; only `front` (nginx) is exposed, everything else lives on `internal-network` (`internal: true`) - **UI**: Bootstrap 5 + NgBootstrap - **i18n**: Transloco (Spanish / Valencian / English) ### πŸ” **Authentication & Security** β€” see [`SECURITY.md`](SECURITY.md) -- Microservices architecture: public **gateway** + private **api**. The api never talks directly to clients; the gateway signs a short-lived EdDSA internal JWT before proxying. +- Microservices architecture: **nginx** (front) as the public door β†’ **gateway** (auth) β†’ private **api**. The api never talks directly to clients; the gateway signs a short-lived EdDSA internal JWT before proxying. - Client JWTs with **two separate secrets** (`JWT_ACCESS_SECRET`, `JWT_REFRESH_SECRET`), plus `typ` and `jti` claims. - **Refresh rotation with reuse detection**: the `refresh_token_family` table revokes the whole family when an already-rotated cookie is replayed. - **Internal Ed25519 JWT** between gateway and api: the gateway holds the private key (signs), the api only the public key (verifies). Privilege separation: a compromised api cannot mint tokens. @@ -47,14 +48,30 @@ Production-ready Nx monorepo including JWT authentication with refresh rotation, ### πŸ—οΈ **Architecture** -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” cookie+Authorization β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” X-Internal-Auth (EdDSA) β”Œβ”€β”€β”€β”€β”€β” -β”‚ Client β”‚ ─────────────────────────▢│ Gateway β”‚ ──────────────────────────▢ β”‚ API β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ - (public) (private) - :3100 :3200 +```mermaid +flowchart LR + Browser(["🌐 Browser"]) + + subgraph priv ["πŸ”’ internal-network Β· internal: true (no Internet access)"] + direction LR + Nginx["Nginx Β· front
Angular SPA + proxy /api/*"] + Gateway["Gateway Β· :3100
client JWT auth
signs internal EdDSA JWT"] + API["API Β· :3200
Express 5 + Sequelize"] + DB[("PostgreSQL 16")] + + Nginx -->|"proxy_pass /api/"| Gateway + Gateway -->|"X-Internal-Auth Β· EdDSA"| API + API --> DB + end + + Browser ==>|"HTTPS Β· cookie + Authorization
the only public door"| Nginx + + classDef public fill:#1f6feb,stroke:#0b3d91,color:#fff; + class Nginx public; ``` +- **Nginx (the `front` container)** is the **only public door**: it serves the SPA and reverse-proxies `/api/*` to the gateway (same origin β†’ cookies travel without CORS). The browser never talks to the gateway directly. +- **Gateway**, **API** and **PostgreSQL** all live on `internal-network` (`internal: true`), with no inbound from the Internet. The gateway authenticates the client JWT, signs the internal EdDSA JWT and proxies to the private api. - Controller-Service-Repository pattern inside the api - Shared DTOs between frontend and backend in `libs/rest-dto` - Internal gateway↔api contract in `libs/internal-auth` (Ed25519 + scopes) @@ -94,7 +111,7 @@ npm run dev ### Application Access - **Frontend**: http://localhost:4200 -- **Gateway (public)**: http://localhost:3100/api/v1/ +- **Gateway (client API)**: http://localhost:3100/api/v1/ (in dev the front consumes it via the Vite proxy; under docker, behind nginx) - **API (private)**: http://localhost:3200 (only reachable via the gateway under docker) - **Database**: localhost:5432 @@ -169,7 +186,7 @@ nx-fullstack-starter/ β”‚ β”‚ β”‚ β”œβ”€β”€ libs/auth/ # Authentication module (service, guards) β”‚ β”‚ β”‚ └── services/ # Business services β”‚ β”‚ └── src/assets/i18n/ # Translation files -β”‚ β”œβ”€β”€ gateway/ # Public service (Express + http-proxy-middleware) +β”‚ β”œβ”€β”€ gateway/ # Auth + proxy service, private behind nginx (Express + http-proxy-middleware) β”‚ β”‚ └── src/ β”‚ β”‚ β”œβ”€β”€ controllers/ # auth.controller (login/logout) β”‚ β”‚ β”œβ”€β”€ middleware/ # hasPermission, refresh rotation @@ -244,7 +261,7 @@ Expert in PostgreSQL and MongoDB schema design, zero-downtime migrations, indexi #### πŸ”§ Backend Developer -Expert in Express + Sequelize following a 4-layer architecture: Routes β†’ Controllers β†’ Services β†’ Models. Works across `apps/api` (business logic) and `apps/gateway` (public auth, proxy). +Expert in Express + Sequelize following a 4-layer architecture: Routes β†’ Controllers β†’ Services β†’ Models. Works across `apps/api` (business logic) and `apps/gateway` (client auth, proxy). - `AbstractCrudService` / `AbstractCrudController` patterns to minimise boilerplate - All HTTP responses through `HttpResponser` (never bare `res.json()`) @@ -418,7 +435,7 @@ npm run build docker compose --env-file .env up -d ``` -> Only `gateway` and `front` expose ports to the host. `api` and `postgresdb` live on `internal-network` with `internal: true`. +> Only `front` (nginx) is exposed. `gateway`, `api` and `postgresdb` live on `internal-network` with `internal: true` and are not reachable from the Internet. ### 🌍 **Internationalisation** diff --git a/docs/SECURITY.md b/docs/SECURITY.md index cfe3412..a6f7c97 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -5,14 +5,34 @@ pasos manuales obligatorios antes de desplegar. ## Modelo de confianza -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” cookie+Authorization β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” X-Internal-Auth (EdDSA) β”Œβ”€β”€β”€β”€β”€β” -β”‚ Cliente β”‚ ───────────────────────────▢│ Gateway β”‚ ───────────────────────────▢ β”‚ API β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ - (privada) (pΓΊblica) +```mermaid +flowchart LR + Client(["🌐 Cliente"]) + + subgraph priv ["πŸ”’ internal-network Β· internal: true"] + direction LR + Nginx["Nginx Β· front
SPA + proxy /api/*"] + Gateway["Gateway
JWT_ACCESS_SECRET / JWT_REFRESH_SECRET (HS256)
INTERNAL_JWT_PRIVATE_KEY (Ed25519, firma)"] + API["API
INTERNAL_JWT_PUBLIC_KEY (Ed25519, sΓ³lo verifica)"] + DB[("PostgreSQL")] + + Nginx -->|"proxy_pass /api/"| Gateway + Gateway -->|"X-Internal-Auth Β· EdDSA"| API + API --> DB + end + + Client ==>|"cookie + Authorization
ΓΊnica puerta pΓΊblica"| Nginx + + classDef public fill:#1f6feb,stroke:#0b3d91,color:#fff; + class Nginx public; ``` -- **Gateway** es el ΓΊnico servicio expuesto a Internet. Posee: +- **Nginx** (contenedor `front`) es la puerta pΓΊblica: sirve la SPA y hace + reverse-proxy de `/api/*` al gateway (mismo origen, para que las cookies + viajen sin CORS). El cliente nunca contacta al gateway directamente. +- **Gateway** vive en `internal-network` (privado, sin entrada desde Internet) β€” + es el servicio que firma los tokens de cara al cliente y proxia hacia el api + privado. Posee: - `JWT_ACCESS_SECRET` y `JWT_REFRESH_SECRET` (HS256) para los tokens del cliente. - `INTERNAL_JWT_PRIVATE_KEY` (Ed25519) para firmar las llamadas que