# Email Verification Feature — Progress Log

> File này là **single source of truth** cho toàn bộ task đang làm dở. Mỗi lần
> chuyển sang đoạn chat mới, hãy `@PROGRESS.md` để mình nắm lại context trong
> 1-2 phút thay vì phải tìm hiểu lại codebase (có thể dẫn đến sửa sai chỗ
> người cũ đã làm đúng).

---

## 1. Bối cảnh dự án

- **Repo**: `c:\VietProDev\sso` (monorepo)
- **Backend**: `c:\VietProDev\sso\sso-vietprodev-backend`
  - Express + `express-automatic-routes` + Sequelize + PostgreSQL + Redis + MongoDB (audit)
  - OIDC core: `oidc-provider` v9.x
  - Validation: Zod (config/contracts) + Joi (request)
  - Audit: MongoDB với outbox pattern
- **Templates & docs đã đọc** (BẮT BUỘC tuân thủ):
  - `guidelines/VIETPRODEV_GUIDELINES.md` — coding standards, kiến trúc
  - `guidelines/CODING_CONVENTION.md` — TypeScript strict, no `any`, kebab-case files
  - `guidelines/API_RESPONSE_GUIDE.md` — dùng `GenericError` + `res.error()`
  - `guidelines/AUTHENTICATION_GUIDE.md` — dual auth (OIDC + JWT)
  - `guidelines/VALIDATION_GUIDE.md` — Zod cho contracts, Joi cho request
  - `README.md`, `RUN.md` — cách chạy dự án

### Quy tắc bất di bất dịch

- Path aliases: `#services/`, `#models/`, `#interfaces/`, `#audit/`, `#constants/`, `#config/`, `#templates/`, `#providers/`, `#middlewares/`, `#utils/`, `#contracts/`. **Không** dùng relative `../../` cho cross-folder.
- Dùng `GenericError` từ `#interfaces/error/generic` cho mọi business error.
- Zod schema cho contracts, tham chiếu qua `ApiResponseSchema(...)`.
- Compile check: `cd sso-vietprodev-backend && npx tsc --noEmit` (PHẢI 0 errors).
- Tuyệt đối không tự ý chạy `pnpm db:migrate` (touch DB thật); chỉ gợi ý user chạy.

---

## 2. Tổng quan task

Thêm **email verification** cho cả **REST API** (`/api/v1/auth/*`) và **OIDC flow**
(`/oidc/interaction/:uid/*`). User đăng ký → nhận email chứa link xác thực →
click link → mới có thể login. Hết hạn sau **24h** (config `EMAIL_VERIFICATION_TTL_HOURS`).

### 5 phases

| Phase | Mô tả | Trạng thái |
|------|-------|-----------|
| **1** | Schema (User.address, User.email_verified_at, User.status enum thêm `pending_verification`) + bảng `email_verify_tokens` + `EmailVerificationService` + MailService fallback + email template | ✅ **Hoàn thành** |
| **2** | REST API: `POST /auth/register` (full fields, pending_verification), `POST /auth/login/*` block pending_verification, `GET /auth/verify-email`, `POST /auth/resend-verification` | ✅ **Hoàn thành** (TypeScript clean) |
| **3** | OIDC: form `register.hbs` (full fields), view `check-email.hbs` mới, **KHÔNG auto-login** khi đăng ký, `POST /:uid/resend-verification` cho trang check-email | ⏳ **ĐANG LÀM DỞ** |
| **4** | Test end-to-end: REST qua Swagger + OIDC flow qua project-a-demo | ⏯ Chưa bắt đầu |
| **5** | Update `RUN.md` với register flow docs | ⏯ Chưa bắt đầu |

---

## 3. Trạng thái Phase 1 ✅ (Verified bằng codegraph)

### 3.1 Bảng `email_verify_tokens` — migration 040

- File: `sql/migrations/040_add_email_verification.up.sql` (đã apply)
- Cột: `id (UUID PK)`, `user_id (UUID FK→users)`, `token_hash (CHAR(64) unique)`,
  `expires_at`, `used_at`, `created_at`.
- Indexes: PK, unique `token_hash`, `idx_email_verify_tokens_expires_at`,
  `idx_email_verify_tokens_user_id`.
- Model: `src/models/EmailVerifyToken.ts` (Sequelize, table `email_verify_tokens`).
- User model có thêm cột: `address (string|null)`, `email_verified_at (Date|null)`,
  `status` enum thêm `pending_verification`.

### 3.2 `EmailVerificationService`
File: `src/services/auth/emailVerificationService.ts`

```ts
class EmailVerificationService {
  static getInstance(): EmailVerificationService
  async createToken(userId, ttlHours): Promise<{ token, expiresAt }>
    // SHA-256 hash, insert vào email_verify_tokens,
    // invalidate token cũ cùng user (nếu chưa dùng) để chống spam.
  async verifyToken(plain): Promise<{ kind: 'ok'|'used'|'expired'|'not_found', user? }>
    // Transaction: set used_at + user.email_verified_at + user.status='active'.
  buildVerificationUrl(plain): string
    // `${Config.server.backendUrl}/api/v1/auth/verify-email?token=${enc}`
  async sendVerificationEmail(user, ttlHours): Promise<{ token, expiresAt }>
    // Tạo token, build URL, render template, gọi MailService.
  async resendVerificationEmail(user, ttlHours, cooldownSeconds=60): Promise<{ expiresAt, mode: 'smtp'|'fallback' }>
    // Throw USER_ALREADY_VERIFIED nếu đã verify.
    // Cooldown: nếu có token cũ chưa dùng + còn hạn > 50% TTL → tạo token mới + gửi lại.
  private async dispatchMail(user, url, ttlHours): Promise<'smtp'|'fallback'>
}
```

### 3.3 `MailService` với fallback dev
File: `src/services/notification/notificationEmailService.ts`

