Appearance
JWT Authentication, Multi-Layer RBAC & Coding Conventions (CLAUDE.md)
Tài liệu này mô tả chi tiết cơ chế Authentication & Authorization (AuthN/AuthZ) trong apps/backend và tổng hợp các coding conventions bắt buộc của dự án.
1. Dual-Token JWT Authentication & Instant Revocation (token_version)
1.1. Login Flow & Subscription Service Gate (enforceServiceGate — TXNG-F46)
Khi người dùng gọi POST /api/v1/auth/login (apps/backend/src/modules/auth/auth.service.ts):
- Verify Credentials: Kiểm tra
email,deleted_at == null,is_active == truevà đối chiếu password bằngbcrypt.compare(). - Subscription Service Gate (
enforceServiceGate): Nếu người dùng thuộc khối Enterprise (role === ROLE.BUSINESShoặcuser_type === 'ENTERPRISE'), hệ thống kiểm tra trạng thái subscription (company.subscription) theo quy tắcTXNG-F46:PENDING: Chặn login (ACCOUNT_NOT_ACTIVATED).TERMINATEDhoặcSUSPENDED: Chặn login (SERVICE_TERMINATED/SERVICE_LOCKED).EXPIRED(hoặcACTIVEnhưng đã quáend_date + grace_days): Tự động chuyển trạng thái gói sangEXPIREDvà chặn login (SERVICE_EXPIRED).
- Issue Dual Tokens (Access Token 24h + Refresh Token 7d):
- Access Token (JWT, TTL 24h) chứa payload:
{ userId, email, role, company_id, token_version }. - Refresh Token (JWT ký bằng
JWT_REFRESH_SECRETriêng, TTL 7d) chứa{ userId, token_version, type: 'refresh' }. - Set đồng thời vào
HttpOnlyCookie (token,refresh_token,sameSite: 'lax') và trả về trong JSON body để tương thích cả SSR lẫn SPA.
- Access Token (JWT, TTL 24h) chứa payload:
1.2. Instant Token Revocation qua token_version & Redis Cache
Trong JwtStrategy.validate() (apps/backend/src/modules/auth/jwt.strategy.ts):
- Hệ thống cache thông tin permissions của user trong Redis với key
user_perms:${userId}(TTL 60 giây). - Ở mỗi request,
JwtStrategyso sánhpayload.token_versiontrong JWT vớicached.tokenVersion(hoặcdbUser.token_versiontrong PostgreSQL). - Khi user
logout,resetPassword, hoặc bị Admin khóa tài khoản, hệ thống tăngtoken_version: { increment: 1 }trong bảngusersvà xóa keyuser_perms:${userId}khỏi Redis. Toàn bộ JWT cũ lập tức bị từ chối với mã401 Unauthorizedmà không cần chờ hết hạn 24 giờ.
2. Multi-Layer RBAC & Guard Pipeline
apps/backend áp dụng pipeline kiểm soát truy cập nhiều lớp (Defense-in-Depth):
Rendering diagram...
2.1. Permission Aggregation trong JwtStrategy
- Source 1 (
userRolePermissions— Direct/Legacy): Gắn trực tiếp qua bảng trung gian giữaUservàRole. - Source 2 (
role_assignments— Multi-Role & Scoped RBAC): Cho phép một user nắm giữ nhiều roles (PLATFORM_ADMIN,GOV_ADMIN,GOV_INSPECTOR,ENT_OWNER,ENT_ADMIN,ENT_STAFF...) kèm datascope(unit_ids,industry_codes,company_ids). - Default Permissions (
ROLE_DEFAULT_PERMISSIONS): Được merge dựa trênrolevàorganization_typescủa tenant (ví dụ tenant loạiSCHOOL/CATERINGtự động nhận bộ quyền cơ bản của phân hệ Bếp ăn học đường).
2.2. Guard Matrix trong apps/backend/src/common/guards/
| Guard Name | File Path | Core Validation Logic |
|---|---|---|
JwtAuthGuard | modules/auth/jwt-auth.guard.ts | Bypass nếu route gắn @Public(). Ngược lại kích hoạt PassportStrategy('jwt') để populate req.user (JwtPayload). |
PermissionsGuard | common/guards/permission.guard.ts | 1. Gọi checkCompanyServiceAccess để chặn doanh nghiệp hết hạn gói dịch vụ (TXNG-F46).2. Nếu user có role === ROLE.ADMIN (1) → allow (return true).3. Đọc metadata @CheckPermissions([...]) và đối chiếu với user.permissions theo toán tử AND (default) hoặc OR (PermissionLogic.OR). |
AdminGuard | common/guards/admin.guard.ts | Chỉ cho phép tài khoản Platform Admin (user.role === ROLE.ADMIN). |
AdminOrActiveCompanyGuard | common/guards/admin-or-active-company.guard.ts | Cho phép ROLE.ADMIN hoặc user thuộc doanh nghiệp đang ở trạng thái ACTIVE. (Lưu ý Security: Guard này KHÔNG thay thế cho AdminGuard hay PermissionsGuard — xem chi tiết tại SEC-04). |
3. Coding Conventions (CLAUDE.md)
Từ hệ thống tài liệu nội bộ (CLAUDE.md, apps/frontend/CLAUDE.md, apps/backend/CLAUDE.md), đội ngũ phát triển tuân thủ các quy chuẩn sau:
3.1. Frontend Conventions (apps/frontend)
- Multi-Brand Config (
config/brand.ts): Không bao giờ hardcode chuỗi"GoTrace"hay"HanoiTrace"trong UI; luôn đọc từ brand config (NEXT_PUBLIC_BRAND). - Component & UI Pattern:
- Sử dụng bộ shared components trong
components/common/(TableWrapper,FormModal,StatusBadge,ImportExportButtons) kết hợp HeroUI (@heroui/react). - Forms sử dụng
react-hook-formkết hợp validation schemazod.
- Sử dụng bộ shared components trong
- API Client & Anti-Crawling:
- Sử dụng centralized API client gắn sẵn auth token và cặp header
x-gotrace-nonce/x-gotrace-signature(lib/anti-crawl.ts) khi gọi các public lookup APIs.
- Sử dụng centralized API client gắn sẵn auth token và cặp header
3.2. Backend Conventions (apps/backend)
- Multi-Tenancy Enforcement (
CompanyHelper):- Với các API CRUD hoặc lấy chi tiết của doanh nghiệp, luôn resolve
companyIdtừuserthông quacompanyHelper.getCompanyId(user, query.company_id)(Admin được truyềncompany_id, còn tài khoản Enterprise luôn bị pin cứng vàouser.company_idtừ DB). - Với các API list/analytics cho phép Admin xem toàn hệ thống khi không filter theo tenant, dùng
companyHelper.getOptionalCompanyId(user, query.company_id).
- Với các API CRUD hoặc lấy chi tiết của doanh nghiệp, luôn resolve
- IP Normalization (
@ClientIp()):- Sử dụng decorator
@ClientIp()(src/common/decorators/client-ip.decorator.ts) thay cho@Ip()mặc định của NestJS để normalize IPv4-mapped IPv6 (::ffff:203.0.113.9→203.0.113.9) ngay tại controller boundary trước khi ghi audit log hoặc check rate-limit.
- Sử dụng decorator
- Unified Excel Import / Export (
TXNG-F49):- Không viết lặp lại logic đọc/ghi Excel trong từng service; khai báo
ModuleImportExportConfig(*.excel.config.ts) và delegate choModuleImportExportService(exportSample,exportData,import,exportErrors).
- Không viết lặp lại logic đọc/ghi Excel trong từng service; khai báo
- National Gateway Sync & Async VC Signing:
- Mọi thay đổi dữ liệu cần đồng bộ quốc gia phải gọi
enqueueInTx(tx, ...)của@gotrace/integration-outboxngay bên trongprisma.$transaction. - Mọi sự kiện phát sinh W3C VC phải gọi
vcOutbox.enqueueInTx(tx, ...)trong DB transaction và gọivcOutbox.scheduleIssuance(outboxId)ở bướcpost-commit(bọc trongtry/catchđể lỗi Redis không làm fail business transaction đã commit).
- Mọi thay đổi dữ liệu cần đồng bộ quốc gia phải gọi