# PLANS — SSO VietProDev

> Roadmap tổng thể dự án: kiến trúc, hướng đi, refactor, cleanup, test plan.
>
> **Trạng thái:** DRAFT (chờ user confirm trước khi implement)
> **Ngày tạo:** 2026-06-18
> **Cập nhật lần cuối:** 2026-06-18

---

## 0. Bối cảnh

### Hiện trạng (từ scan 2026-06-18)

| Thành phần | Trạng thái |
|------------|------------|
| Backend template (1-5 phases) | ✅ Đã merge vào `develop` |
| OIDC provider + adapter | ✅ Có nhưng **thiếu session storage** (findSession, upsertSession) |
| Email verification (Phase 3) | ✅ Done trong session trước, 8/8 unit test pass |
| OIDC `validateCredentials` | ❌ Vẫn là TODO stub — login thật chưa hoạt động |
| `prompt=none` (silent SSO) | ❌ Chưa implement |
| Cross-project session sharing | ❌ Cookie domain không khớp localhost pattern |
| Remember consent | ❌ Mỗi lần phải re-approve scopes |
| Front-channel / back-channel logout | ❌ Chưa enable |
| Pre-existing TS errors | ❌ 7 errors: `verify-email.ts` (3), `resend-verification.ts` (3), `server.ts` (2) |
| `.env` port mismatch | ❌ Đang trỏ `5433` (cũ) nhưng Docker chỉ map `5432` |
| 18 untracked files cũ (từ stash) | ⚠️ Rác — cần dọn |
| `MultiPoolService` auto-load | ❌ `autoLoadPools()` method bị reference nhưng không tồn tại |

### Ràng buộc kiến trúc (từ user)

1. **1 DB chính** (PostgreSQL 18) — `sso`
2. **1 DB backup** (PostgreSQL 18) — `sso_backup` — load balancing với DB chính
3. **1 MongoDB** — `sso_audit` cho audit logs
4. **1 Redis** (optional) — cache, rate limit, queue
5. **2 demo apps**: project-a (port 4001), project-b (port 4002)
6. **SSO** ở port 3001 + Swagger UI để check API

### Luồng nghiệp vụ mong muốn (từ user)

```
1. User mở project-a (localhost:4001)
2. Click "Login with SSO" → redirect SSO /oauth/authorize
3. SSO hiển thị login page → user nhập credentials
4. SSO trả code → project-a đổi code lấy token → vào profile
5. User mở project-b (localhost:4002) trong tab mới
6. project-b tự động login (silent SSO nhờ OIDC session đã share) → vào profile
7. User logout project-b → SSO session bị xóa → project-a cũng logout (SLO)
```

### Yêu cầu đầu tiên (từ user)

> "Trước hết hiện tại cần file triệt để các lỗi bug ban đầu cho dự án clean"

→ Phase 0 (cleanup + setup) phải chạy được, server start được, migration chạy được, swagger mở được. **Cross-project silent SSO là Phase sau**.

---

## 1. Kiến trúc mục tiêu

