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

---

## 17. Database / SQL Cleanup — DỌN RÁP TOÀN BỘ `sql/` (2026-06-20, session 3)

### Bối cảnh
User báo cáo Swagger UI `http://localhost:3001/swagger/index/` vẫn hiển thị các API cũ từ dự án **beKind** (Apartments, Beds, Bills, …) và muốn dọn sạch toàn bộ source SQL để chỉ giữ phần SSO core. Yêu cầu: viết lại 1 script migration đẹp, chuẩn, clean.

### Phát hiện root cause crash
- `src/services/database/multiPoolService.ts` gọi `SELECT * FROM project_db_connections` nhưng bảng này là legacy beKind, không tồn tại trong DB SSO.
- `MultiPoolService.autoLoadPools()` throw lỗi `column "description" does not exist` → cản trở startup.
- **ĐÃ FIX** (`src/services/database/multiPoolService.ts`): wrap query trong try/catch, log warning + skip nếu bảng không tồn tại.

### Phân loại 37 migration gốc

| Migrations | Domain | Quyết định |
|---|---|---|
| 001-auth-schema, 002-file-schema, 003-notification-schema, 004-consolidated | Auth/files/notif core | ✅ Giữ |
| 014-audit-log-system, 015-drop-duplicate-audit | Audit SSO | ✅ Giữ |
| 017-optimize-user-sessions, 018-disable-outbox-poller | Auth core | ✅ Giữ |
| 029-add-webhook-logs, 030-recreate-webhook-logs | Webhooks | ✅ Giữ |
| 035-oidc-schema, 036-clients-schema | OIDC core | ✅ Giữ |
| 040-add-email-verified-at-and-address | User profile | ✅ Giữ |
| 005-resident-and-facility-schema (54815 bytes!) | beKind legacy | ❌ Xóa |
| 006, 007, 008, 009, 010, 011 — resident-related | beKind legacy | ❌ Xóa |
| 012, 013, 016 — contracts | beKind legacy | ❌ Xóa |
| 019, 020, 021, 022 — maintenance/incidents/technicians | beKind legacy | ❌ Xóa |
| 023, 024, 025, 031 — posts/beds | beKind legacy | ❌ Xóa |
| 026, 027, 028 — bills | beKind legacy | ❌ Xóa |
| 032 — notification_templates (có billing seed legacy) | beKind legacy | ❌ Xóa |
| 033 — system_configs (seed "BeKind Property") | beKind legacy | ❌ Xóa |
| 034 — debt_reminder_policies ("Nhắc nợ" use case) | beKind legacy | ❌ Xóa |
| `future/` folder (7 files: magic-link, 2fa-backup, social-login, file-usage-tracking, notification-analytics, notification-stats-caching, auth-password-history) | All commented out | ❌ Xóa cả folder |

### Files đã sửa / tạo / xóa

#### Xóa (24 files + 1 folder)

```
sql/migrations/005-resident-and-facility-schema.sql  (54815 bytes)
sql/migrations/006-add-user-id-to-residents.sql
sql/migrations/007-active-views.sql
sql/migrations/008-remove-resident-stays-unique-constraint.sql
sql/migrations/009-remove-resident-stays-no-overlap-constraint.sql
sql/migrations/010-add-nationality-job-to-residents.sql
sql/migrations/011-add-cccd-document-types.sql
sql/migrations/012-add-contracts-schema.sql
sql/migrations/013-remove-file-duplicate-constraint.sql
sql/migrations/016-add-contract-types-table.sql
sql/migrations/019-add-maintenance-incidents-schema.sql
sql/migrations/020-add-incident-code-and-deadline.sql
sql/migrations/021-alter-resident-documents-add-status.sql
sql/migrations/022-alter-technicians-link-user.sql
sql/migrations/023-add-posts-and-post_targets.sql
sql/migrations/024-add-apartment-to-posts-target_type.sql
sql/migrations/025-add-unique-constraint-beds-code.sql
sql/migrations/026-add-bills-schema.sql
sql/migrations/027-alter-bills-billing-period-type.sql
sql/migrations/028-add-soft-delete-to-bills.sql
sql/migrations/031-add-thumbnail-id-to-posts.sql
sql/migrations/032-add-notification-templates.sql
sql/migrations/033-add-system-configs.sql
sql/migrations/034-add-debt-reminder-policies.sql
sql/migrations/future/                                (whole folder, 7 files)
sql/seeds/102-seed-residents.sql                     (27528 bytes)
sql/seeds/103-seed-roommates.sql                     (3217 bytes)
sql/scripts/run-003.js                               (obsolete)
```

