[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:
pqhuy1987
2026-04-21 11:15:28 +07:00
parent 702411fcc8
commit 49a5f57a50
12 changed files with 1982 additions and 2 deletions

View 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