```
┌──────────────────────────────────────────────────────────────────┐
│                          Browser (User)                          │
└──────────┬───────────────────────────────────┬──────────────────┘
           │                                   │
           ▼                                   ▼
   ┌───────────────┐                   ┌───────────────┐
   │ project-a     │                   │ project-b     │
   │ :4001         │                   │ :4002         │
   │ (RP Client)   │                   │ (RP Client)   │
   └──────┬────────┘                   └──────┬────────┘
          │ redirect (auth code)              │ redirect (auth code)
          │ exchange (token)                  │ exchange (token)
          │ userinfo (Bearer)                 │ userinfo (Bearer)
          ▼                                   ▼
   ┌──────────────────────────────────────────────────────┐
   │            SSO Backend  (sso-vietprodev)              │
   │                  http://localhost:3001                │
   │  ┌──────────────────────────────────────────────┐    │
   │  │  Express                                       │    │
   │  │  ├── /oauth/*           (oidc-provider)        │    │
   │  │  ├── /oidc/interaction/* (custom UI)          │    │
   │  │  ├── /api/v1/*         (REST)                  │    │
   │  │  ├── /swagger/index    (Swagger UI)            │    │
   │  │  └── /health           (healthcheck)            │    │
   │  └──────────────────────────────────────────────┘    │
   │  ┌──────────────────────────────────────────────┐    │
   │  │  OIDC Provider (oidc-provider v9)              │    │
   │  │  ├── Authorization / Token / UserInfo / JWKS    │    │
   │  │  ├── Session storage (Postgres)                │    │
   │  │  ├── Adapter: oidc-provider-adapter-sequelize  │    │
   │  │  └── features: devInteractions=false,          │    │
   │  │               rpInitiatedLogout=true,           │    │
   │  │               introspection=true,               │    │
   │  │               revocation=true                   │    │
   │  └──────────────────────────────────────────────┘    │
   │  ┌──────────────────────────────────────────────┐    │
   │  │  MultiPoolService                              │    │
   │  │  ├── Pool: 'main'   → postgres-main :5432       │    │
   │  │  ├── Pool: 'backup' → postgres-backup :5433     │    │
   │  │  └── Load balancing: round-robin / fail-over   │    │
   │  └──────────────────────────────────────────────┘    │
   │  ┌──────────────────────────────────────────────┐    │
   │  │  AuditLoggerService                            │    │
   │  │  └── Strategy: direct → MongoDB (port 27017)  │    │
   │  └──────────────────────────────────────────────┘    │
   └────┬──────────────────┬───────────────────┬────────────┘
        │                  │                   │
        ▼                  ▼                   ▼
   ┌─────────┐        ┌─────────┐         ┌─────────┐
   │ postgres│        │ postgres│         │  mongo  │
   │ -main   │        │ -backup │         │ 7       │
   │ :5432   │        │ :5433   │         │ :27017  │
   │ db: sso │        │db: sso_ │         │ db:     │
   │         │        │ backup  │         │sso_audit│
   └─────────┘        └─────────┘         └─────────┘
                                              ▲
                                              │ audit logs
                                              │
                                       (write async, retry)
```

### Container layout (Docker)

| Container | Image | Port host | Port container | DB/User | Volume |
|-----------|-------|-----------|----------------|---------|--------|
| sso-postgres-main | postgres:17-alpine | 5432 | 5432 | `sso` / `sso` | `pg_main_data` |
| sso-postgres-backup | postgres:17-alpine | 5433 | 5432 | `sso_backup` / `sso` | `pg_backup_data` |
| sso-mongo | mongo:7 | 27017 | 27017 | db: `sso_audit` | `mongo_data` |
| sso-redis | redis:7-alpine | 6379 | 6379 | (none) | `redis_data` |
| sso-minio | minio/minio:latest | 9000-9001 | 9000-9001 | `minioadmin` | `minio_data` |
| sso-app-dev | (Dockerfile target=development) | 3001 | 3001 | — | bind mount |

> **Lý do dùng postgres:17-alpine** thay vì postgres:18: hiện tại DB cũ ở 16, ảnh mới có thể là 18. Đồng bộ 17 cho dev + 18 cho prod là hợp lý. Tuy nhiên user nói ảnh có PostgreSQL 18 → sẽ xác nhận lại.

### Load balancing giữa main + backup

2 chiến lược (chọn 1 trong Phase setup):

- **A. Round-robin read + write-through primary**: Mọi write đi vào `main`. Read có thể đi `main` hoặc `backup` (round-robin). Backup chỉ là "warm standby" để query reporting.
- **B. Failover**: Main là primary, backup chỉ active khi main chết. Health check mỗi 30s.

→ **Đề xuất Phase 1 dùng B (failover)** đơn giản hơn. Phase 2+ có thể chuyển sang A nếu cần scale read.

### Cookie domain cho localhost dev

Vì project-a (4001) và project-b (4002) cùng host (`localhost`) nhưng khác port → cookie KHÔNG thể share giữa 2 port trên cùng domain (browsers tách cookie theo `(domain, path, name)`).

