# Beta — smoke test đăng nhập web & OTP (Cổ Thư Kỳ Tịch)

Tài liệu **lặp lại được** cho QA beta: đăng ký/đăng nhập web, xác minh SĐT OTP, trang `/account`, và cổng `GET /auth/account-gate-test`. CMS **không** đổi trong flow này; cuối checklist có mục hồi quy CMS.

**Telemetry (soft launch):** Nếu bật `NEXT_PUBLIC_SOFT_LAUNCH_MODE`, sau luồng OAuth/redirect hãy xác nhận events vẫn tới `/soft-launch/telemetry/events` và **không** lộ `accessToken` / JWT trong payload (xem checklist staging: `docs/staging-verification-checklist.md`).

**Funnel Tu Vi (beta / soft launch):** Ghi form → kết quả → lưu → ghi nhận premium — bật `NEXT_PUBLIC_SOFT_LAUNCH_MODE=1` (và backend `SOFT_LAUNCH_MODE=1` nếu deploy). Trên DevTools → Network, xác nhận batch `POST …/soft-launch/telemetry/events` có các `eventType` dự kiến: `tu_vi_form_view`, `tu_vi_form_submit`, `tu_vi_result_view`, `tu_vi_a4_overview_view`, `tu_vi_save_click`, `tu_vi_save_success` / `tu_vi_save_failed`, `premium_teaser_view`, `premium_interest_open`, `premium_interest_submit`, `premium_interest_success` / `premium_interest_failed`. Trong payload từng event: **không** có token/JWT, **không** có `resultJson` / `inputJson` hay dữ liệu sinh chi tiết; được phép (tuỳ bước): `source`, `hasAccount`, `phoneVerified`, `savedReadingId` (chỉ sau **Lưu vào tài khoản** thành công), `viewingYearSolar`, `saveKind`, `errorCode`, `path`, `message` (rút gọn).

---

## Biến môi trường bắt buộc

### Backend (`backend/.env`)

| Biến | Ghi chú |
|------|---------|
| `DATABASE_URL` | PostgreSQL cho Prisma. |
| `JWT_ACCESS_SECRET` | Ký access token. |
| `JWT_ACCESS_TTL` | Tuỳ chọn (vd. `15m`). |
| `REFRESH_TOKEN_HASH_SECRET` | Hash refresh token trong cookie + (dev) fallback pepper OTP nếu không set `PHONE_OTP_PEPPER`. |
| `REFRESH_TOKEN_TTL_SECONDS` | Tuỳ chọn. |
| `PHONE_OTP_PEPPER` | **Production: bắt buộc** — pepper HMAC cho mã OTP. Dev có thể bỏ qua nếu đã có `REFRESH_TOKEN_HASH_SECRET`. |
| `NODE_ENV` | `development` / `production` — ảnh hưởng log OTP và cookie `secure`. |

### Web (`web/.env.local`)

| Biến | Ghi chú |
|------|---------|
| `NEXT_PUBLIC_API_BASE_URL` | Gốc API Nest, **không** dấu `/` cuối (vd. `http://localhost:3000`). |

Sao chép từ `web/.env.example` / `backend/.env.example`.

---

## Lệnh chuẩn bị

### Backend (thư mục `backend/`)

```bash
npm install
npx prisma migrate dev --schema=prisma/schema.prisma   # áp migration nếu cần
npx prisma generate --schema=prisma/schema.prisma
npm run build
npm test
npm run dev    # hoặc start tương đương — mặc định thường port 3000
```

### Web (thư mục `web/`)

```bash
npm install
npm run build
npm run dev    # mặc định thường port 3002 — xem package.json
```

---

## Luồng kiểm thử tay (happy path)

Thực hiện trên trình duyệt, backend + web đang chạy, CORS cho phép origin web.

