Functional Specification · PCCCTrace Web DN

DN-003 — Đăng nhập (Login)

Đặc tả chức năng cho developer — đăng nhập bằng email công ty + mật khẩu, phát hành JWT, chống brute-force theo FR-7.

Screen IDDN-003
ModuleAuth & Onboarding
ActorDN Owner / Admin / Staff (Dealer chỉ mobile)
RequirementsFR-4 MVP, FR-7, FR-71
Trạng tháiDraft — chờ review
Version / Ngàyv1.0 · 03/07/2026
Màn hình liên quanDN-001 · DN-002 · DN-004

1.Tổng quan & phạm vi

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).

Trong phạm vi (In scope)

Ngoài phạm vi (Out of scope)

2.User story & Acceptance criteria

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.
IDAcceptance criteria (Gherkin)
AC-01Given 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-02Given 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-03Given đã 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-04Given 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-05Given 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.

IDThành phầnLoạiBắt buộcHành vi / Ghi chú
LG-00Header cardStaticH1 "Đăng nhập" + lead "Đăng nhập bằng email công ty và mật khẩu của bạn".
LG-01Email công tyEmail inputtype=email · autocomplete="username" · trim + lowercase trước khi gửi.
LG-02Mật khẩuPassword input + toggle 👁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.
LG-04Link "Quên mật khẩu?"LinkDN-004 self-service reset.
LG-05Nút "Đăng nhập"Button primary, full-widthSubmit → disable + spinner "Đang đăng nhập..." chặn double-submit; Enter ở bất kỳ field nào cũng submit.
LG-06Hint đ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-07Khối trấn an bảo mậtCalloutText 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-08Topbar 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.

FieldRuleThông báo lỗi
LG-01Rỗng / sai định dạng emailVui lòng nhập email hợp lệ
LG-02RỗngVui lòng nhập mật khẩu
FormServer: 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
FormServer: đ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
FormServer: lỗi 5xxToast "Có lỗi xảy ra, vui lòng thử lại" — giữ nguyên dữ liệu đã nhập

6.Business rules

IDQuy tắcNguồn
BR-01Xá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-02JWT 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-03Rate 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-04Thô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-06Role 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-07Ghi 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)

StateMô tả hiển thị
Empty / DefaultForm 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.
ErrorBanner đỏ đầ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.
SubmittingNút LG-05 disable + spinner "Đang đăng nhập..." — chặn double-submit.
LockedBanner 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 prototype dn-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.

9.Yêu cầu phi chức năng

10.Edge cases

IDTình huốngXử lý
EC-01User pending_kyc đăng nhập đúngCho đăng nhập, JWT scope hạn chế, redirect DN-002 (BR-05); không coi là lỗi.
EC-02User 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-03User 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-04Refresh token hết hạn giữa phiênRefresh 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-05Role Dealer đăng nhập web403 → 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-06Caps Lock đang bật khi gõ mật khẩuHiệ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

IDCâu hỏiĐề xuất của BA
OQ-01Checkbox "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-02Khoá 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-03Sau đă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

VersionNgàyNgườiThay đổi
v1.003/07/2026BABả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.
← Quay lại mockup