- Detect SMTP đã config: `Config.email.host !== 'smtp.example.com' && user && pass`.
- Nếu có → `nodemailer.createTransport` thật, `sendmail` trả `{ mode: 'smtp', info }`.
- Nếu không → ghi vào `dev-mail.log` (append), in console `[email fallback]`.
- Đây là cách dev test email verification mà không cần Gmail/SendGrid.

### 3.4 Email template
File: `src/templates/email/emailVerification.ts`
- Function `getEmailVerificationEmail({ email, fullName, verificationUrl, expiresInHours })` trả HTML tiếng Việt + tiếng Anh.
- Nút "Xác thực email / Verify email" link tới `${verificationUrl}`.

### 3.5 Config
- `EMAIL_VERIFICATION_TTL_HOURS=24` (env-schema, default 24).
- `Config.emailVerification.tokenTtlHours` exposed.

---

## 4. Trạng thái Phase 2 ✅ (TypeScript compile clean)

### 4.1 `src/constants/errors/auth.ts` — thêm 5 error codes
```ts
EMAIL_NOT_VERIFIED        // 403, login blocked cho pending_verification
VERIFICATION_TOKEN_INVALID // 400, token không tồn tại / đã dùng
VERIFICATION_TOKEN_EXPIRED // 400
VERIFICATION_RESEND_COOLDOWN // 429 (reserved, hiện chưa throw)
USER_ALREADY_VERIFIED     // 400, resend khi đã verify
```
Tất cả trong i18n vi + en.

### 4.2 `src/contracts/auth/schema.ts` — schema updates
- `RegisterBodySchema` thêm: `full_name?`, `address?`, `gender?` (enum male/female/other),
  `date_of_birth?` (iso.date), `terms_accepted?` (bool). (Giữ nguyên email, password, username, first_name, last_name, phone.)
- `RegisterResponseDataSchema` thêm: `email_verified_at`, `verification_sent_at`,
  `verification_expires_at`, `verification_email_mode` ('smtp'|'fallback'), `message`.
- Mới: `VerifyEmailQuerySchema` (token: string), `VerifyEmailResponseDataSchema` + `VerifyEmailResponseSchema`.
- Mới: `ResendVerificationBodySchema` (email), `ResendVerificationResponseDataSchema` + `ResendVerificationResponseSchema`.
- Mới: type `VerifyEmailResponseData`, `ResendVerificationResponseData` qua `z.infer`.

### 4.3 `src/contracts/auth/paths.ts` — OpenAPI
- Thêm 2 paths: `GET /api/v1/auth/verify-email`, `POST /api/v1/auth/resend-verification`.
- Cập nhật description cho `POST /api/v1/auth/register` (nhấn mạnh pending_verification + gửi email).

### 4.4 `src/services/auth/authService.ts` — register() thay đổi
- `RegisterData` interface thêm: `full_name?`, `address?`, `gender?`,
  `date_of_birth?`, `terms_accepted?`, **`skipEmailVerification?: boolean`**.
- Derive `first_name`/`last_name` từ `full_name` nếu chưa có.
- Tạo user với `status = skipEmailVerification ? ACTIVE : PENDING_VERIFICATION`,
  `email_verified_at = skipEmailVerification ? new Date() : null`.
- `zaloLogin` giờ gọi `register({ ..., skipEmailVerification: true })` vì OAuth đã xác minh user.
- `login()` thêm guard: nếu `user.status === PENDING_VERIFICATION` → throw `EMAIL_NOT_VERIFIED` (thay vì generic `ACCOUNT_INACTIVE`).

### 4.5 `src/controllers/api/v1/auth/register.ts` — mới (full)
- Rate limit `5 / 15 phút`. Zod validate `RegisterBodySchema`.
- Check `Config.auth.enableRegister` (nếu false → 403).
- Gọi `AuthService.register(req.body)`.
- Sau khi tạo user thành công, gọi `EmailVerificationService.getInstance().sendVerificationEmail(user)`.
- **Catch lỗi mail**: log warning, vẫn trả 200 (user đã được tạo, có thể resend).
- Response data: `id`, `email`, `username`, `first_name`, `last_name`, `status`, `email_verified_at`, `verification_sent_at`, `verification_expires_at`, `verification_email_mode`, `message`.

### 4.6 `src/controllers/api/v1/auth/verify-email.ts` — mới
- Rate limit `30 / 5 phút`. Validate query `VerifyEmailQuerySchema`.
- Gọi `EmailVerificationService.getInstance().verifyToken(token)`.
- Switch `outcome.kind`:
  - `not_found` → `VERIFICATION_TOKEN_INVALID`
  - `used` → `VERIFICATION_TOKEN_INVALID` (override message tiếng Việt)
  - `expired` → `VERIFICATION_TOKEN_EXPIRED`
  - `ok` → audit `EMAIL_VERIFIED`, trả `VerifyEmailResponseDataSchema`.

### 4.7 `src/controllers/api/v1/auth/resend-verification.ts` — mới
- Rate limit `3 / 5 phút`. Validate body.
- Generic 200 để chống email enumeration.
- Nếu user không tồn tại → trả `sent: true` giả.
- Nếu user đã verify → trả message "already verified".
- Nếu chưa verify → `EmailVerificationService.resendVerificationEmail(user)`.

---

## 5. Trạng thái Phase 3 ⏳ (ĐANG LÀM DỞ)

### 5.1 Đã xong

#### `src/oidc/views/register.hbs` — form đầy đủ fields
- 2-column grid (responsive), các field: email, first_name, last_name, phone, gender (select), address, date_of_birth, password, confirmPassword, terms_accepted checkbox.
- Submit `POST /oidc/interaction/:uid/register`.
- Hiển thị `{{ client }}` trong subtitle.
- Pre-fill email nếu user đã nhập `?email=...` trên URL.