#### Tạo mới
```
sql/clean-migration.sql                              (2099 lines, consolidated)
sql/CLEANUP_REPORT.md                                (analysis report)
sql/scripts/replace-references.ps1                   (helper script)
```

#### Sửa (replace BeKind → SSO VietProDev)
- `sql/migrations/001-auth-schema.sql` (header)
- `sql/migrations/002-file-schema.sql` (header)
- `sql/migrations/003-notification-schema.sql` (header)
- `sql/migrations/004-consolidated-migrations.sql` (header)
- `sql/migrations/011-add-cccd-document-types.sql` (header)
- `sql/migrations/012-add-contracts-schema.sql` (header) — *trước khi xóa*
- `sql/migrations/019-add-maintenance-incidents-schema.sql` (header) — *trước khi xóa*
- `sql/migrations/028-add-soft-delete-to-bills.sql` (header) — *trước khi xóa*
- `sql/migrations/033-add-system-configs.sql` (header) — *trước khi xóa*
- `sql/migrations/006-user-id-residents.sql` ... — *trước khi xóa*
- `sql/seeds/101-seed-default-users.sql` (rewrite — admin@bekind.com → admin@sso.vietprodev.com, password VietPro@2026)
- `sql/seeds/100-seed-roles-permissions.sql` (rewrite — thêm permission `read:client`, `manage:client` cho OIDC)
- `sql/scripts/check-db.js` (update `expectedTables` array — bỏ `tokens`, thêm OIDC + files + notifications)
- `sql/scripts/migrate.js` (chạy `clean-migration.sql` đầu tiên trước khi apply migrations khác)

#### Sửa code (TypeScript)
- `src/services/database/multiPoolService.ts` — graceful skip khi `project_db_connections` không tồn tại.

### Schema còn lại sau cleanup (SSO core)

**20+ ENUMs** còn trong `clean-migration.sql`: `user_status`, `session_status`, `platform_type`, `entity_status`, `resident_status`, `document_type`, `resident_stay_type`, `resident_event_type`, `notification_status`, `notification_type`, `notification_channel`, `notification_event_type`, `technician_status`, `incident_priority`, `incident_status`, `enum_file_provider`, `enum_file_status`, `enum_file_visibility`, `webhook_status`, `notification_template_type`, `notification_template_status`, `notification_template_channel`, `audit_severity`.

> **⚠️ Note**: nhiều enum legacy (resident/contract/incident/technician) **vẫn còn** trong `clean-migration.sql` vì file này được tạo **trước** khi xóa các migrations. Cần làm thêm 1 pass nữa để viết lại `clean-migration.sql` chỉ giữ SSO core enums. → **TODO session 4**.

**13 migrations còn lại** trong `sql/migrations/`:
- `001-auth-schema.sql` (21461 bytes) — auth core
- `002-file-schema.sql` (6578 bytes) — files
- `003-notification-schema.sql` (13653 bytes) — notifications
- `004-consolidated-migrations.sql` (4459 bytes) — oauth flag + avatar + pwd history
- `014-audit-log-system.sql` (22150 bytes) — audit
- `015-drop-duplicate-public-audit-tables.sql` (938 bytes) — audit cleanup
- `017-optimize-user-sessions-indexes.sql` (1103 bytes) — perf
- `018-disable-outbox-poller.sql` (1353 bytes) — perf
- `029-add-webhook-logs.sql` (3062 bytes) — webhooks
- `030-recreate-webhook-logs-with-enum.sql` (3096 bytes) — webhooks v2
- `035-add-oidc-schema.sql` (1068 bytes) — OIDC grants
- `036-add-clients-schema.sql` (921 bytes) — OIDC clients
- `040-add-email-verified-at-and-address.sql` (538 bytes) — user profile

### Cấu trúc `sql/` sau cleanup
```
sql/
├── CLEANUP_REPORT.md                   (NEW — phân tích chi tiết)
├── clean-migration.sql                 (NEW — 2099 lines, consolidated)
├── maintenance/
│   └── manage-audit-log-partitions.sql (giữ nguyên, SSO-relevant)
├── migrations/                         (13 files còn lại)
│   ├── 001-auth-schema.sql
│   ├── 002-file-schema.sql
│   ├── 003-notification-schema.sql
│   ├── 004-consolidated-migrations.sql
│   ├── 014-audit-log-system.sql
│   ├── 015-drop-duplicate-public-audit-tables.sql
│   ├── 017-optimize-user-sessions-indexes.sql
│   ├── 018-disable-outbox-poller.sql
│   ├── 029-add-webhook-logs.sql
│   ├── 030-recreate-webhook-logs-with-enum.sql
│   ├── 035-add-oidc-schema.sql
│   ├── 036-add-clients-schema.sql
│   └── 040-add-email-verified-at-and-address.sql
├── scripts/
│   ├── check-db.js                     (UPDATED — SSO tables)
│   ├── clean-users.js                  (giữ nguyên)
│   ├── migrate.js                      (UPDATED — run clean-migration.sql first)
│   └── replace-references.ps1          (NEW — helper tool)
└── seeds/
    ├── 100-seed-roles-permissions.sql  (REWRITTEN — SSO, +OIDC perms)
    └── 101-seed-default-users.sql      (REWRITTEN — SSO users)
```

