[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>
This commit is contained in:
289
docs/flows/permission-flow.md
Normal file
289
docs/flows/permission-flow.md
Normal file
@ -0,0 +1,289 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user