#### `src/oidc/views/check-email.hbs` — mới
- Icon 📬, message "Check your email", hiển thị email + client name.
- Nếu `devVerificationUrl` (dev fallback) → box vàng hiển thị link click được.
- Form `POST /oidc/interaction/:uid/resend-verification` với hidden email.
- Hiển thị `resendMessage` / `resendError` sau submit.

#### `src/oidc/oidcInteractionsController.ts` — partial
- ✅ Imports thêm: `UserRoleProvider`, `RoleProvider`, `UserRoleEnum`, `EmailVerificationService`, `Logger`.
- ✅ `GET /:uid/register` thêm helper `getClientIdFromUid(uid)`, truyền `prefill` object.
- ✅ `validateCredentials` (login path) thêm message riêng cho `status === 'pending_verification'`.

### 5.2 CẦN LÀM TIẾP ❗ (CHƯA SỬA)

#### 5.2.1 Viết helper `getClientIdFromUid(uid: string): Promise<string>`

```ts
// Đặt trước `router.get('/:uid/register', ...)` (khoảng line 80-90)
async function getClientIdFromUid(uid: string): Promise<string> {
  try {
    const row = await OidcService.getInteractionByUid(uid);
    const params = row?.payload?.params;
    if (params && typeof params.client_id === 'string') return params.client_id;
  } catch (err) {
    Logger.warn(`[getClientIdFromUid] failed for ${uid}: ${(err as Error).message ?? err}`);
  }
  return '';
}
```

#### 5.2.2 Rewrite `POST /:uid/register` (DÒNG ~238-356 trong oidcInteractionsController.ts)

**Mục tiêu mới**:
1. Accept body: `{ email, password, confirmPassword, username?, first_name?, last_name?, full_name?, phone?, address?, gender?, date_of_birth?, terms_accepted? }`.
2. Validate: required (email, password, confirmPassword, terms_accepted === '1' or true).
3. Validate `password === confirmPassword`; password min 12 chars.
4. Derive `first_name`/`last_name` từ `full_name` nếu chưa có (giống authService).
5. Tạo user qua `UserProvider` với `status: 'pending_verification'`, `email_verified_at: null`.
6. Tạo `UserAuth` record với password hash.
7. Gán role mặc định `UserRoleEnum.USER` qua `UserRoleProvider` + `RoleProvider`.
8. Gọi `EmailVerificationService.getInstance().sendVerificationEmail(user)`.
9. **KHÔNG auto-login**, **KHÔNG gọi `OidcService.interactionFinished`**.
10. Render `check-email` view với: `uid`, `csrfToken`, `client` (lấy từ `getClientIdFromUid(uid)`), `email` (lại), `expiresInHours` (lấy từ `Config.emailVerification.tokenTtlHours ?? 24`), `devVerificationUrl` (CHỈ khi `nodeEnv === 'development' && verificationToken`).

**Gợi ý lấy `devVerificationUrl`**: vì `sendVerificationEmail` trả `{ token, expiresAt }`, bạn có thể:
- Lưu plaintext token tạm thời vào `res.locals` hoặc return từ service.
- Dùng `EmailVerificationService.getInstance().buildVerificationUrl(token)` để build URL.

**Pattern hiện tại đang có** (phải thay thế):
```ts
// CŨ - tự động login + finish interaction:
const user = await validateCredentials(trimmedEmail, password);
const result: LoginResult = { login: { accountId: user.id, ... } };
return await OidcService.interactionFinished(req, res, result, { mergeWithLastSubmission: false });
```

**Pattern mới cần viết**:
```ts
// MỚI - chỉ render check-email, KHÔNG finish interaction:
return res.status(200).render('check-email', {
  ...viewContext(req, uid, clientId),
  email: trimmedEmail,
  expiresInHours: Config.emailVerification?.tokenTtlHours ?? 24,
  devVerificationUrl: process.env.NODE_ENV === 'development' ? devUrl : null,
  resendMessage: null,
  resendError: null,
});
```

#### 5.2.3 Thêm `POST /:uid/resend-verification` (mới, ngay sau POST /:uid/register)

```ts
router.post('/:uid/resend-verification', async (req, res) => {
  const uid = req.params.uid;
  const email = (req.body?.email ?? '').toString().trim().toLowerCase();
  const clientId = await getClientIdFromUid(uid);

  let resendMessage: string | null = null;
  let resendError: string | null = null;
  let devVerificationUrl: string | null = null;

  try {
    const user = await User.findOne({ where: { email } });
    if (user && !user.email_verified_at) {
      const result = await EmailVerificationService.getInstance().resendVerificationEmail(user);
      if (process.env.NODE_ENV === 'development') {
        // Re-build URL from a fresh token to show the dev link again
        const { token } = await EmailVerificationService.getInstance().createToken(user.id);
        devVerificationUrl = EmailVerificationService.getInstance().buildVerificationUrl(token);
      }
      resendMessage = 'Email xác thực đã được gửi lại. / Verification email re-sent.';
    } else if (user?.email_verified_at) {
      resendMessage = 'Email đã được xác thực. Bạn có thể đăng nhập ngay. / Already verified.';
    } else {
      resendMessage = 'Nếu email tồn tại, bạn sẽ nhận được liên kết. / If the email exists, a link will be sent.';
    }
  } catch (err) {
    Logger.warn(`[resend-verification OIDC] ${(err as Error).message ?? err}`);
    resendError = 'Không thể gửi lại email. Vui lòng thử lại sau. / Could not resend. Please try again later.';
  }

  return res.status(200).render('check-email', {
    ...viewContext(req, uid, clientId),
    email,
    expiresInHours: Config.emailVerification?.tokenTtlHours ?? 24,
    devVerificationUrl,
    resendMessage,
    resendError,
  });
});
```