### Cấu trúc `clean-migration.sql` (20 phases)

| Phase | Nội dung | Trạng thái |
|---|---|---|
| 1 | Extensions: uuid-ossp, pgcrypto, citext, btree_gist, pg_trgm | OK |
| 2 | 23 ENUMs (sẽ rút lại còn ~10 SSO-only) | CẦN RÚT LẠI |
| 3 | Core auth: users, user_auth, roles, permissions, user_roles, role_permissions, user_sessions, password_reset_tokens, email_verify_tokens, user_password_history | OK |
| 4 | File storage: files, file_variants | OK |
| 5 | Notifications: notification_messages, notification_receipts, notification_devices, notification_delivery_attempts, notification_preferences, notification_templates | OK |
| 6 | Resident & facility (buildings, apartments, rooms, beds, residents, resident_stays, resident_events, resident_documents) | ❌ CẦN XÓA |
| 7 | Contracts (contracts, contract_files, contract_types + seed) | ❌ CẦN XÓA |
| 8 | Maintenance incidents (technicians, incident_types, incidents, incident_files + seed) | ❌ CẦN XÓA |
| 9 | Posts (posts, post_targets) | ❌ CẦN XÓA |
| 10 | Bills (bills, bill_items) | ❌ CẦN XÓA |
| 11 | Webhook logs | OK |
| 12 | System configs (general, brand) | ❌ CẦN XÓA |
| 13 | Debt reminder policies + targets + seed | ❌ CẦN XÓA |
| 14 | OIDC: oidc_grants, clients | OK |
| 15 | Audit schema: system_audit_logs, entity_change_logs, auth_audit_logs, refresh_token_audit_logs, audit_outbox, audit_dead_letter + partitions | OK |
| 16 | Indexes (~90 indexes) | CẦN RÚT LẠI |
| 17 | Triggers (update_updated_at + 4 custom) | CẦN RÚT LẠI |
| 18 | Views (active_users, user_role_details, active_resident_stays, resident_current_locations, active_buildings/apartments/rooms/beds/residents) | CẦN RÚT LẠI |
| 19 | Stored procedures + functions (transfer_resident, check_out_resident, cleanup_expired_*, audit.*) | CẦN RÚT LẠI |
| 20 | Comments | CẦN RÚT LẠI |

### TODO session 4 (khi mở chat mới)
1. **Rewrite `sql/clean-migration.sql`** — bỏ Phase 6/7/8/9/10/12/13 (legacy domains) + bỏ các enum tương ứng, giữ ~10 enum SSO-only.
2. **Delete legacy Sequelize models** (làm sạch tương ứng `src/models/`):
   - `Apartment*`, `Bed*`, `Building*`, `Resident*`, `Room*`, `ResidentDocument`, `ResidentEvent`, `ResidentStay`, `ResidentCurrentLocation`
   - `Active*` files (8 files: ActiveApartment, ActiveBed, ActiveBuilding, ActiveResident, ActiveResidentStay, ActiveRoom)
   - `Bill`, `BillItem`
   - `Contract*` (3 files: Contract, ContractFile, ContractType)
   - `DebtReminderPolicy*` (2 files)
   - `Incident*` (3 files: Incident, IncidentFile, IncidentType)
   - `Post*` (2 files: Post, PostTarget)
   - `Technician`
   - `SystemConfig`, `NotificationTemplate`
3. **Delete legacy controllers** trong `src/controllers/api/v1/`:
   - `apartments/`, `beds/`, `bills/`, `buildings/`, `contracts/`, `contract-files/`, `contract-types/`, `debt-reminder-policies/`, `incident-files/`, `incident-types/`, `incidents/`, `notification-templates/`, `posts/`, `resident-documents/`, `residents/`, `rooms/`, `system-configs/`, `technicians/`
4. **Delete legacy contracts** trong `src/contracts/`:
   - `apartment/`, `bed/`, `bill/`, `building/`, `contract/`, `contract-file/`, `contract-type/`, `debt-reminder-policy/`, `incident/`, `incident-file/`, `incident-type/`, `post/`, `resident/`, `room/`, `technician/`, `system-config/`