1. **Đăng ký** user mới: `/register` — email + mật khẩu (≥ 8 ký tự), tuỳ chọn tên.
2. **Xác nhận redirect** tới `/verify-phone` (sau đăng ký khi tài khoản cần SĐT).
3. Mở **`/account`** (có thể gõ URL) **trước khi** gửi OTP — trang vẫn hiển thị (RequireAuth cho phép `/account` khi chưa xác minh SĐT).
4. **Xác nhận** phần hồ sơ từ `GET /auth/me` hiển thị (email, role, `requiresPhoneVerification`, v.v.).
5. Bấm **“Kiểm tra quyền truy cập đã xác minh SĐT”** (gọi `GET /auth/account-gate-test`).
6. **Xác nhận** thông báo lỗi / trạng thái tương ứng **403** với mã **`PHONE_VERIFICATION_REQUIRED`** (backend không nới guard).
7. Trên `/verify-phone`, nhập SĐT hợp lệ VN → **Gửi OTP**.
8. Ở console backend (chỉ **non-production**), đọc dòng log dạng: `DEV OTP for user <id>: <6 số>`.
9. Nhập OTP → **Xác minh**.
10. Gọi lại **`GET /auth/me`** (reload `/account` hoặc header refetch) — **`requiresPhoneVerification: false`**.
11. Vào lại **`/account`**.
12. Bấm lại gate-test — **thành công** (`ok: true`, `message: PHONE_VERIFIED_ACCESS_GRANTED`).
13. **Đăng xuất** local: bấm **“Đăng xuất”** trên header (xóa token `WEB_ACCESS_TOKEN_STORAGE_KEY` / giá trị `ctkp_web_access_token`, về `/login`) — **chưa** gọi `POST /auth/logout` backend.
14. **Đăng nhập** lại cùng tài khoản.
15. **Xác nhận** không bị redirect ép tới `/verify-phone` (đã xác minh SĐT).

---

## Trường hợp âm (negative)

| Kịch bản | Kỳ vọng gần đúng |
|----------|------------------|
| OTP sai | 400 + mã `OTP_EXPIRED_OR_INVALID`; `attemptCount` tăng. |
| OTP hết hạn | Sau TTL (~10 phút), verify → `OTP_EXPIRED_OR_INVALID`. |
| Gửi OTP quá sớm (cooldown 60s) | 400 + `OTP_COOLDOWN`. |
| SĐT đã gắn user khác | 409 + `PHONE_ALREADY_USED`. |
| Gọi API có Bearer nhưng xóa token / không token | 401 từ `JwtAuthGuard` (hoặc client báo chưa đăng nhập). |
| Số điện thoại sai định dạng VN | 400 + `PHONE_INVALID`. |

---

## Đăng nhập Google (OAuth)

### Backend (`backend/.env`) — khi bật Google login

| Biến | Ghi chú |
|------|---------|
| `GOOGLE_OAUTH_CLIENT_ID` | OAuth 2.0 Client ID. |
| `GOOGLE_OAUTH_CLIENT_SECRET` | Bí mật client (chỉ server). |
| `GOOGLE_OAUTH_CALLBACK_URL` | **Phải trùng** URI “Authorized redirect URI” trên Google Cloud Console (vd. `http://localhost:3000/auth/google/callback`). |
| `WEB_AUTH_SUCCESS_REDIRECT_URL` | Trang web sau đăng nhập thành công; **production** nên trỏ tới `https://<web-domain>/auth/oauth-success` (fragment `#accessToken=…` do backend gắn). |
| `WEB_AUTH_ERROR_REDIRECT_URL` | Trang lỗi, nên là `https://<web-domain>/login` (backend thêm `?auth_error=…`). |

### Google Cloud Console

- **Authorized redirect URI** = giá trị **chính xác** của `GOOGLE_OAUTH_CALLBACK_URL` (gồm scheme, host, port, path).

### Web (`web/.env.local`)

