Hệ thống auth cho khu vực dashboard. Đăng nhập bằng email + password, session lưu server-side, cookie HTTP-only. Mục tiêu: replace middleware cũ (legacy token storage không meet compliance).
Data Model
Entities: User, Session, PasswordResetToken. Session liên kết 1-N với User, TTL 30 ngày, rolling khi có activity. PasswordResetToken single-use, TTL 1h, invalidate hết session khác khi reset thành công.
Stories
P0S-001Login với email + password3 AS
User nhập email + password → nhận session cookie, redirect tới /dashboard. Sai password hiện lỗi generic. Quá 5 lần sai trong 15 phút → tạm khoá.
AS-001Login thành công với credential đúng›
Given
User jane@acme.com tồn tại, password đúng, không có session active
When
POST /api/login với {email, password}
Then
Response 200, set cookie sid (HTTP-only, Secure, SameSite=Lax), session row được tạo trong DB
AS-005Logout khi không có session vẫn 200 (idempotent)›
Given
Không có cookie sid hoặc session đã expire
When
POST /api/logout
Then
Response 200, không lỗi. Idempotent (gloss: gọi nhiều lần kết quả như gọi 1 lần).
P1S-003Password reset qua email3 AS
User nhập email → nhận link reset (TTL 1h, single-use) → đặt password mới → invalidate tất cả session khác.
AS-006Request reset link gửi email (luôn 200 dù email tồn tại hay không)›
Given
Form reset password
When
POST /api/password-reset/request với {email}
Then
Response 200 với message generic "Check your email". Nếu email tồn tại → gửi link, không tồn tại → no-op (không leak).
AS-007Token expired (>1h) trả lỗi›
Given
Token tạo cách đây 65 phút
When
POST /api/password-reset/confirm với token + new password
Then
Response 410 "Token expired"
AS-008Reset thành công invalidate tất cả session khác›
Given
User có 3 session active (laptop, phone, tablet), token còn hạn
When
Reset password thành công
Then
3 session bị xoá, user phải login lại trên cả 3 thiết bị, token reset bị mark used.
P2S-004Remember-me cookie1 AS
Checkbox "Remember me" trong form login → session TTL 30 ngày rolling thay vì 24h.
AS-009Tick remember → cookie Max-Age 30 ngày›
Given
Form login có checkbox "Remember me" được tick
When
Login thành công
Then
Cookie sid có Max-Age=2592000 (30 ngày), session row có extended=true.
Constraints & Invariants
Password storage
Bcrypt cost 12 cho mọi password. Không dùng SHA*, không dùng PBKDF2. Migration từ legacy phải re-hash lúc login lần đầu.
Session cookie
Luôn HttpOnly, Secure, SameSite=Lax. Domain scope tới apex, không subdomain. Cookie name: sid.
Password policy
Tối thiểu 12 ký tự, kiểm tra zxcvbn score ≥ 3. Không bắt buộc ký tự đặc biệt (NIST 800-63B).
What Already Exists
Legacy auth middleware ở src/middleware/auth-legacy.ts đang dùng JWT lưu trong localStorage — đây là cái cần thay (compliance flagged). Bcrypt utility đã có ở src/lib/crypto/hash.ts, reuse. Rate limit helper src/lib/rate-limit.ts đã có cho API publicly-facing — reuse cho login.
Not in Scope
OAuth / SSO — defer phase 2, hiện ưu tiên migrate khỏi legacy trước.
2FA / TOTP — separate spec, planned Q3.
Email change flow — sẽ làm khi có account settings page.
Audit log UI — log có ghi DB nhưng không expose UI trong spec này.