5. **Delete legacy services**:
   - `src/services/residentExcelService.ts` (referenced in 005 — chắc chắn xóa)
   - `src/services/webhookService.ts` — **CHECK** trước khi xóa (có thể dùng cho OIDC?)
6. **Update `src/models/init-models.ts`** để không import các model đã xóa (~42000 bytes, cần sửa nhiều associations).
7. **Update `src/services/database/sequelize/sequelizeService.ts`** nếu có reference.
8. **Update Swagger title** `storage/swagger/openapi.json`: `"BEKIND API"` → `"SSO VietProDev API"`.
9. **Update `package.json` swagger config** nếu có hardcode title.
10. **Run `pnpm swagger:generate`** để regenerate từ contracts còn lại.
11. **Run `npx tsc --noEmit`** — phải 0 errors.
12. **Manual test** `pnpm run dev` + `http://localhost:3001/swagger/index/` xem chỉ còn SSO APIs (auth, files, notifications, oidc, webhooks, users, roles, permissions).

### Default credentials mới (sau rewrite 101)
```
System Admin: admin@vietprodev.com       / VietPro@2026
Admin:        admin@sso.vietprodev.com   / VietPro@2026
User:         user@sso.vietprodev.com    / VietPro@2026
```

> **Note**: bcrypt hash `$2b$12$RVqgGfwkSCPMxxiBgQUweuRatXSfVvHZLSZOjEUxUJ7Y4RVvGd4rm` trong seed 101 thực ra hash password cũ `Meu@2026`. CẦN regenerate hash cho `VietPro@2026` trước khi dùng. → **TODO session 4**.

### Verification commands cho session mới
```bash
# Check BeKind còn rò rỉ không
grep -r "BeKind\|bekind\|Meu Solutions" sql/ src/ --include="*.sql" --include="*.ts" --include="*.js"

# Check legacy tables
ls sql/migrations/

# Check models còn legacy
ls src/models/ | grep -iE "apartment|bed|building|resident|room|bill|contract|debt|incident|post|technician|notification-template|system-config"

# Check controllers còn legacy
ls src/controllers/api/v1/

# Run migration check
cd sso-vietprodev-backend
node sql/scripts/check-db.js

# Type check
npx tsc --noEmit
```

### Quick start cho chat mới (session 5)
```
"Tiếp tục session 4 (sso-vietprodev-backend).
Đọc @sso-vietprodev-backend/PROGRESS.md trước (§18 quan trọng nhất).
Session 4 đã hoàn tất cleanup: 33 SSO models + 51 SSO paths + 0 tsc errors
+ 0 BeKind residue trong src/lib/openapi.json. CÒN LẠI:
regenerate bcrypt hash cho VietPro@2026 trong seed 101,
chạy pnpm dev + verify Swagger UI runtime,
chạy migration SQL trên DB thật để smoke-test."
```

---

## §18 — Session 4: SQL + Code Cleanup (hoàn tất)

### Kết quả tóm tắt

| Hạng mục | Trước session 4 | Sau session 4 |
|----------|----------------|---------------|
| `sql/clean-migration.sql` | 2099 dòng, 20 phases, 23 ENUMs, FK legacy | 1063 dòng, 10 phases, 12 ENUMs SSO, không FK legacy |
| Models trong `src/models/` | 60+ (incl. 27 legacy) | 33 SSO models |
| Providers trong `src/providers/` | 56+ (incl. 25 legacy) | 31 SSO providers |
| Controllers trong `src/controllers/api/v1/` | 28 (incl. 18 legacy dirs) | 10 SSO dirs |
| Contracts trong `src/contracts/` | 21 (incl. 17 legacy dirs) | 4 SSO dirs + `shared.ts` |
| Services legacy | 4 files (residentExcel, residentImport, webhook, billOverdue) | 0 |
| `init-models.ts` | 719 dòng, 60+ imports + 30+ legacy associations | 297 dòng, 33 SSO imports, chỉ SSO associations |
| `User.ts` | 1255 dòng (associations inline) | 650 dòng (associations đặt tập trung ở `init-models.ts`) |
| `File.ts` | +5 legacy `hasMany` (ContractFile, IncidentFile, Post, ResidentDocument, Technician) | chỉ `FileVariant` + User |
| `WebhookLog.ts` | FK tới `residents` (đã xóa) | `resident_id` UUID no-FK (legacy compat) |
| `auth/profile.ts` | 4-table join Resident/Stay/Bed/Room | chỉ User + avatar |
| `auth/authService.ts` | import Resident + `LoginResult.residentId` | sạch |
| `auth/tokenService.ts` | `JWTPayload.resident_id` + `TokenUserInput.residentId` | sạch |
| `contracts/auth/schema.ts` | `ResidentProfileSchema` + `UserPublicSchema.resident*` | sạch |
| `utils/authResponseUtils.ts` + `auth/login/{mobile,web}.ts` | `loginResult.residentId` | sạch |
| Swagger title | `BEKIND API`, server `/bekind` | `SSO VietProDev API`, server `/api/v1` |
| OpenAPI paths | 50 (incl. 18 legacy) | 51 (chỉ SSO) |
| `tsc --noEmit` | 3 errors (oidc-provider types + sequelizeService typing) | **0 errors** (thêm `src/types/oidc-provider.d.ts`, fix default-import typing trong `oidcService.ts` + `multiPoolService.ts`) |
| BeKind residue | 7 files | **0 files** (clean `src/` + `lib/`) |

