# Developer Guide — SSO VietProDev Backend

> **Mục tiêu:** SSO server chạy ở `http://localhost:3001`, đăng ký 2 demo app (project-a-demo, project-b-demo), và test luồng OIDC Authorization Code từ đầu đến cuối.

---

## Table of Contents

1. [Prerequisites](#1-prerequisites)
2. [Quick Start (5 phút)](#2-quick-start)
3. [Environment Variables](#3-environment-variables)
4. [OIDC Clients Registration](#4-oidc-clients-registration)
5. [Demo Apps Setup](#5-demo-apps-setup)
6. [Test End-to-End Flow](#6-test-end-to-end-flow)
7. [Project Structure](#7-project-structure)
8. [Troubleshooting](#8-troubleshooting)

---

## 1. Prerequisites

| Tool | Version | Ghi chú |
|------|---------|---------|
| **Node.js** | >= 20.x | LTS recommended |
| **pnpm** | >= 9.x | Package manager |
| **Docker Desktop** | Latest | Postgres, Redis, MongoDB, MinIO |
| **Git** | Latest | Version control |
| **psql** (tuỳ chọn) | 15+ | Inspect DB thủ công |

> **Windows:** Dùng PowerShell. Tất cả lệnh tương thích PowerShell + bash.

---

## 2. Quick Start

### 2.1. Clone & cài dependencies

```bash
git clone <repo-url> sso-vietprodev
cd sso-vietprodev/sso-vietprodev-backend
pnpm install
```

### 2.2. Tạo file `.env`

```bash
# PowerShell
Copy-Item .env.example .env

# bash
cp .env.example .env
```

> **Dev mode** tự sinh secret mặc định an toàn nếu `JWT_SECRET`, `JWT_REFRESH_SECRET`, `TOKEN_ENCRYPTION_KEY` quá ngắn (< 32 chars).

### 2.3. Khởi động infrastructure

```bash
docker compose up -d postgres redis mongo minio
```

Verify:

```bash
docker compose ps
```

```
NAME                STATUS
sso-postgres        running (healthy)
sso-redis           running (healthy)
sso-mongo           running (healthy)
sso-minio           running (healthy)
```

### 2.4. Chạy migration

```bash
pnpm migrate
```

Script tự động áp dụng các file `.sql` trong `sql/migrations/` theo thứ tự. Migration **038** đã được thực thi — xoá các bảng facility template cũ (residents, buildings, apartments, rooms, beds...). Migration **040** thêm `email_verified_at` + `address` vào `users` và tạo bảng `email_verify_tokens` cho luồng email verification.

### 2.5. Tạo admin user

```bash
docker exec -i sso-postgres psql -U postgres -d sso <<'SQL'
WITH new_user AS (
  INSERT INTO users (id, email, username, first_name, last_name, status, created_at, updated_at)
  VALUES (
    gen_random_uuid(),
    'admin@vietprodev.com',
    'admin',
    'System',
    'Admin',
    'active',
    NOW(),
    NOW()
  )
  RETURNING id
)
INSERT INTO user_auth (id, user_id, password_hash, is_oauth_only, password_changed_at, created_at, updated_at)
SELECT
  gen_random_uuid(),
  id,
  crypt('Vietpro@123', gen_salt('bf', 12)),
  false,
  NOW(),
  NOW(),
  NOW()
FROM new_user;
SQL
```

> **Tài khoản:** `admin@vietprodev.com` / `Vietpro@123`

### 2.6. Khởi động SSO server

```bash
pnpm run dev
```

Output mong đợi:

```
[OK] OIDC provider initialized
[OK] OpenAPI ready
Swagger: http://localhost:3001/swagger/index
[OK] Redis connected
[OK] Jobs initialized
[OK] Partitions ready
[OK] Listening on port 3001
```

### 2.7. Verify

```bash
# Health check
curl http://localhost:3001/health

# OIDC Discovery
curl http://localhost:3001/.well-known/openid-configuration
```

---

## 3. Environment Variables

File `.env.example` chứa 17 sections:

| # | Section | Biến quan trọng |
|---|---------|-----------------|
| 1 | Server | `PORT`, `BACKEND_URL`, `FRONTEND_URL` |
| 2 | Database — Primary | `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME` |
| 2b | Database — Backup/HA | `SSO_LOGIN_BACKUP_URL` |
| 2c | External Project DBs | `PROJECT_A_DATABASE_URL`, `PROJECT_B_DATABASE_URL` |
| 3 | MongoDB | `MONGODB_AUDIT_URL`, `MONGODB_AUDIT_DATABASE` |
| 4 | Redis | `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD` |
| 5 | Auth & Security | `JWT_SECRET`, `JWT_REFRESH_SECRET`, `TOKEN_ENCRYPTION_KEY`, `BCRYPT_ROUNDS` |
| 6 | Admin API | `ADMIN_API_KEY` |
| 7 | OIDC Provider | `OIDC_ISSUER`, `OIDC_ACCESS_TOKEN_TTL`, `OIDC_COOKIE_KEYS`, `OIDC_PRIVATE_JWK_PATH` |
| 8 | Cookies | `COOKIE_DOMAIN`, `COOKIE_CROSS_SITE` |
| 9 | CORS | `CORS_ORIGINS` |
| 10 | Sessions | `SESSION_CACHE_TTL` |
| 11 | Storage | `STORAGE_PROVIDER`, `MINIO_*` |
| 12 | Email | `EMAIL_HOST`, `EMAIL_PORT`, `EMAIL_USER`, `EMAIL_PASS` |
| 13–17 | Social / Notifications / Jobs / Logging | Tuỳ môi trường |

### So sánh `.env` cũ vs mới

| Thành phần | `.env` cũ | `.env` mới |
|------------|-----------|-----------|
| Database | `SSO_LOGIN_PRIMARY_URL` (connection string) | `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME` (individual) |
| Backup DB | `SSO_LOGIN_BACKUP_URL` | `SSO_LOGIN_BACKUP_URL` ✅ giữ nguyên |
| External DBs | `PROJECT_A_DATABASE_URL` / `PROJECT_B_DATABASE_URL` (commented) | Uncomment sau khi tạo project DBs |
| OIDC Config | `OIDC_ISSUER`, `OIDC_ACCESS_TOKEN_TTL`, `OIDC_REFRESH_TOKEN_TTL` | Giữ nguyên + thêm `OIDC_COOKIE_KEYS` |
| JWKS | Không có | `OIDC_PRIVATE_JWK_PATH`, `OIDC_PUBLIC_JWKS_PATH` |
| Cookie | `COOKIE_DOMAIN=.localhost` | Giữ nguyên |
| HA Config | Không có | Thêm `DB_HEALTH_CRON`, `CIRCUIT_BREAKER_*` |
| Notifications | Không có section riêng | Tách thành Queue, Zalo, OneSignal, VAPID riêng |
| Cron Jobs | Không có | `BACKUP_VERIFY_CRON`, `DB_HEALTH_CRON`, `AUDIT_CLEANUP_CRON` |
| Virus Scan | Không có | `VIRUS_SCAN_ENABLED`, `CLAM_HOST`, `CLAM_PORT` |
| Idempotency | Không có | `IDEMPOTENCY_KEY_TTL`, `CIRCUIT_BREAKER_*` |
| Cấu trúc | Flat, không phân section | 17 sections rõ ràng, comment đầy đủ |

### Development vs Production

| Setting | Development | Production |
|---------|-------------|------------|
| `NODE_ENV` | `development` | `staging` / `production` |
| Secrets | Auto-generated if too short | Must set explicitly |
| `ADMIN_API_KEY` | `change-me-admin-api-key` (dev) | Strong random string |
| `DB_HOST` | `localhost` | Internal Docker network name |
| `COOKIE_DOMAIN` | `.localhost` | `.vietprodev.com` |
| `FORCE_SECURE_COOKIES` | `false` | `true` |

---

## 4. OIDC Clients Registration

Cần đăng ký **2 OIDC clients** — một cho mỗi demo app. Sau khi SSO server chạy, gọi API:

### 4.1. Register Project A Demo Client

```bash
curl -X POST http://localhost:3001/admin/clients \
  -H "X-Admin-Api-Key: change-me-admin-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "app_code": "project-a",
    "client_id": "project-a-demo",
    "client_secret": "project-a-demo-secret-123456",
    "name": "Project A Demo",
    "redirect_uris": ["http://localhost:4001/auth/callback"],
    "post_logout_redirect_uris": ["http://localhost:4001"],
    "scopes": ["openid", "profile", "email"],
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "token_endpoint_auth_method": "client_secret_post",
    "require_pkce": false
  }'
```

### 4.2. Register Project B Demo Client

```bash
curl -X POST http://localhost:3001/admin/clients \
  -H "X-Admin-Api-Key: change-me-admin-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "app_code": "project-b",
    "client_id": "project-b-demo",
    "client_secret": "project-b-demo-secret-654321",
    "name": "Project B Demo",
    "redirect_uris": ["http://localhost:4002/auth/callback"],
    "post_logout_redirect_uris": ["http://localhost:4002"],
    "scopes": ["openid", "profile", "email"],
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "token_endpoint_auth_method": "client_secret_post",
    "require_pkce": false
  }'
```

### 4.3. Verify clients registered

```bash
curl http://localhost:3001/admin/clients \
  -H "X-Admin-Api-Key: change-me-admin-api-key"
```

---

## 5. Demo Apps Setup

### 5.1. Project A Demo

```bash
cd c:/VietProDev/sso/project-a-demo
npm install
npm run dev
```

- **URL:** http://localhost:4001
- **Client ID:** `project-a-demo`
- **Client Secret:** `project-a-demo-secret-123456`
- **Redirect URI:** `http://localhost:4001/auth/callback`

### 5.2. Project B Demo

```bash
cd c:/VietProDev/sso/project-b-demo
npm install
npm run dev
```

- **URL:** http://localhost:4002
- **Client ID:** `project-b-demo`
- **Client Secret:** `project-b-demo-secret-654321`
- **Redirect URI:** `http://localhost:4002/auth/callback`

---

## 6. Test End-to-End Flow

### 6.1. OIDC Authorization Code Flow

```
┌─────────────────────────────────────────────────────────────────────┐
│ 1. User clicks "Login with SSO"                                      │
│    GET /auth/login → redirect to SSO                                  │
│                                                                       │
│ 2. SSO shows login page (OIDC interaction)                           │
│    GET http://localhost:3001/oidc/interaction/:uid                   │
│                                                                       │
│ 3. User enters credentials                                           │
│    POST http://localhost:3001/oidc/interaction/:uid/login            │
│                                                                       │
│ 4. SSO shows consent page (first time only)                          │
│    GET http://localhost:3001/oidc/interaction/:uid                    │
│                                                                       │
│ 5. User approves scopes                                              │
│    POST http://localhost:3001/oidc/interaction/:uid/confirm          │
│                                                                       │
│ 6. SSO redirects back with authorization code                        │
│    GET http://localhost:4001/auth/callback?code=XXX&state=YYY        │
│                                                                       │
│ 7. Demo app exchanges code for tokens                                 │
│    POST http://localhost:3001/oauth/token                            │
│                                                                       │
│ 8. Demo app fetches user info                                         │
│    GET http://localhost:3001/oauth/userinfo                          │
│                                                                       │
│ 9. Demo app shows user dashboard                                      │
│    GET /                                                               │
└─────────────────────────────────────────────────────────────────────┘
```

### 6.2. Browser Flow (Recommended)

1. Mở http://localhost:4001 (Project A) hoặc http://localhost:4002 (Project B)
2. Click **Login with SSO**
3. Nhập `admin@vietprodev.com` / `Vietpro@123`
4. Click **Authorize / Consent** (lần đầu)
5. Demo app hiển thị thông tin user

### 6.3. curl Flow (for verification)

**Bước 1 — Authorize:**

```
http://localhost:3001/oauth/authorize
  ?client_id=project-a-demo
  &response_type=code
  &scope=openid%20profile%20email
  &redirect_uri=http%3A%2F%2Flocalhost%3A4001%2Fauth%2Fcallback
  &state=xyz123
```

**Bước 2 — Đổi code lấy token:**

```bash
curl -X POST http://localhost:3001/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=<paste-code-here>" \
  -d "redirect_uri=http://localhost:4001/auth/callback" \
  -d "client_id=project-a-demo" \
  -d "client_secret=project-a-demo-secret-123456"
```

**Bước 3 — Lấy userinfo:**

```bash
curl http://localhost:3001/oauth/userinfo \
  -H "Authorization: Bearer <access_token>"
```

### 6.4. SSO endpoints reference

| Endpoint | Method | Auth | Mô tả |
|----------|--------|------|--------|
| `/.well-known/openid-configuration` | GET | None | OIDC discovery metadata |
| `/oauth/jwks` | GET | None | Public JWKS |
| `/oauth/authorize` | GET/POST | None | Authorization endpoint |
| `/oauth/token` | POST | client_secret | Token endpoint |
| `/oauth/userinfo` | GET | Bearer | User claims |
| `/oauth/introspect` | POST | client_secret | Token introspection |
| `/oauth/revoke` | POST | client_secret | Token revocation |
| `/oauth/logout` | GET/POST | None | Logout endpoint |
| `/oidc/interaction/:uid` | GET | None | Login/consent page |
| `/oidc/interaction/:uid/login` | POST | None | Login submission |
| `/oidc/interaction/:uid/register` | POST | None | Registration |
| `/oidc/interaction/:uid/confirm` | POST | None | Consent approval |
| `/oidc/interaction/:uid/resend-verification` | POST | None | Re-send verification email (OIDC flow) |
| `/api/v1/auth/register` | POST | None | REST: create account + send verification email |
| `/api/v1/auth/verify-email` | GET | None | REST: confirm verification token from email link |
| `/api/v1/auth/resend-verification` | POST | None | REST: re-send verification email |

---

## 6.5. Email Verification Flow

Mọi tài khoản mới (cả REST và OIDC) đều phải **xác thực email** trước khi đăng nhập. User mới có `status = 'pending_verification'` và **không thể login** cho tới khi click link trong email.

### 6.5.1. Các bước (REST flow)

1. `POST /api/v1/auth/register` với body:
   ```json
   {
     "email": "user@example.com",
     "password": "At-least-12-chars",
     "username": "optional",
     "first_name": "...",
     "last_name": "...",
     "phone": "+84...",
     "address": "...",
     "gender": "male|female|other",
     "date_of_birth": "1990-01-15",
     "terms_accepted": true
   }
   ```
2. User được tạo với `status = 'pending_verification'`, `email_verified_at = null`.
3. Một token SHA-256 hash được lưu vào bảng `email_verify_tokens`.
4. Email chứa link `GET /api/v1/auth/verify-email?token=<plain>` được gửi tới user.
5. User click link → token được đánh dấu `used_at`, `user.status` flip sang `active`, `user.email_verified_at = NOW()`.
6. Login thành công qua `/api/v1/auth/login`.

### 6.5.2. Các bước (OIDC flow)

1. Demo app (project-a-demo) → redirect user tới SSO `/oauth/authorize?...`
2. SSO render `/oidc/interaction/:uid` (login page) có nút **Create an account** dẫn tới `/oidc/interaction/:uid/register`.
3. User điền form đầy đủ (email, name, phone, address, gender, DOB, password, terms) → `POST /oidc/interaction/:uid/register`.
4. Backend tạo user (status `pending_verification`), gán role `user` mặc định, gửi email verification, **không auto-login**.
5. SSO render `verify-pending.hbs` — có nút **Resend verification email** + (dev mode) link click trực tiếp.
6. User mở email, click link `GET /api/v1/auth/verify-email?token=...` → flip `active`.
7. User quay lại `/oidc/interaction/<new_uid>` (qua lại authorize) → login bình thường → consent → redirect về demo app.

### 6.5.3. Dev mode fallback

Khi `EMAIL_HOST` chưa được cấu hình (mặc định `smtp.example.com`), `NotificationEmailService` ghi link xác thực vào `dev-mail.log` thay vì gửi thật. Cách test nhanh:

```powershell
# Tail log khi đăng ký
Get-Content -Path ".\dev-mail.log" -Wait -Tail 20
```

Trên trang `verify-pending.hbs` ở dev mode, **một box vàng** hiển thị link xác thực trực tiếp — không cần mở file log.

### 6.5.4. Curl examples

**Register (REST):**
```bash
curl -X POST http://localhost:3001/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "password": "MyStrongP@ssw0rd-12",
    "first_name": "Test",
    "last_name": "User",
    "phone": "+84909090909",
    "address": "Hanoi, Vietnam",
    "gender": "male",
    "date_of_birth": "1990-01-15",
    "terms_accepted": true
  }'
```

**Verify (REST) — lấy token từ `dev-mail.log`:**
```bash
curl "http://localhost:3001/api/v1/auth/verify-email?token=<paste-token-here>"
```

**Resend (REST):**
```bash
curl -X POST http://localhost:3001/api/v1/auth/resend-verification \
  -H "Content-Type: application/json" \
  -d '{ "email": "test@example.com" }'
```

**Login trước khi verify** → response `403 EMAIL_NOT_VERIFIED`:
```bash
curl -X POST http://localhost:3001/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{ "email": "test@example.com", "password": "MyStrongP@ssw0rd-12" }'
```

**Login sau khi verify** → 200 + tokens.

### 6.5.5. Env vars liên quan

| Var | Default | Mô tả |
|-----|---------|--------|
| `EMAIL_HOST` | `smtp.example.com` | SMTP server. Nếu để default, hệ thống rơi vào dev mode (ghi `dev-mail.log`). |
| `EMAIL_PORT` | `587` | SMTP port. |
| `EMAIL_USER` | `""` | SMTP username. |
| `EMAIL_PASS` | `""` | SMTP password. |
| `EMAIL_FROM` | `noreply@yourdomain.com` | From address. |
| `EMAIL_VERIFICATION_TTL_HOURS` | `24` | TTL của verification token (giờ). |
| `BACKEND_URL` | `http://localhost:3001` | Base URL dùng để build link verify trong email. |

> **Production:** **BẮT BUỘC** cấu hình SMTP (`EMAIL_HOST/PORT/USER/PASS`). Nếu không, user sẽ không nhận được email và sẽ bị kẹt ở `pending_verification`.

### 6.5.6. Test checklist thủ công

Sau khi `pnpm run dev` + chạy migration (đã bao gồm migration `040_add-email-verified-at-and-address`):

- [ ] **REST register** — POST `/api/v1/auth/register` đầy đủ fields → 200 với `verification_email_mode: 'fallback'`. Check `dev-mail.log` có link.
- [ ] **REST verify** — Click link từ log → 200 với `verified: true`. Check DB: `user.status='active'`, `email_verified_at` không null, `email_verify_tokens.used_at` không null.
- [ ] **REST login trước khi verify** — tạo user mới, login ngay → 403 `EMAIL_NOT_VERIFIED`.
- [ ] **REST login sau khi verify** → 200 + tokens.
- [ ] **REST resend** — POST `/auth/resend-verification` → 200, check log có link mới.
- [ ] **OIDC flow** — mở project-a-demo → "Create an account" → form đầy đủ → submit → thấy `verify-pending.hbs` (không redirect tới demo).
- [ ] **OIDC verify** — mở link trong log → verify → quay lại `/oidc/interaction/<new_uid>` → login thành công → redirect về demo `?code=...`.
- [ ] **OIDC resend** — từ trang verify-pending, click "Resend" → token mới trong log.

---

## 7. Project Structure

```
sso-vietprodev/
├── sso-vietprodev-backend/      # SSO Authorization Server
│   ├── src/
│   │   ├── controllers/         # Route handlers (admin/ + api/v1/)
│   │   ├── oidc/                # OIDC Provider
│   │   │   ├── oidcService.ts   # Provider config, findAccount
│   │   │   ├── oidcAdapterService.ts  # Postgres adapter
│   │   │   ├── oidcRoutes.ts    # /oauth/* routes
│   │   │   └── oidcInteractionsController.ts  # login/register/consent
│   │   ├── providers/            # BaseProvider wrappers
│   │   ├── services/             # Business logic
│   │   └── middlewares/          # Auth, validators
│   ├── sql/
│   │   ├── migrations/           # 001-038 (SSO core + facility cleanup)
│   │   └── seeds/                # 100-103 (roles, users, residents)
│   ├── .env / .env.example
│   └── docker-compose.yml
├── project-a-demo/               # Demo app A (port 4001)
│   └── server.js
└── project-b-demo/               # Demo app B (port 4002)
    └── server.js
```

---

## 8. Troubleshooting

### OIDC login redirect loop (FIXED)

**Triệu chứng:** Sau khi POST `/oidc/interaction/:uid/login`, browser redirect về `/oauth/authorize?...` rồi lại redirect về `/oidc/interaction/<new_uid>` (login page). Lặp vô hạn.

**Nguyên nhân:** oidc-provider set cookie `_interaction` với `path=/oidc/interaction/:uid` và `_interaction_resume` với `path=/oauth/authorize/:uid`. Khi browser redirect sang `/oauth/authorize?...` (không có UID trong path), cookie `_interaction_resume` không được gửi → oidc-provider tạo session mới → loop.

**Fix:** Thay vì redirect tới `/oauth/authorize?...`, controller `oidcInteractionsController.ts` redirect tới `/oauth/authorize/:uid?...` (oidc-provider's resume route), browser gửi đúng cookie và resume interaction thành công.

### Token exchange returns `invalid_client`

**Nguyên nhân:** oidc-provider so sánh `clientSecret` plaintext với giá trị client gửi lên. Nếu DB lưu bcrypt hash thì compare luôn fail.

**Fix:** Lưu `client_secret_hash` ở dạng **plaintext** (oidc-provider dùng `constantEquals` để so sánh, không hỗ trợ bcrypt). Hoặc override `compareClientSecret` trong client model.

Để cập nhật secret:
```sql
UPDATE clients SET client_secret_hash = 'project-a-demo-secret-123456' WHERE client_id = 'project-a-demo';
UPDATE clients SET client_secret_hash = 'project-b-demo-secret-123456' WHERE client_id = 'project-b-demo';
```

### Server không start

**Port 3001 đã dùng:**
```bash
netstat -ano | findstr :3001
taskkill /F /PID <pid>
```

**OIDC not initialized:**
```bash
docker compose ps postgres   # Kiểm tra Postgres healthy
pnpm migrate                 # Chạy lại migration
pnpm run dev                 # Restart
```

### OIDC Client lỗi

**`invalid_client`:**
- Sai `client_id` hoặc `client_secret`
- Client `status != 'active'`

**`redirect_uri_mismatch`:**
- `redirect_uri` trong request phải **khớp chính xác** giá trị đã đăng ký

**`invalid_grant` (expired code):**
- Authorization code hết hạn sau ~60 giây. Exchange ngay sau khi nhận redirect.

### Demo app không nhận user info

1. Kiểm tra `access_token` còn hạn (15 phút mặc định)
2. Refresh token: POST `/oauth/token` với `grant_type=refresh_token`
3. Xem console log ở demo app terminal

### Database clean

```bash
# Reset hoàn toàn (MẤT HẾT DATA)
docker compose down postgres
docker volume rm sso-vietprodev-backend_postgres_data
docker compose up -d postgres
pnpm migrate
# Sau đó tạo lại admin user và clients
```

---

## Available Scripts

| Command | Description |
|---------|-------------|
| `pnpm run dev` | Dev server hot reload |
| `pnpm run build` | Build TypeScript → `dist/` |
| `pnpm run start` | Run production build |
| `pnpm migrate` | Apply all migrations |
| `pnpm seed` | Run seeds |
| `pnpm db:setup` | migrate + seed |
| `pnpm docker:dev:detach` | Full stack in Docker |
| `pnpm docker:stop` | Stop all containers |