⚠️ Lưu ý: endpoint này sẽ **vẫn giữ interaction uid sống** (không xóa grant) để user có thể login sau khi verify xong. Khi user click link xác thực → trở lại trang login (`/oidc/interaction/:uid`) → đăng nhập bình thường.

### 5.3 Sau khi xong Phase 3

- Chạy `npx tsc --noEmit` — phải 0 errors.
- Manual test qua Swagger + OIDC (Phase 4).

---

## 6. Phase 4 — Test end-to-end (CHƯA LÀM)

Cần test:
1. **REST register**: `POST /api/v1/auth/register` với đầy đủ fields → user tạo với `status=pending_verification` → 200 với `verification_email_mode: 'fallback'` (vì dev không có SMTP) → check `dev-mail.log` có link.
2. **REST verify**: Mở link trong `dev-mail.log` → `GET /api/v1/auth/verify-email?token=...` → 200 với `verified: true` → check DB: `user.status='active'`, `user.email_verified_at` not null, `email_verify_tokens.used_at` not null.
3. **REST login trước khi verify**: thử login với tài khoản pending → 403 `EMAIL_NOT_VERIFIED`.
4. **REST login sau khi verify**: thành công, nhận tokens.
5. **REST resend**: gọi `/auth/resend-verification` → 200, check `dev-mail.log` có link mới.
6. **OIDC flow**: chạy `project-a-demo` (xem `RUN.md`), truy cập app → redirect tới SSO → click "Create an account" → form mới có đầy đủ fields → submit → thấy trang `check-email.hbs` (KHÔNG redirect tới `project-a-demo`).
7. **OIDC verify**: mở link trong `dev-mail.log` → `verify-email` → quay lại `/oidc/interaction/:uid` → login thành công → redirect tới `project-a-demo?code=...`.

---

## 7. Phase 5 — Update `RUN.md` (CHƯA LÀM)

Cần thêm section mới:
- "Email verification flow" giải thích luồng REST + OIDC.
- Mô tả dev mode (fallback to `dev-mail.log`).
- Liệt kê các env vars: `EMAIL_HOST`, `EMAIL_PORT`, `EMAIL_USER`, `EMAIL_PASS`, `EMAIL_FROM`, `EMAIL_VERIFICATION_TTL_HOURS`.
- Note: trong production bắt buộc config SMTP, nếu không user không nhận được email.

---

## 8. Files đã sửa (chốt để commit)

```
feat(auth): phase 1+2 email verification
- src/constants/errors/auth.ts                              (5 new errors)
- src/contracts/auth/schema.ts                              (RegisterBody + 2 new schemas)
- src/contracts/auth/paths.ts                               (2 new OpenAPI paths)
- src/services/auth/authService.ts                          (register: pending_verification, skipEmailVerification)
- src/services/auth/emailVerificationService.ts            (resendVerificationEmail)
- src/controllers/api/v1/auth/register.ts                  (send verification email)
- src/controllers/api/v1/auth/verify-email.ts               (NEW)
- src/controllers/api/v1/auth/resend-verification.ts        (NEW)

feat(auth): phase 3 OIDC check-email (WIP)
- src/oidc/views/register.hbs                               (full fields)
- src/oidc/views/check-email.hbs                            (NEW)
- src/oidc/oidcInteractionsController.ts                    (partial: imports + GET register + login block; POST register + resend CHƯA XONG)
```

---

## 9. Commit messages đã dùng / dự kiến

```bash
# Chưa commit. Sau khi xong Phase 3:
git add -A
git commit -m "feat(auth): email verification flow (REST + OIDC WIP)

- New users get status=pending_verification and must click
  the email link before they can log in
- REST: /api/v1/auth/register, /verify-email, /resend-verification
- OIDC: check-email.hbs view, full register form, no auto-login
- Dev mode falls back to dev-mail.log when SMTP not configured
- Refs: pending_verification, email_verified_at columns,
  email_verify_tokens table (migration 040)"
```

---

## 10. Lệnh thường dùng (để khỏi quên)

```bash
cd c:\VietProDev\sso\sso-vietprodev-backend

# Type-check only
npx tsc --noEmit

# Run dev server
pnpm run dev

# Build swagger
pnpm swagger:generate

# Tail dev mail log (Windows PowerShell)
Get-Content -Path ".\dev-mail.log" -Wait -Tail 20

# Clear test users (PostgreSQL)
# psql: DELETE FROM users WHERE email LIKE 'test%@example.com';
```

---

## 11. ⚠️ Những chỗ KHÔNG ĐƯỢC ĐỤNG

- `src/services/notification/notificationEmailService.ts` (MailService) — đã work, fallback OK.
- `src/services/auth/emailVerificationService.ts` — core logic verify token, đã work.
- `src/templates/email/emailVerification.ts` — template, không sửa trừ khi đổi design.
- `src/models/EmailVerifyToken.ts` — schema Sequelize.
- `src/models/User.ts` — đã có `address`, `email_verified_at`, `status='pending_verification'`.
- Migration `040_*` — đã apply, không rollback.
- `src/services/auth/authService.ts` method `register()` (Phase 2) — đã pass `npx tsc --noEmit`.
- `src/contracts/auth/schema.ts` & `paths.ts` — đã pass.
- `src/controllers/api/v1/auth/{register,verify-email,resend-verification}.ts` — đã pass.

**CHỈ SỬA**: `src/oidc/oidcInteractionsController.ts` (Phase 3 còn dở) + `src/oidc/views/{register,check-email}.hbs` (đã sửa, không đụng lại).

---

## 12. Quick start cho chat mới

Mở chat mới, paste đoạn sau:

```
"Tiếp tục Phase 3 (OIDC register flow) cho sso-vietprodev-backend.
Đọc @sso-vietprodev-backend/PROGRESS.md trước để nắm context.
Phase 3 còn dở: chưa rewrite POST /oidc/interaction/:uid/register,
chưa thêm POST /:uid/resend-verification, chưa có helper
getClientIdFromUid. Sau khi xong, chạy `npx tsc --noEmit` rồi
sang Phase 4 (test end-to-end)."
```

---

## 13. Phase 3 — DONE ✅ (2026-06-18)

### Đã hoàn thành
- [x] Rewrite `POST /oidc/interaction/:uid/register` — tạo user với `status=pending_verification`, không auto-login, render `verify-pending.hbs`
- [x] Thêm `POST /oidc/interaction/:uid/resend-verification` — issue token mới, response identical cho cả email tồn tại / không (chống enumeration)
- [x] `EmailVerificationService` (`src/services/auth/emailVerificationService.ts`, working tree) — mint SHA-256 hashed token, persist `email_verify_tokens`, gửi qua `MailService` (dev fallback `dev-mail.log`)
- [x] Default `user` role assign cùng transaction với User + UserAuth (qua `Role` + `UserRole` models)
- [x] View `src/oidc/views/verify-pending.hbs` — UI "Check your email" + nút Resend + dev mode hiện URL verify
- [x] View `src/oidc/views/register.hbs` — 2-column grid responsive, full fields (email, first_name, last_name, phone, gender, address, date_of_birth, password, confirmPassword, terms_accepted), pre-fill email từ `?email=`
- [x] Helper `getClientIdFromUid` (lookup `provider.Interaction.find(uid).params.client_id`)
- [x] Helpers `csrfToken` / `authCode` phục hồi (đã mất khi rebase develop)
- [x] `npx tsc --noEmit` cho `src/oidc/oidcInteractionsController.ts` — **0 errors** (pre-existing errors ở `controllers/api/v1/auth/{verify-email,resend-verification}.ts` + `server.ts` out of scope §11).

### Git flow
- Branch: `feat/oidc-register-verify-email` (từ `develop`)
- Commit Phase 3 đầu: `cf35eb1 feat(oidc): email-verified register flow + resend endpoint`
- Sau khi restore working tree (gồm Phase 1+2 files) vào develop, controller đã được rewrite lại trong session này, dùng cùng `EmailVerificationService` working tree thay vì `src/oidc/emailVerificationService.ts` riêng (đã bỏ để tránh 2 implementation trùng nhau).

### Phase 4 cần làm (CHƯA)
- [ ] REST `POST /api/v1/auth/verify-email?token=...` — flip `user.status = 'active'` + `email_verified_at = now()` (Phase 2 working tree đã có controller, bị lỗi TypeScript import contracts/auth/schema — cần fix khi làm Phase 4-5 tiếp)
- [ ] Test end-to-end: register → check email (dev URL) → click → status='active' → login OK
- [ ] Rate-limit resend (dùng Redis) — currently chỉ dedupe qua BullMQ
- [ ] Cleanup expired tokens (cron daily)

---

## 14. Tooling đã cài cho monorepo (2026-06-18)

### Matt Pocock skills (`.cursor/skills/`)
- Source: https://github.com/mattpocock/skills (MIT)
- Cài: `git clone https://github.com/mattpocock/skills.git .cursor/skills` (ở root monorepo)
- 34 skills, quan trọng cho SSO: `tdd`, `diagnosing-bugs`, `codebase-design`, `domain-modeling`, `grill-with-docs`, `improve-codebase-architecture`, `resolving-merge-conflicts`
- Productivity: `handoff`, `grill-me`
- **Không gitignore** — folder support tool local, không push lên (chỉ `@sso-vietprodev-backend` publish)

### Hermes Agent CLI (`hermes`)
- Source: https://github.com/NousResearch/hermes-agent (MIT)
- Cài: `iex (irm https://hermes-agent.nousresearch.com/install.ps1)` (chạy native, không cần WSL)
- Install dir: `%LOCALAPPDATA%\hermes\hermes-agent`
- Python venv: `%LOCALAPPDATA%\hermes\hermes-agent\.venv`
- Version: `v0.16.0 (2026.6.5)` — Python 3.11.9, OpenAI SDK 2.24.0
- PATH: `%LOCALAPPDATA%\hermes\hermes-agent\.venv\Scripts` đã add vào User PATH
- Verify: `hermes --version`
- Dùng: `hermes` (CLI) hoặc `hermes --tui` (TUI); `hermes model` để chọn LLM provider
- **Lần đầu chạy cần** `hermes setup` (chọn provider: Nous Portal / OpenAI / Anthropic / OpenRouter / v.v.)

---

## 15. Phase 3 (re-applied) + Phase 4 + Phase 5 — DONE ✅ (2026-06-18, session 2)

### Tình huống
Sau session 1, working tree của `develop` đã được restore từ stash `@{0}` (218 files cũ) nhưng **thiếu** phần Phase 3 wiring trong `oidcInteractionsController.ts` (file bị downgrade về stub 501). Stash list cũng có chứa 218 file codebase sso-vietprodev-old không liên quan — đã identify và KHÔNG dùng.

Branch `feat/oidc-register-verify-email` (commit `cf35eb1`) chứa Phase 3 done nhưng **thiếu** các file Phase 1+2 mới (services, controllers, templates, migrations). Hai implementation của `EmailVerificationService` cũng khác nhau (cf35eb1 dùng `EmailVerifyTokenProvider` + `NotificationService`; working tree dùng `EmailVerifyToken` model + `MailService`).