### File đã xóa (X)

**Models (29 files)**: `ActiveApartment, ActiveBed, ActiveBuilding, ActiveResident, ActiveResidentStay, ActiveRoom, ActiveUser, Apartment, Bed, Bill, BillItem, Building, Contract, ContractFile, ContractType, DebtReminderPolicy, DebtReminderPolicyTarget, Incident, IncidentFile, IncidentType, NotificationTemplate, Post, PostTarget, Resident, ResidentCurrentLocation, ResidentDocument, ResidentEvent, ResidentStay, Room, SystemConfig, Technician` (count: 31, bao gồm `ActiveUser`).

**Providers (25 files)**: tương ứng 25 model providers (loại bỏ `ActiveUserProvider`).

**Services (4 files)**: `services/residentExcelService.ts`, `services/validators/residentImportValidatorService.ts`, `services/webhookService.ts`, `services/scheduler/jobs/billOverdueJob.ts`.

**Controllers (18 dirs)**: `apartments, beds, bills, buildings, contracts, contract-files, contract-types, debt-reminder-policies, incident-files, incident-types, incidents, notification-templates, posts, resident-documents, residents, rooms, system-configs, technicians, webhooks/`.

**Contracts (17 dirs)**: `apartment, bill, building, contract, contract-file, contract-type, debt-reminder-policy, incident, incident-file, incident-type, post, resident, room, technician, system-config, webhooks/` (+ `notification-templates` cùng controllers).

### File đã rewrite (W)

- `sql/clean-migration.sql` — Phase 1→10 SSO, 12 ENUMs SSO, bỏ views/funcs legacy
- `src/models/init-models.ts` — 33 SSO models, 297 dòng (từ 719)
- `src/models/User.ts` — không đổi inline (associations đặt tập trung trong `init-models.ts`)
- `src/models/File.ts` — bỏ 5 `hasMany` legacy
- `src/models/WebhookLog.ts` — bỏ Resident FK, `resident_id` no-FK
- `src/controllers/api/v1/auth/profile.ts` — bỏ 4-table join, chỉ User + avatar
- `src/services/auth/authService.ts` — bỏ Resident import + `LoginResult.residentId`
- `src/services/auth/tokenService.ts` — bỏ `JWTPayload.resident_id` + `TokenUserInput.residentId`
- `src/services/scheduler/scheduleJobService.ts` — `updateOverdueBills` no-op (Bill model xóa)
- `src/contracts/auth/schema.ts` — bỏ `ResidentProfileSchema` + `UserPublicSchema.resident*`; thêm `ResendVerification*` + `VerifyEmail*` schemas (đã thiếu trước đó)
- `src/utils/authResponseUtils.ts` + `src/controllers/api/v1/auth/login/{mobile,web}.ts` — bỏ `loginResult.residentId`
- `src/services/database/multiPoolService.ts` — đổi `await import()` dynamic thành direct default-import (TS-typed)
- `src/oidc/oidcService.ts` — TS-typed default-import cho `sequelizeService`
- `src/types/oidc-provider.d.ts` — **NEW**: ambient declaration cho `oidc-provider` (no published types)
- `src/config/{schema,env-schema,base,index,staging,production}.ts` — branding SSO + domain mới (vietprodev.com)
- `src/server.ts` — CORS allowlist SSO origins
- `src/services/notification/notificationService.ts` — `actorEmail: 'system@sso.vietprodev.com'`
- `src/services/auth/passwordService.ts` — default password `VietPro@2026`
- `src/services/auth/passwordValidationService.ts` — `User-Agent: SSO-VietProDev-Backend`
- `lib/generator/openapi/generate-openapi.ts` — title `SSO VietProDev API`, server `/api/v1`
- `lib/storage/swagger/openapi.json` — **DELETE** (build-output stale copy, regenerated as `storage/swagger/openapi.json`)