→ **Cách giải quyết**: Dùng **subdomain** pattern:
- `sso.localhost:3001` — SSO backend (cần `/etc/hosts` mapping `sso.localhost` → `127.0.0.1`)
- `a.localhost:4001` — project-a
- `b.localhost:4002` — project-b
- Cookie domain: `.localhost` (browsers nhận ra đây là parent domain, share giữa `*.localhost`)

Hoặc đơn giản hơn Phase đầu: **mỗi project tự lưu session riêng** và **project-b chủ động gọi SSO check session** qua `prompt=none` (silent SSO). User mở project-b → tự gọi `GET /oauth/authorize?prompt=none` → SSO check cookie → nếu có session thì trả code ngay (no UI).

→ **Đề xuất Phase 1 dùng cách 2** (silent SSO qua prompt=none). Phase 2+ có thể switch sang subdomain nếu cần SSO logout chéo.

---

## 2. Phases (Roadmap)

> Mỗi phase có **goal rõ ràng**, **acceptance criteria**, **out of scope**, **rollback plan**.

### Phase 0 — Cleanup & Foundation ⏱ 2-3h
**Mục tiêu:** Dự án compile clean, môi trường dev chạy được, Swagger mở được.

**Tasks:**
- [ ] **Backup DB cũ** (safety net): `pg_dump vietprodev_sso > backup_2026-06-18.sql`
- [ ] **Drop DB cũ** + tạo DB mới: `sso` + `sso_backup`
- [ ] Update `docker-compose.yml`:
  - `postgres` → `postgres-main` (port 5432)
  - Thêm `postgres-backup` (port 5433)
  - Cùng image: `postgres:17-alpine`, cùng network `sso-network`
  - User/password: `sso` / `sso` cho cả 2
- [ ] Cập nhật `.env`:
  - `DB_PORT=5432` (sửa từ 5433)
  - `DB_USER=sso`, `DB_PASSWORD=sso`
  - `DB_CONNECTION_STRING=postgresql://sso:sso@localhost:5432/sso`
  - `SSO_LOGIN_BACKUP_URL=postgresql://sso:sso@localhost:5433/sso_backup`
  - `DB_READ_HOST=localhost`, `DB_READ_PORT=5433` (cho MultiPoolService đọc từ backup)
  - Update comment examples ở line 58-59 dùng `sso` thay vì `postgres`
- [ ] Fix pre-existing TS errors (7 errors):
  - `controllers/api/v1/auth/{verify-email,resend-verification}.ts` → thêm 6 schemas vào `contracts/auth/schema.ts`: `VerifyEmailQuerySchema`, `VerifyEmailResponseDataSchema`, `VerifyEmailResponseData`, `ResendVerificationBodySchema`, `ResendVerificationResponseDataSchema`, `ResendVerificationResponseData`
  - `server.ts` line 451, 626 → remove hoặc implement `MultiPoolService.autoLoadPools()` (sẽ implement ở Phase 3, nên Phase 0 chỉ remove call tạm)
- [ ] Dọn 18 file untracked cũ (codebase sso-vietprodev-old):
  - `db_check.js`, `guidelines/`, `postman/`, `secrets/`, `sql/migrations/037-039`, `src/contracts/admin/`, `src/contracts/oidc/`, `src/controllers/admin/`, `src/middlewares/admin-api-key.ts`, `src/oidc/jwksService.ts`, `src/oidc/views/check-email.hbs`, `src/providers/ClientProvider.ts`, `src/services/admin/`, `src/types/`
  - Verify trước khi xóa: diff với `/tmp/sso-stash-backup/files.txt` xem có code hữu ích không
  - Dùng `git clean -fd` cho untracked (KHÔNG drop stash)
- [ ] Verify `npx tsc --noEmit` = 0 errors
- [ ] Verify `docker compose up -d postgres-main postgres-backup mongo redis minio` chạy thành công
- [ ] Verify `pnpm migrate` apply hết 40 migrations
- [ ] Verify `pnpm run dev` start được + `curl http://localhost:3001/health` 200
- [ ] Verify Swagger: mở `http://localhost:3001/swagger/index` thấy UI