| Biến | Ghi chú |
|------|---------|
| `NEXT_PUBLIC_API_BASE_URL` | Gốc API; nút Google mở `${NEXT_PUBLIC_API_BASE_URL}/auth/google` (code web fallback local `http://localhost:3000` nếu biến chưa set). |

### Luồng kiểm thử tay (Google — happy path)

1. Mở **`/login`**.
2. Bấm **Đăng nhập Google**.
3. Hoàn tất **consent** trên Google.
4. Trình duyệt về **`/auth/oauth-success`** (có thể thấy hash `#accessToken=…` rất ngắn rồi biến mất).
5. **Xác nhận** URL **không còn fragment** (chỉ path + query nếu có).
6. **Xác nhận** token đã lưu: DevTools → Application → Local Storage → key `ctkp_web_access_token` (export `WEB_ACCESS_TOKEN_STORAGE_KEY`).
7. Nếu tài khoản **chưa** xác minh SĐT → redirect **`/verify-phone?next=/`**.
8. **Xác minh OTP** như mục happy path phía trên.
9. Vào **`/account`**, bấm **gate test** — **thành công** (`PHONE_VERIFIED_ACCESS_GRANTED`).

### Trường hợp âm (Google)

| Kịch bản | Kỳ vọng gần đúng |
|----------|------------------|
| User hủy consent / access denied | Redirect `/login?auth_error=GOOGLE_ACCESS_DENIED`; UI hiển thị thông báo thân thiện. |
| State cookie không khớp / thiếu | `GOOGLE_OAUTH_STATE` trên URL lỗi. |
| Backend thiếu biến Google OAuth | `GOOGLE_NOT_CONFIGURED` hoặc lỗi máy chủ tùy `NODE_ENV`. |
| Email Google trùng tài khoản đã có **mật khẩu** (không auto-link) | `GOOGLE_EMAIL_CONFLICT`. |

---

## Hồi quy CMS (không đổi code CMS)

- Mở app CMS, **`POST /auth/login`** email + mật khẩu admin/editor vẫn thành công.
- **`GET /auth/me`** vẫn có **`permissions`** (và các trường hợp đồng bộ trước đây).
- Các route CMS (bài viết, media, …) **không** bị chặn bởi `PhoneVerifiedGuard` (guard chỉ gắn endpoint gate-test / các route bạn chủ đích bảo vệ sau này).

---

## Production

- **Không** log OTP thô khi `NODE_ENV=production` (backend chỉ log DEV OTP ngoài production).
- Đặt **`PHONE_OTP_PEPPER`** riêng; production **không** được chỉ dựa vào fallback dev.
- **Google OAuth:** nếu hiển thị nút đăng nhập Google trên web, backend **phải** có đủ các biến OAuth (Client ID/Secret, callback, URL success/error). `WEB_AUTH_SUCCESS_REDIRECT_URL` phải trỏ tới path **`/auth/oauth-success`** trên đúng origin web.
- **`WEB_AUTH_ERROR_REDIRECT_URL`** nên trỏ tới **`/login`** trên web để user đọc `auth_error`.
- **CORS:** origin của web (và CMS nếu gọi API cross-origin) phải nằm trong `CORS_ALLOWED_ORIGINS` và cho phép **`credentials: true`** (không dùng `*`).

Xem thêm checklist production trong `backend/README.md`.

---

## Tham chiếu nhanh

| Mục | Đường dẫn / endpoint |
|-----|----------------------|
| Đăng nhập web | `/login` |
| Đăng ký | `/register` |
| OAuth success (web, sau Google) | `/auth/oauth-success` |
| Xác minh SĐT | `/verify-phone` |
| Tài khoản (beta) | `/account` |
| Gate test (JWT + SĐT) | `GET /auth/account-gate-test` |
| Bắt đầu Google OAuth (API) | `GET /auth/google` |
| Token localStorage (web) | Hằng export `WEB_ACCESS_TOKEN_STORAGE_KEY` trong `web-auth-client.ts` (= `ctkp_web_access_token`) |

---
