# 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.
- Tạo commit `feat(oidc): re-apply Phase 3 wiring on top of develop restore` trên branch `develop`.
- Sau đó fast-forward `feat/oidc-register-verify-email` lên `develop` (hoặc merge ngược để có 1 history thẳng).
- Push lên remote + tạo MR.

---

## 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
```