**Acceptance criteria:**
- `npx tsc --noEmit` exit 0
- 6 containers running, all healthy (postgres-main, postgres-backup, mongo, redis, minio, [optional: app-dev])
- `pnpm migrate` apply hết 40 migrations
- Server log `[OK] Listening on port 3001`
- `curl http://localhost:3001/health` → 200
- Swagger mở tại `http://localhost:3001/swagger/index` với danh sách endpoint
- `.env` không còn dòng nào reference port 5433 sai
- Working tree clean (không còn untracked cũ)

**Out of scope:** Implement OIDC login thật, silent SSO, full HA, audit MongoDB.

**Rollback:** 
- Code: `git checkout develop -- .` + restore files từ `git stash show stash@{0}` nếu cần
- DB: `psql < backup_2026-06-18.sql` để restore data cũ (nếu đã backup)

---

### Phase 1 — Wire OIDC Login (REST + OIDC) ⏱ 4-6h
**Mục tiêu:** User có thể login qua SSO UI, nhận code, đổi lấy token, gọi userinfo.

**Tasks:**
- [ ] Wire `validateCredentials` trong `oidcInteractionsController.ts`:
  - Dùng `UserProvider.findByEmail` + `UserAuthProvider.findByUserId` + `PasswordService.verifyPassword`
  - Check `user.status` (block nếu `pending_verification` / `inactive` / `suspended`)
  - Brute-force: dùng `UserAuth.login_attempts` + `locked_until`
- [ ] Verify `POST /api/v1/auth/register` đã hoạt động (test bằng curl từ RUN.md)
- [ ] Verify `GET /api/v1/auth/verify-email?token=...` flip `status='active'`
- [ ] Verify `POST /api/v1/auth/login` trả 200 + tokens
- [ ] Test OIDC flow:
  - GET `http://localhost:4001` → click "Login" → redirect SSO
  - SSO login page → nhập `admin@vietprodev.com` / `Vietpro@123`
  - SSO consent page → approve
  - Redirect về project-a `/auth/callback` với code
  - Project-a exchange code → token → userinfo → render profile
- [ ] Fix bug trong `oidcAdapterService`: thiếu `findSession`, `upsertSession`, `destroySession` (oidc-provider yêu cầu)
- [ ] Add 5 unit tests cho `validateCredentials` (mocked UserProvider)

**Acceptance criteria:**
- Login OIDC thành công, project-a render được profile với `user.email`
- Logout project-a xóa session SSO
- `pnpm test` tăng thêm 13/13 tests pass

**Out of scope:** Silent SSO (cross-project), remember consent, SLO.

**Rollback:** `git reset --hard 9ee96c4` (commit Phase 3 cũ).

---

### Phase 2 — Cross-Project Silent SSO ⏱ 6-8h
**Mục tiêu:** Project-b tự động login khi user đã SSO ở project-a.

**Tasks:**
- [ ] Implement `prompt=none` handling trong `oidcInteractionsController.ts`:
  - Trong `GET /:uid`, check `details.prompt.name === 'none'`
  - Nếu có session (cookie `_session_resume` còn hạn) → return code ngay, không render UI
  - Nếu không có session → return `interaction_required` error theo OIDC spec
- [ ] Setup cookie domain `.localhost` cho OIDC session cookie (config trong `oidcService.ts`)
- [ ] Implement đầy đủ `oidcAdapterService` session methods:
  - `findSession`, `getSession`, `upsertSession`, `destroySession`
  - Theo spec oidc-provider SessionAdapter
- [ ] Update `findAccount` để load claims thật từ `User` + `UserAuth` (không chỉ `{ sub }`)
- [ ] Update `oidcService.ts` features:
  - `resourceIndicators: { defaultResource: () => '...' }` (nếu cần)
  - `clientBasedCORS: true` (cho phép CORS cho client từ origin khác nhau)