### Kết quả `npx tsc --noEmit`

```
exit=0 — 0 errors
```

Toàn bộ lỗi pre-existing OIDC infrastructure đã fix session 4 (thêm `src/types/oidc-provider.d.ts`, fix default-import typing trong `oidcService.ts` + `multiPoolService.ts`). Tất cả errors từ session 3 cleanup đã fix (trước đó còn: `auth/profile.ts`, `auth/authService.ts`, `auth/tokenService.ts`, `contracts/auth/schema.ts`, `init-models.ts`, `User.ts`, `File.ts`, `WebhookLog.ts`, `scheduleJobService.ts`, các `login/{mobile,web}.ts`).

### Kết quả `pnpm swagger:generate`

- `info.title = "SSO VietProDev API"` ✓
- `info.description` đã đổi ✓
- `servers[1].url = "/api/v1"`, description `"SSO VietProDev API"` ✓
- 50 paths, tất cả SSO (auth, users, roles, permissions, user-roles, role-permissions, files, notifications, audit-logs, audit-dead-letters)
- **0 path legacy** (`/residents, /buildings, /contracts, /incidents, /posts, /bills, /beds, /apartments, /rooms, /technicians, /system-configs, /contract-files, /contract-types, /debt-reminder-policies, /incident-files, /incident-types, /notification-templates, /resident-documents` đã xóa hết)

### Verification: BeKind residue

**0 match** trong `src/` + `lib/` + `storage/swagger/openapi.json`:

```bash
$ grep -rEi "BeKind|bekind|Meu Solutions" src/ lib/ storage/swagger/openapi.json --include="*.ts" --include="*.json"
# (no output)
```

Tất cả 17 hit pre-existing trong session 3 đã clean:
- `src/server.ts:154` CORS allowlist (2 origins) → `gateway.dev.vietprodev.com`, `sso-dev.vietprodev.com`
- `src/config/{schema,env-schema,base}.ts` 3× `projectName: 'Bekind Backend'` → `SSO VietProDev Backend`
- `src/config/index.ts` comment `/bekind for staging` → `/sso for staging`
- `src/config/{staging,production}.ts` hostnames (`*.bekind.com`, `minio.bekind.com`, `bekind-staging`, `bekind` bucket) → `*.vietprodev.com`, `sso-staging`, `sso` bucket
- `src/services/notification/notificationService.ts` 4× `actorEmail: 'system@bekind.vn'` → `system@sso.vietprodev.com`
- `src/services/auth/passwordService.ts` default password `Bekind@123` → `VietPro@2026`
- `src/services/auth/passwordValidationService.ts` `User-Agent: BeKind-Backend` → `SSO-VietProDev-Backend`
- `src/services/database/multiPoolService.ts` comment `beKind-era` → `multi-tenant`
- `lib/storage/swagger/openapi.json` cached old copy (7 KB, 7168 lines) → **DELETED** (build-output stale; regenerated as `storage/swagger/openapi.json`)

### Default credentials (giữ nguyên)

```
Admin: admin@sso.vietprodev.com / VietPro@2026
```

### TODO chuyển session 5

1. **Regenerate bcrypt hash** cho `VietPro@2026` trong `sql/seeds/101-seed-default-users.sql` (TODO session 3 chưa xong).
2. **Test `pnpm dev` + Swagger UI** verify runtime (chưa chạy trong session 4).
3. **Verify migration** trên DB thật: `node sql/scripts/migrate.js` + smoke-test login + register endpoints.

### Commands chạy lại để verify

```bash
cd sso-vietprodev-backend

# Type check — phải 0 errors
npx tsc --noEmit

# Verify SSO paths only
node -e "const o=require('./storage/swagger/openapi.json'); \
  console.log('Title:', o.info.title); \
  console.log('Paths:', Object.keys(o.paths).length); \
  console.log(Object.keys(o.paths).sort().join('\n'));"

# Regenerate OpenAPI (idempotent)
pnpm swagger:generate

# Manual UI check
pnpm dev
# → http://localhost:3001/swagger/index/
```
---

## §18 — Session 5: Bug Audit, Cleanup & Refactor (2026-06-20)

### Bugs fixed

**1. User info returns `Unknown` / missing email in demo apps**

`oidcService.ts` — `findAccount.claims()` chỉ trả về `{ sub }`. Sửa để lấy đầy đủ user claims từ database:

```ts
claims: () => ({
  sub,
  ...(user?.email ? { email: user.email } : {}),
  ...(user?.email_verified_at != null ? { email_verified: true } : { email_verified: false }),
  ...(fullName ? { name: fullName } : {}),
  ...(user?.username ? { preferred_username: user.username } : {}),
})
```

