Files
solution-erp/docs/flows/permission-flow.md
pqhuy1987 49a5f57a50 [CLAUDE] Docs: database-guide + 6 flow diagrams
docs/database/database-guide.md:
- Conventions (naming, data types, audit fields, soft delete)
- Schema hien tai (Identity tables sau migration Init) + seed 12 role + admin
- Schema planned: Phase 1 dot 2 (Supplier/Project/Department + Permission Matrix)
- Schema planned: Phase 3 (Contract + Approval + Comment + Attachment + Template + Clause + CodeSequence)
- Mermaid ERD cho tung phase
- Migration workflow (create/apply/revert)
- Index strategy + unique indexes
- Backup/restore SQL
- Common pitfalls + SQL cheatsheet

docs/flows/ — 6 flow documentation:
- README.md: index
- auth-flow.md: login/refresh/me/logout (IMPLEMENTED, sequence + edge cases + security checklist)
- permission-flow.md: Phase 1 dot 2 - Role x MenuKey x CRUD resolution + FE guard + BE policy
- contract-creation-flow.md: Phase 2 - Drafter flow chon template -> fill -> preview -> save draft
- contract-approval-flow.md: Phase 3 - state machine 9 phase chi tiet + reject flow + timeline UI
- form-render-flow.md: Phase 2 - OpenXml + ClosedXML + LibreOffice PDF convert
- sla-expiry-flow.md: Phase 3 - BackgroundService auto-approve qua SLA + warning notify

Update references:
- CLAUDE.md (root): them 2 row Tai lieu quan trong
- docs/CLAUDE.md: update project layout voi flows/ + database/
- docs/STATUS.md: log docs addition
- docs/changelog/migration-todos.md: tick Phase 0 docs items

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:15:28 +07:00

9.1 KiB
Raw Blame History

Permission Flow — Resolution Menu + CRUD

Status: 📝 Planned (Phase 1 đợt 2) Actors: System (resolution) | Admin (configure matrix) | Any user (guard apply)

1. Mô hình

3 layer resolution:

User
  └─ có nhiều Role (UserRoles table)
       └─ có Permission (Permissions table, row per MenuKey × CRUD)
            └─ gắn với MenuItem (tree Key → ParentKey)

Quyết định cuối cùng: union của tất cả permission từ mọi role của user. Nếu bất kỳ role nào CanRead=true cho MenuKey=Contracts → user được đọc.

2. Ma trận Permission

    MenuKey         Role.Admin  Role.Drafter  Role.CCM  Role.BOD
    ─────────────────────────────────────────────────────────────
    Dashboard       R           R             R         R
    Contracts       CRUD        CR (self)     RU        RU
    Suppliers       CRUD        R             R         R
    Projects        CRUD        R             R         R
    Users           CRUD        —             —         —
    Roles           CRUD        —             —         —
    Permissions     CRUD        —             —         —
    Reports         R           —             R         R

(Chi tiết đầy đủ role × menu mapping ở ../workflow-contract.md §5)

3. Flow — user login → FE resolve menu