- [ ] Update `oidcService.ts` cookies: thêm `domain: '.localhost'` cho `long` và `short`
- [ ] Add helper `addSessionCookie()` trong `oidcInteractionsController.ts` (set cookie cross-project)
- [ ] Update project-a/server.js và project-b/server.js:
  - Thêm `/auth/silent-login` route: gọi `GET {SSO}/oauth/authorize?prompt=none&...`
  - Khi load `/`, check session → nếu chưa có thì silent-login trước
- [ ] Add 5 integration tests cho silent SSO flow

**Acceptance criteria:**
- Mở project-a → login → mở project-b tab mới → tự động có profile (không cần nhập lại)
- Logout project-a → project-b session invalid → redirect login
- Cookies: `_session` cookie có domain `.localhost`, share được giữa `localhost:4001` và `localhost:4002`

**Out of scope:** Front-channel logout UI, back-channel logout, sub-domain based session.

**Rollback:** Revert `oidcService.ts`, `oidcInteractionsController.ts`, `oidcAdapterService.ts` về commit trước.

---

### Phase 3 — HA: Main + Backup + Load Balancing ⏱ 8-10h
**Mục tiêu:** MultiPoolService xử lý 2 DBs với failover, health check, audit logs sang MongoDB.

**Tasks:**
- [ ] Implement `MultiPoolService.autoLoadPools()`:
  - Đọc từ table `project_db_connections` (đã có schema từ migration 037)
  - Tạo pool cho main (`name='main'`) + backup (`name='backup'`) + project DBs
  - Cache config 5 phút
- [ ] Health check mỗi 30s cho cả main + backup
- [ ] Implement fail-over logic:
  - Khi `main.healthCheck()` fail → đánh dấu `degraded`
  - Write queries auto-redirect sang `backup` khi main down
  - Background task retry main mỗi 60s
- [ ] Audit logs:
  - Mặc định ghi vào MongoDB
  - Add `MongoAuditRepository` (đã có sẵn 1 phần trong `audit/auditRepository.ts`)
  - Test: write audit → check MongoDB
- [ ] Thêm env vars:
  - `DB_MAIN_*` (riêng main, thay vì `DB_*`)
  - `DB_BACKUP_*` (riêng backup)
  - Giữ backward compat: nếu `DB_*` set mà `DB_MAIN_*` không thì dùng `DB_*` cho main
- [ ] Test failover: stop container `postgres-main` → server vẫn serve write (qua backup)
- [ ] Test audit: trigger login → check MongoDB có log mới

**Acceptance criteria:**
- `MultiPoolService` có 2 pools registered: `main` + `backup`
- Stop main → write vẫn succeed (qua backup)
- MongoDB có audit log mỗi login attempt

**Out of scope:** Read load balancing (round-robin), read replica, streaming replication.

**Rollback:** `MultiPoolService` reverts to single pool (current behavior).

---

### Phase 4 — Production Readiness ⏱ 4-6h
**Mục tiêu:** Dự án sẵn sàng cho staging/production.

**Tasks:**
- [ ] Add CSRF protection cho register/login forms
- [ ] Add rate limiting (dùng Redis): max 5 login attempts / 5 min / IP
- [ ] Add audit log retention: 365 ngày (đã có `AUDIT_RETENTION_DAYS`)
- [ ] Add HTTPS redirect (production mode)
- [ ] Generate production secrets (rotation script)
- [ ] Add OpenAPI spec generator cho mọi REST endpoint
- [ ] Update PROGRESS.md + RUN.md với final state
- [ ] Tag release: `v1.0.0`
- [ ] Push to remote

**Acceptance criteria:**
- `pnpm run build:prod` exit 0
- `docker compose -f docker-compose.yml up` chạy được cho production profile
- All 8 checklist items trong RUN.md §6.5.6 pass

**Out of scope:** K8s manifests, CI/CD pipeline, monitoring/alerting.

**Rollback:** Git tag + revert commit.

---

## 3. Architecture Decisions