**2. Logout "No, stay signed in" redirects to wrong app (Project A instead of Project B)**

Vấn đề root cause: `client_id` không được gửi trong form body → `resolveRedirectUri` nhận `clientId=undefined` → fallback về `http://localhost:4001` (Project A).

Fixes:
- `oidcService.ts` — `logoutFormHtml`: thêm `<input type="hidden" name="client_id" value="${clientId}" />` vào form.
- `logoutSource`: truyền `clientId` vào form.
- `oidcRoutes.ts` — POST `/oauth/logout/confirm`: đọc `client_id` từ `req.body` (ưu tiên `req.query`).

**3. `postLogoutSuccessSource` redirect fails after session destroy**

Sau khi logout confirmed, session bị destroy trước khi `postLogoutSuccessSource` chạy → `ctx.oidc.session?.state?.postLogoutRedirectUri` bị undefined → fallback về logout-success page.

Fix: Dùng `ctx.oidc.params.post_logout_redirect_uri` (query string, tồn tại sau session destroy) thay vì `ctx.oidc.session.state.postLogoutRedirectUri`. Validate URI với registered `postLogoutRedirectUris` của client trước khi redirect.

### Verification

- `npx tsc --noEmit` → exit 0
- OIDC flow: register → verify → login → consent → token exchange → userinfo trả về đầy đủ `{ sub, name, preferred_username, email, email_verified }`
- Logout Project B → "No, stay signed in" → redirect về Project B (4002) ✅
- Logout Project B → "Yes, sign out" → redirect về Project B ✅
- Logout Project A → "No, stay signed in" → redirect về Project A (4001) ✅


Comprehensive audit + cleanup pass on top of the SSO core. `tsc --noEmit` clean; OpenAPI regenerates; smoke-tested against the running dev server.

### Bugs fixed
- **`src/config/env-schema.ts` + `src/config/base.ts`** — `DB_NAME` defaulted to `"sso"`, but `.env` and all migrations use `vietprodev_sso`. Login/register endpoint connected to a non-existent DB and returned `database "sso" does not exist`. Defaulted both to `vietprodev_sso`.

### Dead code removed
- `src/utils/incidentCodeGenerator.ts` — referenced removed `incidents` table.
- `src/utils/billCodeGenerator.ts` — referenced removed `bills` table.
- `src/utils/templateCodeGenerator.ts` — referenced removed `notification_templates` table.
- `src/utils/nestedFilterUtils.ts` — beKind-specific, no remaining importers.
- `src/middlewares/response.ts` — removed `SequelizeExclusionConstraintError` branch that translated to `bed_id` / "Giường đã có resident…" (bed model removed in session 4).
- `src/middlewares/response.ts` — removed composite-constraint messages for `uq_apartments_code`, `uq_rooms_code`, `uq_beds_code` (apartments/rooms/beds tables removed in session 4). Falls back to the standard single-field duplicate message.

### `console.log` / `console.error` cleanup → structured `LoggingService`
Controllers now route error logs through `LoggingService.logErrorAsync` instead of stdout:
- `src/controllers/api/v1/files/index.ts` (3 sites)
- `src/controllers/api/v1/files/upload-multiple.ts` (3 sites)
- `src/controllers/api/v1/auth/profile.ts`
- `src/controllers/api/v1/auth/send-confirm-otp.ts`
- `src/controllers/api/v1/auth/forgot-password/send-otp.ts`
- `src/controllers/api/v1/auth/forgot-password/confirm-reset.ts` (2 sites)
- `src/controllers/api/v1/notifications/subscribe/index.ts` (3 sites)
- `src/middlewares/response.ts` — `SequelizeForeignKeyConstraintError` branch

Other `console.*` calls in `src/utils/logger.ts`, `src/config/index.ts`, scheduler, redis adapters, and infra services are intentional (they are the logger implementation or low-level infra startup banners).

### Verification
- `npx tsc --noEmit` → exit 0
- `pnpm swagger:generate` → 50 paths registered, OpenAPI spec written
- Dev server running on port 3001; `/health`, `/.well-known/openid-configuration`, `/oauth/jwks`, `/swagger/index`, `/swagger/openapi.json` all return 200; protected endpoints return 401 unauthenticated as expected.

---

## §19 — Session 6: Bug Fixes + Phase 4 Production Readiness (2026-06-22)

### Bug: `column "backchannel_logout_uri" does not exist` → OIDC provider failed

**Root cause:** Migration `043-add-backchannel-logout-clients.sql` đã chạy trên database Docker `sso` thay vì local Windows `vietprodev_sso`. Khi `ClientProvider.listActive()` chạy query, gặp column missing → `OidcService.initialize()` reject → `instance` không được set → cascade `getCallbackMiddleware()` throw.