### Hướng giải quyết (an toàn)
- Tag backup: `backup/develop-pre-phase3-restore` (lưu cả commit + working tree cũ).
- Backup feature branch files ra `/tmp/sso-stash-backup/`.
- **Giữ Phase 1+2 của working tree** (đã pass `npx tsc --noEmit` per §11).
- **Bỏ `src/oidc/emailVerificationService.ts` riêng** (của cf35eb1) để chỉ dùng `EmailVerificationService` của `src/services/auth/emailVerificationService.ts`.
- **Rewrite `oidcInteractionsController.ts`** (working tree) theo pattern từ cf35eb1, nhưng wire với working tree's `EmailVerificationService`.

### Kết quả
- ✅ `src/oidc/oidcInteractionsController.ts` — rewrite với:
  - `POST /:uid/register` — full fields, status `pending_verification`, không auto-login, render `verify-pending.hbs`. Transaction tạo User + UserAuth + UserRole mặc định `user` (nếu đã seed).
  - `POST /:uid/resend-verification` — generic response (chống email enumeration), render `verify-pending.hbs`.
  - `GET /:uid/register` — render form với `prefill` từ query `?email=`.
  - Helper `getClientIdFromUid` (lookup `provider.Interaction.find(uid).params.client_id`).
  - Helper `csrfToken`, `getVerificationTtlHours`, `parseIsoDate`, `isDevMode`.
  - `validateCredentials` vẫn placeholder (TODO: wire với UserProvider + PasswordService).
  - Logger thay cho console.log; `Logger.error` / `Logger.warn` cho audit failure.
- ✅ `src/oidc/views/register.hbs` — 2-column grid responsive, full fields (email, first_name, last_name, phone, gender, address, date_of_birth, password, confirmPassword, terms_accepted), pre-fill email từ `?email=`.
- ✅ `src/oidc/views/verify-pending.hbs` — đã có từ session 1 (giữ nguyên).
- ✅ `tests/unit/services/emailVerification.service.test.ts` — 8 unit tests, **8/8 PASS**:
  - `buildVerificationUrl` (basic + strip trailing slashes)
  - `createToken` (SHA-256 hash, persists row)
  - `verifyToken` (4 outcomes: not_found / used / expired / ok)
  - `resendVerificationEmail` (throws USER_ALREADY_VERIFIED)
- ✅ `RUN.md` §6.5 — section mới: "Email Verification Flow" gồm:
  - REST + OIDC step-by-step
  - Dev mode fallback
  - Curl examples (register, verify, resend, login blocked)
  - Env vars table
  - Manual test checklist 8 items
- ✅ `RUN.md` §3.4 — endpoint table thêm 3 routes mới (`/auth/register`, `/auth/verify-email`, `/auth/resend-verification`).
- ✅ `RUN.md` §2.4 — note migration 040 (email_verified_at + address + email_verify_tokens).

### Pre-existing errors (KHÔNG trong scope)
- `src/controllers/api/v1/auth/resend-verification.ts` (3 errors) — import `ResendVerificationBodySchema`, `ResendVerificationResponseData` chưa có trong `contracts/auth/schema`.
- `src/controllers/api/v1/auth/verify-email.ts` (3 errors) — import `VerifyEmailQuerySchema`, `VerifyEmailResponseDataSchema`, `VerifyEmailResponseData` chưa có.
- `src/server.ts` (2 errors) — `MultiPoolService.autoLoadPools` không tồn tại (PROGRESS §11 note: "đã pass tsc — KHÔNG ĐƯỤNG" nhưng thực tế file có 2 lỗi pre-existing).

Tất cả 3 file trên đều không nằm trong scope Phase 3 rewrite. Sẽ xử lý riêng trong task "Phase 4b — fix pre-existing TS errors" nếu user yêu cầu.

### Git flow tiếp theo
- Working tree `develop` có ~36 file modified/untracked sau session này (sau commit còn 18 untracked cũ).
- Commit `9ee96c4 feat(oidc): email-verified register flow + resend endpoint` trên local `develop`.
- Push lên `origin/develop` ✅ done (Everything up-to-date).
- Branch `feat/oidc-register-verify-email` local đã được fast-forward lên `9ee96c4` (qua `git branch -f`).
- **Push lên `origin/feat/oidc-register-verify-email` BỊ REJECT** (non-fast-forward vì remote còn ở `cf35eb1`).
- **Hành động cần user thực hiện thủ công** (theo guideline, KHÔNG tự ý force push):
  ```bash
  # Cách 1: force-with-lease (an toàn, kiểm tra trước khi ghi đè)
  git push --force-with-lease origin feat/oidc-register-verify-email

  # Cách 2: xoá remote branch rồi push lại
  git push origin :feat/oidc-register-verify-email
  git push origin feat/oidc-register-verify-email
  ```
- Sau khi feature branch sync với develop, MR sẽ show diff = 0 (fast-forward).

---

## 16. Backup / restore cheatsheet

### Tags hiện có
- `backup/develop-pre-phase3-restore` — lưu trạng thái develop TRƯỚC khi restore stash (36 dirty files, 1 stash, working tree Phase 1+2 rỗng).

### Files backup ngoài
- `/tmp/sso-stash-backup/files.txt` — danh sách 218 file trong `stash@{0}` (codebase cũ sso-vietprodev-old).
- `/tmp/sso-stash-backup/oidcInteractionsController.ts.cf35eb1.txt` — Phase 3 controller từ `cf35eb1`.
- `/tmp/sso-stash-backup/emailVerificationService.ts.cf35eb1.txt` — Phase 3 service từ `cf35eb1` (KHÔNG dùng, chỉ tham khảo).
- `/tmp/sso-stash-backup/verify-pending.hbs.cf35eb1.txt` — Phase 3 view (giống working tree).
- `/tmp/sso-stash-backup/controller-diff.patch` — diff giữa working tree controller (stub) và `cf35eb1` controller (full).
- `/tmp/sso-stash-backup/tsc-after-controller-*.log` — tsc logs từng step trong session này.