### ADR-001: Adapter OIDC = Custom Sequelize (không dùng `oidc-provider-adapter-sequelize`)

**Context:** Có 2 options:
- A. Dùng official `oidc-provider-adapter-sequelize` package
- B. Custom adapter (`oidcAdapterService.ts` hiện tại)

**Decision:** B (custom), vì:
- Schema custom (table `oidc_grants`, `oidc_clients`) đã được define trong migration 035, 036
- Cần control chi tiết để support multi-tenant + audit

**Consequences:**
- (+) Full control + custom queries dễ
- (-) Phải tự maintain — đã có bug thiếu `findSession`/`upsertSession` (Phase 2 sẽ fix)
- (-) Phải tự test compatibility với mỗi version `oidc-provider` upgrade

### ADR-002: Audit log = MongoDB (không Postgres)

**Context:** Có 2 options:
- A. Ghi audit vào Postgres (table `audit_logs` đã có)
- B. Ghi audit vào MongoDB (collection trong `sso_audit`)

**Decision:** B (MongoDB) cho audit high-volume, A (Postgres) cho sensitive audit (auth events). Lý do:
- Audit volume cao (mỗi request có thể có 1-3 audit rows) → Mongo write nhanh hơn
- Mongo dễ scale horizontal
- Postgres giữ data có relation (user, role) — audit móc nối được

**Consequences:**
- (+) Audit không block Postgres write
- (-) Cần monitor MongoDB health riêng
- (-) Phải có strategy fallback nếu Mongo chết (queue retry, đã có trong `audit/auditLoggerService.ts`)

### ADR-003: Silent SSO dùng `prompt=none` (không subdomain)

**Context:** Có 2 options:
- A. Dùng subdomain (`a.localhost:4001`, `b.localhost:4002`) + cookie `.localhost`
- B. Dùng `prompt=none` — mỗi project tự check session khi load

**Decision:** B (`prompt=none`), vì:
- Phase 2 đơn giản hơn, không cần `/etc/hosts` config
- Phù hợp với OIDC standard (nhiều SaaS dùng cách này)
- Browser support `prompt=none` tốt (Chrome, Firefox, Safari)
- Subdomain có thể add sau nếu cần SLO chéo

**Consequences:**
- (+) Đơn giản, dùng được luôn
- (-) Project phải gọi `/auth/silent-login` trước khi render UI
- (-) SLO chéo (logout project-a → project-b cũng logout) cần thêm logic

### ADR-004: Database port = 5432 (main) + 5433 (backup)

**Context:** Docker compose hiện chỉ có 1 postgres ở 5432. Cần 1 postgres nữa cho backup.

**Decision:**
- Main: 5432 (host) → 5432 (container)
- Backup: 5433 (host) → 5432 (container)
- Lý do: convention Postgres standard, dễ nhớ

**Consequences:**
- (+) Nhất quán với convention
- (-) Port 5433 bị conflict nếu user từng có local Postgres ở 5433 (cũ) → fix `.env` thôi

---

## 4. Open Questions & Decisions (đã chốt 2026-06-18)

### Q1: DB strategy ✅
**Decision:** Phase 0 có **2 instance PostgreSQL** ngay (main + backup). Lý do: tránh phải re-architect khi sang Phase 3, đỡ tốn công gấp đôi.

### Q2: Cross-project silent SSO ✅
**Decision:** Hướng **A — `prompt=none` (OIDC standard)**.

**Lý do chọn A (đã phân tích chi tiết):**
1. OIDC standard, tương thích mọi OIDC client
2. Không phá pattern deploy: localhost:3001/4001/4002 giữ nguyên, deploy thật cũng giữ nguyên (chỉ đổi URL trong `.env`)
3. Chrome/Firefox/Safari/Edge support tốt
4. Auth0/Okta/Keycloak đều dùng cách này → kinh nghiệm industry
5. Đơn giản, ít code change

**Hướng B (subdomain `a.localhost`, `b.localhost`) bị loại** vì:
- Cần `/etc/hosts` config mỗi dev
- Cần refactor lại khi deploy thật (subdomain pattern trên production)
- Phá structure hiện tại