**Fix:** Chạy migration trên đúng DB:

```sql
-- Local Windows PostgreSQL, database: vietprodev_sso
ALTER TABLE clients ADD COLUMN IF NOT EXISTS backchannel_logout_uri TEXT;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS backchannel_logout_session_required BOOLEAN NOT NULL DEFAULT FALSE;
UPDATE clients SET backchannel_logout_uri = 'http://localhost:4001/auth/backchannel-logout',
  backchannel_logout_session_required = true WHERE client_id = 'project-a-demo';
UPDATE clients SET backchannel_logout_uri = 'http://localhost:4002/auth/backchannel-logout',
  backchannel_logout_session_required = true WHERE client_id = 'project-b-demo';
```

### Sync: project-b-demo/server.js

`project-b-demo/server.js` thiếu silent SSO flow so với `project-a-demo/server.js`. Đồng bộ hóa:

- Home route `/` → redirect to `/auth/silent-login` (không còn hiển thị nút)
- `/auth/silent-login` endpoint với `prompt=none`
- `/auth/callback` handle `login_required` + `consent_required` → fallback interactive login
- `/auth/backchannel-logout` POST endpoint
- Session config: `saveUninitialized: false`, `name: project-b.sid`

### Phase 4.4: HTTPS Redirect Middleware

**File:** `src/middlewares/httpsRedirect.ts`

- Redirects HTTP → HTTPS (308 Permanent Redirect) khi `NODE_ENV != development`
- Support `X-Forwarded-Proto` từ trusted proxies (loopback)
- Skip health check paths: `/health`, `/healthz`, `/ready`, `/favicon.ico`
- Registered FIRST in `server.ts` `initServer()` — trước cả CORS

### Phase 4.5: OIDC Signing Key Rotation

**Files:**
- `src/jobs/secretRotation.ts` — RSA key generation, rotation logic, retirement cleanup, keystore persistence
- `src/config/oidcConfigService.ts` — thêm `jwkFile` config
- `src/types/oidc-provider.d.ts` — thêm declaration cho `ExternalSigningKey`

**Architecture:**

oidc-provider v9.8.4 `initialize_keystore.js` dùng `structuredClone()` để deep-clone input JWK. Nếu có `d` field → validate toàn bộ RSA private components (`d, p, q, dp, dq, qi`). Nếu `d = ""` → crash.

Giải pháp: dùng `ExternalSigningKey` class từ `oidc-provider/lib/helpers/keystore.js`. Class này bypasses private-field validation (chỉ check `sign()` method tồn tại), cho phép lưu full JWK trên disk mà oidc-provider không báo lỗi.

```typescript
class RsaExternalSigningKey extends ExternalSigningKey {
  constructor(private jwk) {
    super();
    this.kid = jwk.kid;
    this.alg = jwk.alg ?? 'RS256';
  }
  keyObject() {
    return crypto.createPrivateKey({ key: this.jwk, format: 'jwk' });
  }
  async sign(data): Promise<Buffer> {
    return crypto.sign(null, Buffer.from(data), this.keyObject());
  }
}
```

**Flow:**
1. Server start → load key từ `config/keys/keystore.json`
2. Wrap key trong `RsaExternalSigningKey`
3. oidc-provider dùng cho signing (gọi `.sign()`) và JWKS endpoint (chỉ expose public parts)
4. Rotation: `rotateSigningKey()` → mark old key pending retirement (+24h grace period)
5. `retireOldKeys()` → remove keys đã qua deadline (gọi mỗi server start)

**Key files:**
- `config/keys/keystore.json` — chứa full JWK (public + private), never committed
- `src/jobs/secretRotation.ts` — rotation logic

### Phase 4.6: OpenAPI Final Scan

- `pnpm swagger:generate` → 69 operations, all domains registered
- `pnpm swagger:check` → ✅ All `$ref` resolved, all `operationId` present and unique
- Baseline created: `storage/swagger/openapi.baseline.json`

### Phase 4.7: Documentation

- `RUN.md` — Added §14 Production Deployment (env vars, build, key rotation, health check)
- `PLANS.md` — Updated Phase 4 file list
- `README.md` — Added production features section
- `RELEASE_NOTES.md` — Created v1.0.0 release notes

### Phase 4.8: Smoke Test

```
npx tsc --noEmit → exit 0
pnpm swagger:validate → 69 operations validated
curl http://localhost:3001/health → {"status":"healthy",...}
curl http://localhost:3001/oauth/jwks → public JWK (kid=1dee62d15159ef65, no 'd' field) ✅
```

