# 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`](../workflow-contract.md))

## 3. Flow — user login → FE resolve menu

```mermaid
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:**

```json
[
  {
    "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

```mermaid
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):**

```tsx
// 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

```mermaid
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

```csharp
// 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):**

```csharp
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:**

```csharp
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
