Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 31 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 · <b>front</b><br/>SPA Angular + proxy /api/*"]
Gateway["<b>Gateway</b> · :3100<br/>auth JWT cliente<br/>firma JWT interno EdDSA"]
API["<b>API</b> · :3200<br/>Express 5 + Sequelize"]
DB[("PostgreSQL 16")]

Nginx -->|"proxy_pass /api/"| Gateway
Gateway -->|"X-Internal-Auth · EdDSA"| API
API --> DB
end

Browser ==>|"HTTPS · cookie + Authorization<br/>ú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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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**

Expand Down
16 changes: 9 additions & 7 deletions apps/gateway/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
4 changes: 1 addition & 3 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -84,6 +81,7 @@ services:
- '${FRONT_PORT:-80}'
networks:
- edge-network
- internal-network

volumes:
starter-postgresdb:
Expand Down
45 changes: 31 additions & 14 deletions docs/README_eng.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 · <b>front</b><br/>Angular SPA + proxy /api/*"]
Gateway["<b>Gateway</b> · :3100<br/>client JWT auth<br/>signs internal EdDSA JWT"]
API["<b>API</b> · :3200<br/>Express 5 + Sequelize"]
DB[("PostgreSQL 16")]

Nginx -->|"proxy_pass /api/"| Gateway
Gateway -->|"X-Internal-Auth · EdDSA"| API
API --> DB
end

Browser ==>|"HTTPS · cookie + Authorization<br/>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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()`)
Expand Down Expand Up @@ -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**

Expand Down
32 changes: 26 additions & 6 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 · <b>front</b><br/>SPA + proxy /api/*"]
Gateway["<b>Gateway</b><br/>JWT_ACCESS_SECRET / JWT_REFRESH_SECRET (HS256)<br/>INTERNAL_JWT_PRIVATE_KEY (Ed25519, firma)"]
API["<b>API</b><br/>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<br/>ú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
Expand Down