Chuyển tới nội dung chính

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
Khi nào dùng role.utils?

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"
thông tin

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á:

  1. Backend cập nhật quyền và cung cấp file permissions.ts (được sinh tự động từ tool rbacgen của hệ thống).
  2. Frontend có thể nhận file permissions.ts này và đặt vào thư mục gốc của dự án (hoặc bất kỳ đâu).
  3. Chạy lệnh:
    node scripts/gen-permissions.js [đường_dẫn_file_nếu_không_để_ở_gốc]
    Script này sẽ tự động đọc file taxonomy gốc, biên dịch thành cấu trúc object PERMISSIONS lồ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 ưu tiên cao nhất

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 viKhi nào dùngVí dụ
role.utils (@vppos/core/utils)Mọi nơiCheck vai trò thô (raw roles) để thay đổi UI/logicisSuperAdmin(roles) → ẩn nút xóa hệ thống
usePermission (@vppos/core/hooks)Trong React componentCheck module + permission chi tiếtcan(PERMISSIONS.INVENTORY.PRODUCTS.PRODUCT.DELETE) → ẩn nút xóa sản phẩm
canAccess (Container only)Chỉ ContainerRouting + sidebar filteringRouteGuard, 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).