**Kết hợp mở rộng (Phase 4):**
- Thêm `frontchannel` logout để SLO chéo: SSO logout → redirect lại các client đang mở
- Có thể thêm `backchannel` logout (production-only, cần HTTPS)

### Q3: DB cũ `vietprodev_sso` ✅
**Decision:** Hướng **1 — Drop hoàn toàn, dùng DB mới**.

**Quy trình an toàn:**
1. Backup SQL trước (safety net): `pg_dump ... > backup_2026-06-18.sql`
2. Drop DB cũ: `DROP DATABASE vietprodev_sso;`
3. Drop DB backup cũ: `DROP DATABASE vietprodev_sso_backup;`
4. Tạo DB mới: `CREATE DATABASE sso;` + `CREATE DATABASE sso_backup;`
5. Chạy `pnpm migrate` (40 files)
6. Tạo admin user qua script trong RUN.md §2.5

**Lý do:**
- Template mới clean hơn (đã drop ở migration 038)
- Schema cũ có thể không tương thích (bcrypt secret, status enum khác, v.v.)
- Tránh technical debt "data lai"
- Nếu cần rollback: restore từ SQL backup

**Out of scope:** Viết migration script convert data cũ → mới. Nếu sau này cần, có thể viết riêng.

### Q4: Database credential ✅
**Decision:**
- Docker: user `sso` / password `sso` (đơn giản, dễ nhớ cho dev)
- File `.env` chính: `DB_USER=postgres` / `DB_PASSWORD=@dmin123` (match user setup, nhưng **cần drop volume cũ trước**)
- Production: secrets sẽ generate random qua `pnpm run setup:prod` (script sẽ viết ở Phase 4)

**Đề xuất:** Phase 0 dùng `sso` / `sso` cho Docker + update `.env` tương ứng. Đơn giản hơn.

### Q5: PostgreSQL version cho Docker ✅
**Decision:** `postgres:17-alpine` cho cả main + backup.
- Lý do: 17 ổn định, tương thích cả data cũ (16) lẫn mới (18), tài liệu nhiều
- Out of scope: dùng 18-alpine (match DB user 18) vì 18 mới ra, ít tài liệu
- Có thể upgrade lên 18 ở Phase 4 nếu cần

### Q6: 18 untracked files cũ (codebase sso-vietprodev-old) ✅
**Decision:** **Xóa hết** sau khi verify an toàn.

**Quy trình:**
1. Diff từng file với stash backup đã lưu (`/tmp/sso-stash-backup/files.txt`)
2. Nếu file không cần → xóa (`git clean -f` cho tracked, manual xóa cho untracked)
3. Stash `@{0}` → giữ (không drop) để backup, hoặc drop sau khi chắc chắn

**Lý do:** Đây là codebase cũ từ `sso-vietprodev-old` — KHÔNG liên quan đến dự án SSO mới. Tất cả logic SSO mới đã có trong template (40 migrations + services + providers).

---

## 5. File Touch List (dự kiến)

| File | Phase 0 | Phase 1 | Phase 2 | Phase 3 | Phase 4 |
|------|---------|---------|---------|---------|---------|
| `.env` | ✅ port fix | — | — | ✅ thêm DB_MAIN/BACKUP | — |
| `docker-compose.yml` | ✅ 2 postgres | — | — | — | — |
| `docker-compose.ha.yml` | — | — | — | ✅ simplified (chỉ giữ backup) | — |
| `src/server.ts` | ✅ fix 2 TS errors | — | — | — | — |
| `src/oidc/oidcService.ts` | — | — | ✅ cookie domain | — | ✅ https redirect |
| `src/oidc/oidcInteractionsController.ts` | — | ✅ wire validateCredentials | ✅ prompt=none | — | — |
| `src/oidc/oidcAdapterService.ts` | — | — | ✅ findSession/upsertSession | — | — |
| `src/contracts/auth/schema.ts` | ✅ thêm 6 schemas | — | — | — | — |
| `src/services/database/multiPoolService.ts` | — | — | — | ✅ autoLoadPools | — |
| `src/audit/auditLoggerService.ts` | — | — | — | ✅ Mongo audit | — |
| `project-a-demo/server.js` | — | — | ✅ silent-login route | — | — |
| `project-b-demo/server.js` | — | — | ✅ silent-login route | — | — |
| `tests/unit/services/auth/validateCredentials.test.ts` | — | ✅ new | — | — | — |
| `tests/integration/silent-sso.test.ts` | — | — | ✅ new | — | — |
| `PROGRESS.md` | ✅ update | ✅ update | ✅ update | ✅ update | ✅ final |
| `RUN.md` | — | — | — | — | ✅ final |
| `PLANS.md` | ✅ update | ✅ update | ✅ update | ✅ update | ✅ final |