sequenceDiagram
    actor U as User
    participant FE as Frontend
    participant API as MenusController.GetMyTree
    participant Q as GetMyMenuTreeQueryHandler
    participant CU as ICurrentUser
    participant DB

    U->>FE: Login thành công (có token)
    FE->>API: GET /api/menus/me
    API->>Q: Send(GetMyMenuTreeQuery)

    Q->>CU: UserId + Roles
    CU-->>Q: userId, roles[]

    Q->>DB: SELECT Permissions<br/>JOIN Roles ON Permissions.RoleId = Roles.Id<br/>JOIN MenuItems ON Permissions.MenuKey = MenuItems.Key<br/>WHERE Roles.Id IN (user's roles)

    DB-->>Q: raw rows

    Q->>Q: Group by MenuKey<br/>UNION CRUD flags (OR)<br/>Build tree (Key → ParentKey)

    Q-->>API: MenuTreeDto[] with resolved CRUD per node
    API-->>FE: 200

    FE->>FE: Cache in AuthContext + localStorage
    FE->>FE: Render sidebar (filter nodes có CanRead=true)

Example response:

[
  {
    "key": "Dashboard",
    "label": "Tổng quan",
    "icon": "LayoutDashboard",
    "order": 1,
    "parentKey": null,
    "canRead": true,
    "canCreate": false,
    "canUpdate": false,
    "canDelete": false,
    "children": []
  },
  {
    "key": "Master",
    "label": "Danh mục",
    "icon": "Database",
    "order": 2,
    "parentKey": null,
    "children": [
      {
        "key": "Suppliers",
        "label": "Nhà cung cấp",
        "parentKey": "Master",
        "canRead": true, "canCreate": true, "canUpdate": true, "canDelete": false,
        "children": []
      }
    ]
  }
]

4. Flow — FE guard render

flowchart TD
    Start([Component mount]) --> CheckMenu{usePermission<br/>can 'Contracts', 'Update'?}
    CheckMenu -->|no permission| Hide[Không render nút 'Sửa']
    CheckMenu -->|has permission| Show[Render nút 'Sửa']
    Show --> ClickEdit[User click 'Sửa']
    ClickEdit --> CallAPI[PATCH /api/contracts/:id]
    CallAPI --> BEGuard{Controller<br/>Authorize 'Contracts.Update'?}
    BEGuard -->|no| Reject[403 Forbidden]
    BEGuard -->|yes| Proceed[Update DB + return 200]

FE pattern (sẽ implement Phase 1 đợt 2):

// usePermission.ts
export function usePermission() {
  const { menu } = useAuth()  // menu cached từ login
  return {
    can: (menuKey: string, action: 'Read' | 'Create' | 'Update' | 'Delete') => {
      const node = findInTree(menu, menuKey)
      return node?.[`can${action}`] ?? false
    },
  }
}

// PermissionGuard.tsx
<PermissionGuard menuKey="Contracts" action="Update">
  <Button>Sửa</Button>
</PermissionGuard>

// Route guard
<Route
  path="/admin/permissions"
  element={
    <PermissionGuard menuKey="Permissions" action="Read" fallback={<Forbidden />}>
      <PermissionMatrixPage />
    </PermissionGuard>
  }
/>

5. Flow — Admin configure permission matrix

sequenceDiagram
    actor A as Admin
    participant FE as Permission Matrix Page
    participant API as PermissionsController
    participant M as UpsertPermissionCommandHandler
    participant DB

    A->>FE: Mở /admin/permissions
    FE->>API: GET /api/permissions?roleId={id}
    API-->>FE: Array permissions (row per menuKey)

    A->>FE: Tick checkbox "Contracts.CanUpdate = true" cho role Drafter
    FE->>FE: Optimistic update UI

    FE->>API: PUT /api/permissions<br/>{roleId, menuKey: "Contracts", canRead, canCreate, canUpdate, canDelete}
    API->>M: Send(UpsertPermissionCommand)

    M->>DB: SELECT existing Permission<br/>WHERE RoleId=? AND MenuKey=?
    alt Exists
        M->>DB: UPDATE flags
    else Not exists
        M->>DB: INSERT new row
    end

    M-->>API: 204
    API-->>FE: 204

    Note over FE,A: User với role Drafter<br/>sẽ thấy nút Update HĐ<br/>sau khi họ refresh/re-login<br/>(hoặc SignalR notify Phase 3)

Ghi chú quan trọng:

  • Permission update KHÔNG real-time đến user đang online ở Phase 1 đợt 2 — họ phải logout/login để thấy.
  • Phase 3 có thể thêm SignalR notify "permission changed" → FE auto refetch /api/menus/me.
  • Phase 4 có thể invalidate JWT khi permission đổi (rare change, nhưng secure).

6. Backend guard

// Api/Controllers/ContractsController.cs
[HttpPut("{id}")]
[Authorize(Policy = "Contracts.Update")]  // custom policy
public async Task<IActionResult> Update(Guid id, UpdateContractCommand cmd)
{
    // ...
}

Custom policy registration (Program.cs):

services.AddAuthorization(opts =>
{
    foreach (var menu in MenuKeys.All)
    {
        foreach (var action in new[] { "Read", "Create", "Update", "Delete" })
        {
            opts.AddPolicy($"{menu}.{action}", p =>
                p.Requirements.Add(new MenuPermissionRequirement(menu, action)));
        }
    }
});
services.AddSingleton<IAuthorizationHandler, MenuPermissionHandler>();

Handler:

public class MenuPermissionHandler : AuthorizationHandler<MenuPermissionRequirement>
{
    private readonly IApplicationDbContext _db;
    // ...
    protected override async Task HandleRequirementAsync(...)
    {
        var userId = context.User.GetUserId();
        var hasPermission = await _db.Permissions
            .Where(p => p.Role.Users.Any(u => u.Id == userId))
            .Where(p => p.MenuKey == req.MenuKey)
            .AnyAsync(p => req.Action switch
            {
                "Read" => p.CanRead,
                "Create" => p.CanCreate,
                "Update" => p.CanUpdate,
                "Delete" => p.CanDelete,
                _ => false,
            });
        if (hasPermission) context.Succeed(req);
    }
}

7. Seed mặc định (Phase 1 đợt 2)

Seed trong DbInitializer.InitializeAsync:

  1. Menu tree seed — từ MenuKeys.cs const class (Phase 1 đợt 2):

    Dashboard
    Master
      ├── Suppliers
      ├── Projects
      └── Departments
    Contracts
    Forms
    Approvals
    Reports
    System
      ├── Users
      ├── Roles
      └── Permissions
    
  2. Default permissions:

    • Admin role → full CRUD mọi menu
    • Các role khác → chỉ Read mặc định, admin config thêm sau qua UI

8. Edge cases

Case Xử lý
User có 0 role Chỉ thấy Dashboard (nếu mở cho anonymous), không vào được menu khác
Role bị xóa FK ON DELETE CASCADE xóa permissions liên quan
Menu bị remove khỏi MenuKeys.cs Seed job cảnh báo + giữ orphan permissions (dev fix manual)
Admin tự xóa quyền admin của mình Check trong UpsertPermissionCommand — nếu target role là Admin + current user đang ở role Admin → throw ForbiddenException("Không thể tự xóa quyền admin")
User có nhiều role conflict (1 role cho, 1 role cấm) Union (OR) — có ít nhất 1 role cho là được

9. Performance

  • Menu tree cache trong AuthContext sau login → không hit API mỗi navigate
  • /api/menus/me response size ~5KB gzipped (với ~30 menu nodes)
  • Authorization handler cache scope request (1 query / request) — EF Core auto cache
  • Phase 4 optimize: Redis distributed cache cho permission matrix (nếu >100 concurrent users)

10. Testing checklist (Phase 1 đợt 2)

  • Admin login → thấy tất cả menu
  • Tạo user role Drafter only → chỉ thấy menu Contracts + self HĐ
  • Tạo role tùy chỉnh "CCM Reviewer" chỉ read Contracts + read Reports → verify không thấy Master menu
  • User có 2 role (Drafter + Finance) → thấy union của cả 2
  • Admin xóa quyền Update của role Drafter → user Drafter refresh → không thấy nút Sửa
  • Backend 403 khi FE bypass (dev tools unhide nút) → gọi API trực tiếp bị chặn