Authentication & Permission
Luồng đăng nhập
Kiến trúc Phân quy ền
Hệ thống phân quyền chia thành 3 tầng:
Tầng 1: Role (Vai trò)
Hằng số và utilities cho kiểm tra vai trò, định nghĩa trong @vppos/core:
// core/src/constants/permissions/roles.ts
export const ROLES = {
SUPER_ADMIN: "superadmin",
ADMIN: "admin",
USER: "user",
} as const;
// core/src/utils/role.utils.ts — Single Source of Truth
import { isSuperAdmin, isAdmin, hasAnyRole } from "@vppos/core/utils";
isSuperAdmin(roles); // Check superadmin
isAdmin(roles); // Check admin
hasAnyRole(roles, ["admin"]); // Check có bất kỳ role nào trong list
Dùng ở mọi nơi (Container + MFE apps) khi cần check nhanh phân cấp vai trò để thay đổi giao diện hoặc logic xử lý. Ví dụ: ẩn nút "Xóa hệ thống" nếu không phải superadmin.
Tầng 2: Module (Module đăng ký)
Module key do backend cấp theo gói đăng ký (subscription). MODULE_KEYS có cấu trúc nested với IDE auto-complete:
import { MODULE_KEYS } from "@vppos/core/constants";
// Nested structure — IDE tự gợi ý
MODULE_KEYS.INVENTORY.ROOT; // "inventory"
MODULE_KEYS.INVENTORY.PRODUCTS.ROOT; // "inventory.products"
MODULE_KEYS.INVENTORY.PRODUCTS.CATEGORY; // "inventory.products.category"
MODULE_KEYS.ANALYTICS.SALES.ROOT; // "analytics.sales"
Khi BE thêm module/view mới → chỉ cần thêm key vào MODULE_KEYS trong core → tự động có type-safe ở mọi nơi dùng.
Tầng 3: Permission (Quyền hành động)
Hệ thống cung cấp sẵn file taxonomy chuẩn xác thông qua PERMISSIONS object. Hook usePermission hỗ trợ Auto-complete để dễ dàng gọi mà không sợ gõ sai string.
import { usePermission } from "@vppos/core/hooks";
import { PERMISSIONS } from "@vppos/core/constants";
const { can, canAll, canAny, hasModule, isSuperAdmin, isAdmin, isUser } = usePermission();
// Permission checks với IDE auto-complete
can(PERMISSIONS.INVENTORY.PRODUCTS.PRODUCT.DELETE); // Check 1 permission
canAll(
PERMISSIONS.INVENTORY.PRODUCTS.PRODUCT.UPDATE,
PERMISSIONS.INVENTORY.PRODUCTS.PRODUCT.DELETE
); // Phải có TẤT CẢ
canAny(PERMISSIONS.INVENTORY.PRODUCTS.PRODUCT.UPDATE, PERMISSIONS.INVENTORY.PRODUCTS.PRODUCT.VIEW); // Có BẤT KỲ 1 trong list
Quy trình cập nhật Permission mới (Từ Backend)
Khi Backend thêm quyền mới, Frontend không cần thêm string thủ công. Toàn bộ chu trình đã được chuẩn hoá:
- Backend cập nhật quyền và cung cấp file
permissions.ts(được sinh tự động từ toolrbacgencủa hệ thống). - Frontend có thể nhận file
permissions.tsnày và đặt vào thư mục gốc của dự án (hoặc bất kỳ đâu). - Chạy lệnh:
Script này sẽ tự động đọc file taxonomy gốc, biên dịch thành cấu trúc object
node scripts/gen-permissions.js [đường_dẫn_file_nếu_không_để_ở_gốc]PERMISSIONSlồng nhau (nested) và ghi đè vào@vppos/core/constants/permissions/permissions.ts. IDE lập tức nhận diện được key mới!
// Module checks hasModule("inventory.products"); // Check module access
// Role checks isSuperAdmin; // boolean isAdmin; // boolean isUser; // boolean (không phải superadmin/admin)
## Route Protection — `canAccess()`
`canAccess()` là hàm **duy nhất** được export từ `accessControl.ts` (Container). Được dùng bởi:
- **`RouteGuard.tsx`** — Chặn truy cập route
- **`useSidebarMenu.ts`** — Lọc menu sidebar
### Thứ t ự ưu tiên kiểm tra
```mermaid
flowchart TD
A["User truy cập route"] --> B{"isPublic?"}
B -->|Yes| Z["✅ Cho phép"]
B -->|No| C{"isAuthenticated?"}
C -->|No| D["❌ Redirect /auth/login"]
C -->|Yes| E{"excludeRoles?"}
E -->|User role bị exclude| F["❌ 403 Access Denied"]
E -->|Không bị exclude| G{"superadminOnly?"}
G -->|Yes + là superadmin| Z
G -->|Yes + không phải superadmin| F
G -->|No| H{"Là superadmin?"}
H -->|Yes| Z["✅ Bypass tất cả"]
H -->|No| I{"allowedRoles?"}
I -->|Có + user role trong list| J{"requiredModule?"}
I -->|Có + user role KHÔNG trong list| F
I -->|Không set| J
J -->|null = luôn cho phép| Z
J -->|Có module key| K{"User có module này?"}
K -->|Yes| Z
K -->|No| F
RoutePermission interface
interface RoutePermission {
isPublic?: boolean; // Route public không cần xác thực
superadminOnly?: boolean; // Chỉ superadmin
excludeRoles?: string[]; // Ưu tiên CAO NHẤT — loại trừ roles
allowedRoles?: string[]; // Chỉ roles này được vào
requiredModule?: string; // Module key BE cấp (dùng MODULE_KEYS nested)
}
excludeRoles được kiểm tra trước cả superadmin bypass. Dùng để ẩn feature đang phát triển hoặc route không dành cho superadmin (ví dụ: superadmin không quản lý hàng hóa của từng doanh nghiệp).
Phân biệt: Khi nào dùng gì?
| Công cụ | Phạm vi | Khi nào dùng | Ví dụ |
|---|---|---|---|
role.utils (@vppos/core/utils) | Mọi nơi | Check vai trò thô (raw roles) để thay đổi UI/logic | isSuperAdmin(roles) → ẩn nút xóa hệ thống |
usePermission (@vppos/core/hooks) | Trong React component | Check module + permission chi tiết | can(PERMISSIONS.INVENTORY.PRODUCTS.PRODUCT.DELETE) → ẩn nút xóa sản phẩm |
canAccess (Container only) | Chỉ Container | Routing + sidebar filtering | RouteGuard, useSidebarMenu |
Token Sync
MFEs không quản lý token riêng. Token được đồng bộ từ Container:
// RemoteApp.tsx của mỗi MFE
function RemoteApp() {
useTokenSync(); // Lắng nghe token changes từ Container
// ...
}
Container lưu token vào localStorage với key từ STORAGE_KEYS.token. Hook usePermission đọc roles, modules, permissions từ localStorage — hỗ trợ cả chế độ impersonate (giả lập tài kho ản).