**Total estimated lines changed:**
- Phase 0: ~150 lines (env, docker, contracts)
- Phase 1: ~200 lines (controller wire + tests)
- Phase 2: ~400 lines (prompt=none, adapter, project silent-login)
- Phase 3: ~300 lines (MultiPool + audit)
- Phase 4: ~100 lines (CSRF, rate limit, docs)
- **Total: ~1150 lines**

---

## 6. Risks & Mitigations

| Risk | Impact | Mitigation |
|------|--------|------------|
| Pre-existing TS errors có thể là dấu hiệu code chưa sẵn sàng | Medium | Fix ngay Phase 0 trước khi build feature |
| DB cũ `vietprodev_sso` có data user thật | High | Không drop — backup file SQL hoặc giữ volume Docker cũ |
| OIDC session không share được vì cookie domain | High | Plan B: dùng `prompt=none` (ADR-003) |
| 18 file untracked cũ có thể chứa code hữu ích | Low | Diff từng file với stash backup trước khi xóa |
| `MultiPoolService.autoLoadPools` thiếu | Medium | Implement hoặc remove call tùy nhu cầu |
| Stash `@{0}` drop mất data | High | Tag backup trước khi drop, đã có `backup/develop-pre-phase3-restore` |

---

## 7. Timeline (ước lượng)

| Phase | Thời gian | Có thể parallel? |
|-------|-----------|------------------|
| 0 - Cleanup & Foundation | 2-3h | 1 dev |
| 1 - Wire OIDC Login | 4-6h | 1 dev |
| 2 - Silent SSO | 6-8h | 1 dev (cần test kỹ) |
| 3 - HA + Audit | 8-10h | 1 dev (cần test failover) |
| 4 - Production | 4-6h | 1 dev |
| **Total** | **24-33h** | ~3-4 working days |

---

## 8. Status Tracking

| Phase | Status | Started | Completed | Notes |
|-------|--------|---------|-----------|-------|
| 0 | 🟡 Ready to start | — | — | Đã chốt decisions trong §4. Đợi user confirm để bắt đầu. |
| 1 | ⚪ Not started | — | — | Phụ thuộc Phase 0 |
| 2 | ⚪ Not started | — | — | Phụ thuộc Phase 1 |
| 3 | ⚪ Not started | — | — | Phụ thuộc Phase 2 |
| 4 | ⚪ Not started | — | — | Phụ thuộc Phase 3 |

Legend: 🔴 Pending | 🟡 In progress | 🟢 Done | ⚪ Not started

---

## 9. Changelog

- **2026-06-18** — Initial draft (created from full project scan)
- **2026-06-18** — User confirmed key decisions:
  - Q1: 2 instance PostgreSQL từ Phase 0
  - Q2: Cross-project SSO dùng `prompt=none` (ADR-003) + reject subdomain pattern
  - Q3: Drop DB cũ `vietprodev_sso` sau khi backup SQL safety net
  - Q4: Docker user/password = `sso` / `sso` (đơn giản hơn)
  - Q5: PostgreSQL `17-alpine` (ổn định, đủ dùng)
  - Q6: Xóa 18 untracked files cũ sau khi verify an toàn