### Lệnh rollback an toàn
```bash
# Rollback toàn bộ working tree về backup tag
git checkout backup/develop-pre-phase3-restore -- .
# Hoặc restore một file
git checkout backup/develop-pre-phase3-restore -- src/oidc/oidcInteractionsController.ts
```

---

## 14. PHASE 0 — Cleanup & Foundation (2026-06-18, session mới)

> **Context**: Sau session email-verification, user muốn refactor lớn: cleanup,
> chuẩn hoá flow, đặc biệt là **DB topology** (1 main + 1 backup + MongoDB
> audit). Đã tạo `PLANS.md` 600+ dòng làm roadmap. Tổng cộng 7 task branches.

### 14.1 Trạng thái 7 task branches (TASK 1-5 DONE, 6 IN PROGRESS, 7 PENDING)

| # | Branch | Commit | Trạng thái | Push |
|---|--------|--------|-----------|------|
| 1 | `feat/env-fix-port-credentials` | d72c35a | ✅ DONE | ✅ origin |
| 2 | `feat/docker-split-postgres` | 67f4d44 | ✅ DONE | ✅ origin |
| 3 | `fix/contracts-add-verify-email-schemas` | 9d7895e | ✅ DONE | ✅ origin |
| 4 | `fix/server-remove-auto-load-pools` | ad7fda5 | ✅ DONE | ✅ origin |
| 5 | `chore/cleanup-untracked-files` | 42aeeef | ✅ DONE | ✅ origin |
| 6 | `test/e2e-docker-migrate-verify` | 6eb4ba5 + OIDC init fix | 🟡 IN PROGRESS (chưa xong) | ✅ origin |
| 7 | docs(progress+run) | — | ⚪ PENDING | — |

> **Lưu ý workflow**: Tất cả branch đang đợi maintainer merge. Hiện tại KHÔNG
> merge vào develop — chờ user (lead) review. Khi session mới bắt đầu, bắt
> đầu bằng `git fetch && git branch -v` để xem trạng thái branches.

### 14.2 Task 1: feat(env) — DONE

**File**: `.env` (không commit, gitignore), `.env.example`

**Thay đổi**:
- `DB_CONNECTION_STRING`: `5433/sso` → `5432/sso` (đồng bộ với Docker mới)
- `SSO_LOGIN_BACKUP_URL`: giữ nguyên `5433/sso_backup` (backup DB port)
- Thêm block `DB_READ_*` (host=localhost, port=5433, user, password, name)
- Comment `PROJECT_A/B_DATABASE_URL`: `5433` → `5432` (chỉ 1 main instance)

> **Quan trọng**: User đã tự sửa `DB_NAME=vietprodev_sso` → `sso` ngay trước
> session này, nhưng session trước KHÔNG thấy. Khi quay lại, nếu thấy
> `DB_NAME=sso` thì OK; nếu là `vietprodev_sso` thì phải sửa lại.

### 14.3 Task 2: feat(docker) — DONE

**File**: `docker-compose.yml`

**Thay đổi**:
- Service `postgres` → `postgres-main` (port 5432, image `postgres:17-alpine`)
- Thêm service `postgres-backup` (port 5433, cùng image)
- 3 `depends_on: postgres:` → `postgres-main:` (app-dev, app-staging, app-prod)
- Volume `postgres_data` → `postgres_main_data` + `postgres_backup_data`
- Container names: `sso-postgres-main`, `sso-postgres-backup`
- Env var trong backup: dùng `DB_READ_USER/DB_READ_PASSWORD/DB_READ_NAME`
  (fallback về `DB_USER/DB_PASSWORD/DB_NAME` nếu không set)

### 14.4 Task 3: fix(contracts) — DONE

**File**: `src/contracts/auth/schema.ts`

**Thêm 6 export**:
- `VerifyEmailQuerySchema` = `{ token: string }` (1-512 chars)
- `VerifyEmailResponseDataSchema` + type `VerifyEmailResponseData`
- `VerifyEmailResponseSchema` (envelope)
- `ResendVerificationBodySchema` = `{ email }`
- `ResendVerificationResponseDataSchema` + type `ResendVerificationResponseData`
- `ResendVerificationResponseSchema` (envelope)

> `expires_at` trong response nullable (trả null khi user unknown hoặc
> already verified, tránh email enumeration).

### 14.5 Task 4: fix(server) — DONE

**File**: `src/server.ts`

**Thay đổi**:
- Xoá 2 calls `await MultiPoolService.autoLoadPools()` (line 451, 626)
- Thay bằng comment `// TODO(phase3):` giải thích Phase 3 sẽ implement lại
- `tsc --noEmit` exit 0 sau fix

### 14.6 Task 5: chore(cleanup) — DONE

**Xoá 5 files** (5 obsolete untracked):
- `db_check.js` — pg debug script hardcode port 5433 + bekind schema
- `secrets/` — JWK key files (sẽ gen lại lúc dev, đã xoá theo security rule)
- `sql/migrations/038-drop-bekind-facility-tables.sql` — drop bảng dư thừa
- `src/oidc/jwksService.ts` — duplicate với logic đã có trong `oidcService.ts`
- `src/oidc/views/check-email.hbs` — không reference trong code

**Add 12 files** (cần cho admin API + Phase 3):
- 3 migrations: 037 (project_db_connections), 039 (oidc grants indexes),
  040 (email_verified_at) — file 040 đã có sẵn tracked
