Mục đích: Cho phép mọi user thuộc tenant DN (Owner / Admin / Staff) đăng nhập app.pccctrace.vn bằng email + mật khẩu truyền thống (auth model chốt 2026-06-09). Backend so khớp bcrypt.compare(password, password_hash) → phát hành JWT với sub = user_id (không phải email — email là login identifier, cố định sau đăng ký: chức năng đổi email đã loại bỏ 03/07/2026, trường hợp cần đổi liên hệ hỗ trợ xử lý thủ công).
Chống brute-force: sai 5 lần / 15 phút → khoá tạm 30 phút (FR-7); thông báo lỗi generic.
Điều hướng theo trạng thái tenant: active → app (DN-039 Home) · pending_kyc → DN-002.
Link "Quên mật khẩu?" → DN-004 (self-service reset).
Ngoài phạm vi (Out of scope)
SSO / OAuth bên thứ ba — không có trong lộ trình (định hướng simple/minimum, 30 DN).
OTP SMS — không bao giờ: SĐT chỉ là contact data, không tham gia auth.
Đăng nhập Dealer trên web — Dealer chỉ dùng mobile app (xem EC-05).
2.User story & Acceptance criteria
Là nhân sự của doanh nghiệp PCCC đã có tài khoản, tôi muốn đăng nhập nhanh bằng email công ty và mật khẩu, để vào ngay khu vực quản lý truy xuất nguồn gốc tem của doanh nghiệp mình.
ID
Acceptance criteria (Gherkin)
AC-01
Given tài khoản hợp lệ thuộc tenant active, When nhập đúng email + mật khẩu và bấm "Đăng nhập", Then nhận JWT và điều hướng vào app (DN-039 Home) trong <2s.
AC-02
Given email hoặc mật khẩu sai, When submit, Then hiển thị lỗi generic "Email hoặc mật khẩu không đúng" — không tiết lộ email có tồn tại hay không.
AC-03
Given đã sai 5 lần trong 15 phút, When thử lần tiếp theo, Then trả lỗi khoá tạm 30 phút kèm thời gian còn lại; đúng mật khẩu cũng không vào được khi đang khoá.
AC-04
Given tài khoản thuộc tenant pending_kyc, When đăng nhập đúng, Then chỉ được đưa tới DN-002 (KYC Pending), không vào app.
AC-05
Given tick "Ghi nhớ đăng nhập 30 ngày", When đăng nhập thành công, Then nhận refresh token 30 ngày; không tick → phiên hết khi đóng trình duyệt.
3.Luồng nghiệp vụ
Nhập email + mật khẩu ──► POST /auth/login
│
├── 200 + tenant active ────► JWT (sub=user_id) ──► DN-039 Home
├── 200 + pending_kyc ──────► DN-002 KYC Pending (chỉ màn này)
├── 401 INVALID_CREDENTIALS ► lỗi generic + đếm số lần sai
├── 423 ACCOUNT_LOCKED ─────► "Thử lại sau {n} phút" (FR-7)
└── "Quên mật khẩu?" ───────► DN-004 Recovery
4.UI elements
Layout: card căn giữa, max-width 440px, nền --neutral. Tham chiếu trực tiếp prototype DN-003 — dùng đúng design tokens trong tokens.css.
ID
Thành phần
Loại
Bắt buộc
Hành vi / Ghi chú
LG-00
Header card
Static
—
H1 "Đăng nhập" + lead "Đăng nhập bằng email công ty và mật khẩu của bạn".
LG-01
Email công ty
Email input
Có
type=email · autocomplete="username" · trim + lowercase trước khi gửi.
LG-02
Mật khẩu
Password input + toggle 👁
Có
autocomplete="current-password" · toggle mắt chuyển type=password ⇄ text, có aria-label.
LG-03
"Ghi nhớ đăng nhập 30 ngày"
Checkbox
—
Điều khiển việc phát hành refresh token 30d (AC-05). Mặc định không tick — xem OQ-01.
Submit → disable + spinner "Đang đăng nhập..." chặn double-submit; Enter ở bất kỳ field nào cũng submit.
LG-06
Hint điều khoản
—
—
Đã bỏ (03/07/2026): consent đã thu bằng checkbox tại DN-001 (OQ-05) — dòng "Bằng việc đăng nhập, bạn đồng ý..." là thừa và mâu thuẫn mô hình consent. Footer chung vẫn có link Điều khoản / Quyền riêng tư.
LG-07
Khối trấn an bảo mật
Callout
—
Text ngắn cho end-user: "Kết nối được mã hoá an toàn (SSL/TLS)." — không hiển thị chi tiết kỹ thuật nội bộ (bcrypt, mã yêu cầu FR) cho người dùng cuối.
LG-08
Topbar link "Đăng ký doanh nghiệp"
Link
—
→ DN-001 (user chưa có tài khoản).
5.Validation & thông báo lỗi
Validate client (on submit) + server (nguồn chân lý cuối). Thông báo lỗi 100% tiếng Việt, luôn generic với lỗi thông tin đăng nhập.
Field
Rule
Thông báo lỗi
LG-01
Rỗng / sai định dạng email
Vui lòng nhập email hợp lệ
LG-02
Rỗng
Vui lòng nhập mật khẩu
Form
Server: sai email hoặc mật khẩu (401)
Email hoặc mật khẩu không đúng. Sai 5 lần trong 15 phút sẽ tạm khoá đăng nhập 30 phút — banner đỏ đầu card, viền đỏ cả 2 field, không nói field nào sai
Form
Server: đang khoá tạm (423)
Tài khoản tạm khoá do nhập sai nhiều lần. Vui lòng thử lại sau {n} phút
Form
Server: lỗi 5xx
Toast "Có lỗi xảy ra, vui lòng thử lại" — giữ nguyên dữ liệu đã nhập
6.Business rules
ID
Quy tắc
Nguồn
BR-01
Xác thực bằng bcrypt.compare(password, password_hash), cost 12 — đồng bộ DN-001 BR-07. Không bao giờ lưu/log plaintext.
FR-4
BR-02
JWT sub = user_id (bất biến), không phải email. Access token hiệu lực 1 giờ; refresh token 30 ngày chỉ khi tick "Ghi nhớ".
FR-4
BR-03
Rate limit: sai 5 lần / 15 phút (theo cặp email + IP) → khoá tạm 30 phút. Trong lúc khoá, mọi lần thử đều trả 423, kể cả đúng mật khẩu.
FR-7
BR-04
Thông báo lỗi generic — không phân biệt "email không tồn tại" và "sai mật khẩu" (chống user enumeration).
FR-7
BR-05
Điều hướng sau đăng nhập theo kyc_status: active → DN-039 Home · pending_kyc → DN-002 · rejected → DN-002 (khối từ chối).
FR-1a, FR-18
BR-06
Role Dealer không đăng nhập được web — trả lỗi chỉ dẫn dùng mobile app (xem EC-05).
07-1 inventory
BR-07
Ghi audit log mọi lần đăng nhập thành công và mọi lần khoá tạm: user_id, IP, user-agent, timestamp (identity audit).
FR-71
7.API contract (đề xuất)
7.1 · Đăng nhập
POST /api/v1/auth/login
{
"email": "anhtuan@hathanhpccc.vn", // lowercase, trim
"password": "********",
"remember": true // LG-03 — phát hành refresh token 30d
}
// 200 OK
{
"access_token": "eyJ...", // JWT sub=user_id, exp 1h
"refresh_token": "rt_...", // chỉ khi remember=true, exp 30d
"user": { "user_id": "us_01H...", "role": "owner" },
"kyc_status": "active" // client điều hướng theo BR-05
}
// Lỗi
401 INVALID_CREDENTIALS → "Email hoặc mật khẩu không đúng" // generic — BR-04
423 ACCOUNT_LOCKED → { "retry_after_seconds": 1800 }
403 PLATFORM_NOT_ALLOWED → role Dealer — "Vui lòng dùng ứng dụng di động"
429 RATE_LIMITED → theo IP (lớp hạ tầng)
7.2 · Làm mới phiên
POST /api/v1/auth/refresh
{ "refresh_token": "rt_..." }
// 200 → access_token mới · 401 → hết hạn, đăng nhập lại
8.Trạng thái màn hình (UI states)
State
Mô tả hiển thị
Empty / Default
Form trống chỉ có placeholder, không lỗi, nút "Đăng nhập" enable.
Loading (skeleton)
Toàn bộ card thay bằng skeleton shimmer khớp layout thật (title, lead, 2 field, hàng remember/quên mật khẩu, nút, dòng trấn an) — dùng khi trang đang tải.
Error
Banner đỏ đầu card "Email hoặc mật khẩu không đúng. Sai 5 lần trong 15 phút sẽ tạm khoá đăng nhập 30 phút." + viền đỏ cả 2 field (generic — không chỉ field nào sai); giữ nguyên email đã nhập, xoá mật khẩu, focus mật khẩu.
Banner cam "Tài khoản tạm khoá... thử lại sau {n} phút" + disable form tới hết thời gian khoá.
Khối demo "State & Spec" trong prototypedn-003-login-mockup.html có khối "State & Spec" ở góc phải dưới: preview trực tiếp 3 trạng thái Loading / Empty / Error (bấm lại nút đang active để về default) + link mở file spec này. Khối này chỉ phục vụ review prototype — không build vào production.
Hiệu năng: đăng nhập hoàn tất <2s trên mạng 3G nhanh; trang tải <1s.
Accessibility: label gắn for/id; lỗi đọc được bởi screen reader (aria-describedby, aria-invalid); toggle mắt có aria-label; focus ring rõ.
Ngôn ngữ: 100% tiếng Việt.
Đơn giản hoá: quy mô 30 DN — không cần captcha ở MVP; rate limit theo email + IP là đủ (đồng bộ DN-001).
10.Edge cases
ID
Tình huống
Xử lý
EC-01
User pending_kyc đăng nhập đúng
Cho đăng nhập, JWT scope hạn chế, redirect DN-002 (BR-05); không coi là lỗi.
EC-02
User mất quyền truy cập email đăng nhập (nghỉ việc, email công ty bị thu hồi)
Đăng nhập không cần hộp thư — chỉ cần đúng email + mật khẩu. Quên cả mật khẩu → liên hệ trực tiếp DN Admin công ty đặt lại mật khẩu tạm qua DN-008 (chốt 03/07/2026, xem DN-004 spec BR-08); DN Owner → support@pccctrace.vn.
EC-03
User bị revoke khỏi tenant (DN-008) nhưng còn phiên cũ
Access token bị vô hiệu ở lớp API (check trạng thái member mỗi request nhạy cảm); đăng nhập mới trả 401 generic.
EC-04
Refresh token hết hạn giữa phiên
Refresh trả 401 → đăng xuất mềm, quay về DN-003 kèm thông báo "Phiên đã hết hạn, vui lòng đăng nhập lại"; giữ URL đích để quay lại sau đăng nhập.
EC-05
Role Dealer đăng nhập web
403 → thông báo "Tài khoản Dealer chỉ dùng ứng dụng di động PCCCTrace" (BR-06); không tính vào bộ đếm sai mật khẩu.
EC-06
Caps Lock đang bật khi gõ mật khẩu
Hiện hint nhỏ "Caps Lock đang bật" dưới field (detect getModifierState).
11.Câu hỏi mở — cần chốt trước khi dev
ID
Câu hỏi
Đề xuất của BA
OQ-01
Checkbox "Ghi nhớ đăng nhập 30 ngày" — prototype đang tick sẵn. Mặc định nên tick hay không?
Không tick mặc định (an toàn hơn trên máy dùng chung); cần sửa prototype nếu chốt.
OQ-02
Khoá tạm 30 phút theo cặp email + IP hay chỉ email?
Theo cặp email + IP để tránh attacker khoá nhầm user thật; đồng bộ với DN-001 BR-05.
OQ-03
Sau đăng nhập, "URL đích" trước đó (deep-link) có được giữ để redirect lại không?
Có — lưu redirect_to query param, chỉ chấp nhận đường dẫn nội bộ (chống open redirect).
12.Changelog
Version
Ngày
Người
Thay đổi
v1.0
03/07/2026
BA
Bản đầu tiên — viết từ prototype dn-003-login-mockup.html + UX spec 07-3 §6.1 (auth model email + mật khẩu, updated 2026-06-09) + PRD FR-4/FR-7/FR-71.