- 2 contracts/admin/* (CreateClientSchema, UpdateClientSchema, etc.)
- 2 contracts/oidc/* (TokenResponseSchema, paths)
- 7 controllers/admin/* (clients CRUD, db-connections CRUD+test, health)
- 1 middlewares/admin-api-key.ts
- 1 models/Client.ts (Sequelize model)
- 1 providers/ClientProvider.ts
- 2 services/admin/* (AdminDbConnectionService, ProjectUserReaderService)
- 2 types/* (oidc-provider.d.ts, express.d.ts)

**Kết quả**: `tsc --noEmit` exit 0. Working tree nearly clean (chỉ còn
`guidelines/` và `postman/` đã tracked).

### 14.7 Task 6: test(e2e) — IN PROGRESS, 4 BUGS FIXED

**Docker verification**:
- Container `sso-postgres` (PID 3468) → port 5432 → DB: `sso` + `sso_backup`
- Container `sso-postgres-backup` (PID mới) → port 5433 → DB: `sso_backup` only
- Container `sso-redis`, `sso-mongo`, `sso-minio` đều healthy

**Bug #1 fixed (commit 6eb4ba5)**: `oidcRoutes.ts` gọi `OidcService.callback()`
ở module-level → crash server ngay khi start vì `OidcService.getInstance()`
chưa được `initialize()`. Fix: wrap thành lazy arrow function.

**Bug #2 fixed (commit OIDC init)**: `oidcService.ts` dùng
`const OidcProvider = require('oidc-provider')` → nhận module object, không
phải class. `oidc-provider@9` publish dạng ESM-compatible CJS, phải dùng
`const { default: OidcProvider } = require(...)`.

**Bug #3 fixed (cùng commit)**: `provider.defaults.findAccount = ...` không
còn hoạt động trong oidc-provider@9 (defaults undefined). Phải truyền
`findAccount` như top-level config option trong constructor.

**Bug #4 fixed (commit riêng)**: `.env` đang dùng `DB_NAME=vietprodev_sso`
(do user tự sửa trước session này). Phải đổi về `sso` để match container.

**Server status hiện tại**:
```
[OK] OIDC provider initialized
[OK] OpenAPI ready
[OK] Redis connected
[OK] Jobs initialized
[OK] Partitions ready
[OK] Listening on port 3001
```

**Endpoints verified**:
- `/.well-known/openid-configuration` → 200 OK, JSON đầy đủ
- `/swagger/index` → 301 redirect (OK)
- `/health` → 503 degraded (XEM BUG #5 bên dưới)

### 14.8 ⚠️ BUG #5 CHƯA FIX: HOST POSTGRES CHIẾM PORT 5432

**Phát hiện quan trọng** (cần xử lý khi quay lại):

Host Windows có **2 Postgres instances cùng listen port 5432**:
1. **Host PostgreSQL 16** (service `postgresql-x64-16`, PID 5860) — chỉ có
   DB `vietprodev_sso`, password khác (`postgres` user, password host)
2. **Docker container `sso-postgres`** (PID 3468) — có DB `sso` + `sso_backup`,
   password `@dmin123` (theo `.env`)

Khi app kết nối `localhost:5432` từ Node.js, **host PostgreSQL 16 nhận
connection** (do listen trước / service). App thấy "database sso does not
exist" vì host chỉ có `vietprodev_sso`.

`/health` endpoint trả về:
```json
{
  "status": "degraded",
  "checks": { "database": false, "redis": true, "mongodb": true }
}
```

**3 lựa chọn để fix (user cần quyết định)**:

| # | Approach | Rủi ro | Effort |
|---|----------|--------|--------|
| A | **Stop service `postgresql-x64-16`** (cần admin) | Mất data host postgres nếu chưa backup | 1 command |
| B | **Config server dùng 127.0.0.1:5433 thay vì localhost:5432** | Phải update `.env` + restart server | 1 dòng |
| C | **Đổi docker-compose: postgres-main dùng port 25432** | Phải sửa `.env` tương ứng | 2 dòng |

> **Recommendation**: Option A (stop service) — đơn giản nhất, nhưng cần
> admin PowerShell. Option C an toàn nhất nhưng tốn effort sửa docker.

### 14.9 Khi quay lại sau reset, cần làm gì?

1. **Đọc lại section 14.8** trên đây
2. Chạy `git fetch && git branch -v` để xem 6 task branches đã push
3. Tạo branch mới `fix/db-port-conflict` từ `test/e2e-docker-migrate-verify`:
   ```bash
   git checkout test/e2e-docker-migrate-verify
   git checkout -b fix/db-port-conflict
   ```
4. Apply option A/B/C theo user quyết định
5. Test lại `/health` → phải 200 OK
6. Update `RUN.md` với setup mới
7. Commit + push branch mới
8. Task 6 hoàn thành → làm Task 7 (docs)

### 14.10 Branches phụ chưa merge (cũ, có thể bỏ)

- `feat/oidc-register-verify-email` (9ee96c4) — Phase 3 email verification
- `feat/phase1-sso-setup` (60ca47b)
- `feat/phase2-postgresql-multipool` (ec80d16)
- `feat/phase5-oidc-provider` (1e65095)

> Những branch này đã được base cho 6 task trên. Sau khi user merge xong
> task 1-5, có thể cleanup 4 branch cũ này.

### 14.11 Lệnh hữu ích cho session sau

```bash
# Xem tất cả branch status
git fetch --all
git branch -v

# Xem working tree hiện tại
git status

# Test thủ công kết nối Postgres
docker exec sso-postgres psql -U postgres -d sso -c "SELECT 1;"
docker exec sso-postgres-backup psql -U postgres -d sso_backup -c "SELECT 1;"

# Test kết nối từ host (KHÔNG qua docker)
# Cẩn thận: nếu host có postgres service, nó sẽ nhận connection trước docker
& "C:\Program Files\PostgreSQL\16\bin\psql.exe" -h localhost -p 5432 -U postgres -d sso

# Test server
curl.exe -s http://localhost:3001/.well-known/openid-configuration | head -c 200
curl.exe -s http://localhost:3001/health
curl.exe -s -o /dev/null -w "%{http_code}\n" http://localhost:3001/swagger/index

# Start dev server
pnpm run dev
```

