Files
solution-erp/.claude/skills/permission-matrix/SKILL.md
pqhuy1987 5113e4c771 [CLAUDE] Phase2: Form Engine MVP + docs (gotchas, skill, handoff)
Backend Forms:
- Domain/Forms: ContractTemplate (FormCode, Name, ContractType, FileName, StoragePath, Format, FieldSpec JSON, IsActive) + ContractClause
- EF config voi unique FormCode + query filter IsDeleted
- DbSets + IApplicationDbContext update
- Migration AddForms (bang 14 total)
- Packages: DocumentFormat.OpenXml 3.x + ClosedXML 0.105+
- Application/Forms:
  - IFormRenderer interface + RenderResult record
  - FormFeatures.cs: List/Get/Render CQRS
  - IWebHostEnvironmentLocator (abstract IWebHostEnvironment)
- Infrastructure/Forms:
  - DocxRenderer: OpenXml-based placeholder {{field}} replace, handle split runs (gom text tat ca <w:t> trong paragraph, replace, gan lai text dau + clear rest)
  - XlsxRenderer: ClosedXML cell value replace
  - FormRenderer router theo format docx/xlsx
- Api:
  - FormsController: GET /templates (filter type, onlyActive), GET /templates/{id}, POST /templates/{id}/render (return file)
  - WebHostEnvironmentLocator impl
- DbInitializer SeedContractTemplatesAsync: seed 8 template metadata, IsActive=true chi khi file ton tai

Templates vat ly:
- Copy 5 .docx/.xlsx tu FORM/ sang wwwroot/templates/
- 3 .doc (FO-002.02/03/06) chua convert: IsActive=false (Word COM bi stuck luc test, can retry voi DisplayAlerts=0 hoac LibreOffice)
- scripts/convert-doc-to-docx.ps1 (Word COM automation)

Frontend fe-admin:
- types/forms.ts: ContractTemplate + ContractTypeLabel
- pages/forms/FormsPage.tsx: list templates + Render dialog (paste JSON data → download .docx/.xlsx)
- Route /forms them vao App.tsx

Bug fix:
- SpaceProcessingModeValues namespace: wrap EnumValue<> full path
- SaveAs2($path, 16) thay vi SaveAs([ref], [ref]) — PowerShell type issue
- Word COM stuck: kill process, skip .doc cho MVP

Docs (theo yeu cau user):
- docs/gotchas.md MOI: 17 pitfalls nhom theo tech stack / EF Core / OpenXml / JSON / dev workflow
- .claude/skills/form-engine/SKILL.md: placeholder → full spec (algorithm + code pointers + API + limitations)
- .claude/skills/permission-matrix/SKILL.md: placeholder → full spec (BE policy + FE guard + seed + pitfalls)
- docs/HANDOFF.md MOI: brief 5 phut cho session sau (run quickstart + where we are + next steps + file tree + gotchas ref)
- docs/STATUS.md: update cumulative stats + next up Phase 3
- docs/changelog/migration-todos.md: tick Phase 2 iteration 1 items + add iteration 2 list
- docs/changelog/sessions/2026-04-21-1200-phase2-form-engine.md: session log
- CLAUDE.md root: them reference den gotchas + HANDOFF

E2E verified:
- GET /api/forms/templates (onlyActive=false) → 8 templates
- POST /api/forms/templates/{FO-002.05}/render voi data dict → HTTP 200 + file .docx 482KB (Microsoft Word 2007+ OK)

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

144 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: permission-matrix
description: Hệ thống phân quyền Role × MenuKey × CRUD. Seed 12 menu + admin full. FE PermissionGuard + usePermission. BE AuthorizationHandler + 48 policy. Dùng khi debug access denied, gán role, menu không hiện.
when-to-use:
- "permission denied"
- "access denied"
- "menu không hiện"
- "gán role cho user"
- "seed permission"
- "permission matrix edit"
---
# Permission Matrix Skill
> **Status:** Phase 1 đợt 2 IMPLEMENTED.
## Model
```
User ────< UserRoles ────< Role ────< Permissions ────< MenuItem
(RoleId, MenuKey, CRUD flags)
```
- 1 User có N Role (qua `AspNetUserRoles` rename → `UserRoles`)
- 1 Role có N Permission (1 row per MenuKey × 4 CRUD flag)
- Union (OR) nhiều role → user có quyền nếu **bất kỳ role nào** cho quyền đó
- Admin role → **bypass** check (luôn pass mọi policy)
## Menu tree (seed)
12 menu trong `MenuKeys.All`:
```
Dashboard
Master
├── Suppliers
├── Projects
└── Departments
Contracts
Forms
Reports
System
├── Users
├── Roles
└── Permissions
```
Tree hierarchy qua `ParentKey` field. Seed trong `DbInitializer.SeedMenuTreeAsync`.
## Code pointers
**Backend:**
- `Domain/Identity/MenuKeys.cs` — const class, single source of truth
- `Domain/Identity/MenuItem.cs` — entity (Key PK, Label, ParentKey, Order, Icon)
- `Domain/Identity/Permission.cs` — entity (RoleId, MenuKey, 4 flag)
- `Application/Permissions/Queries/GetMyMenuTree/GetMyMenuTreeQuery.cs` — resolve per-user, union OR, filter tree
- `Application/Permissions/PermissionFeatures.cs` — list/upsert
- `Api/Authorization/MenuPermissionRequirement.cs` + `MenuPermissionHandler.cs` — policy check
- `Api/Program.cs` — register 48 policy `{menu}.{action}` trong AddAuthorization
- `Infrastructure/Persistence/DbInitializer.cs``SeedMenuTreeAsync` + `SeedAdminPermissionsAsync`
- `Api/Controllers/MenusController.cs`, `RolesController.cs`, `PermissionsController.cs`
**Frontend (fe-admin):**
- `src/lib/menuKeys.ts` — const mirror, cần **đồng bộ tay** với BE
- `src/types/menu.ts` — MenuNode type
- `src/hooks/usePermission.ts``can(menuKey, action)` helper
- `src/components/PermissionGuard.tsx` — wrap button/content
- `src/components/Layout.tsx` — render sidebar động từ AuthContext.menu
- `src/pages/system/PermissionsPage.tsx` — ma trận edit UI
- `src/contexts/AuthContext.tsx``loadMenu()` on login + localStorage cache
## BE policy usage
Register trong Program.cs:
```csharp
services.AddAuthorization(opts =>
{
foreach (var menu in MenuKeys.All)
foreach (var action in MenuKeys.Actions)
opts.AddPolicy($"{menu}.{action}", p =>
p.Requirements.Add(new MenuPermissionRequirement(menu, action)));
});
services.AddScoped<IAuthorizationHandler, MenuPermissionHandler>();
```
Apply ở controller:
```csharp
[HttpPut("{id:guid}")]
[Authorize(Policy = "Contracts.Update")]
public async Task<IActionResult> Update(...) { }
```
## FE guard usage
```tsx
// Hook
const { can } = usePermission()
if (!can('Contracts', 'Update')) return null
// Component wrap
<PermissionGuard menuKey="Contracts" action="Update">
<Button>Sửa</Button>
</PermissionGuard>
// Route guard
<Route
path="/system/permissions"
element={
<PermissionGuard menuKey="Permissions" action="Read" fallback={<Forbidden />}>
<PermissionsPage />
</PermissionGuard>
}
/>
```
## Workflow — gán quyền cho role mới
1. Admin login → `/system/permissions`
2. Chọn role (vd "CostControl")
3. Tick checkbox trên matrix grid — mỗi lần tick tự động PUT `/api/permissions` upsert
4. User thuộc role đó logout/login lại → thấy permission mới (menu refresh từ `/api/menus/me`)
## Guard rules đã implement
- **Admin bypass:** role `Admin` luôn pass mọi policy (kể cả chưa seed row Permission)
- **Not user active:** `User.IsActive=false` → AuthorizationHandler return fail
- **Self-demote protection:** admin đang edit không thể giảm quyền role Admin (check trong `UpsertPermissionCommandHandler`)
## Common pitfalls
- **Quên refresh menu sau update permission** → user thấy menu cũ. Giải pháp: logout/login, hoặc Phase 3 thêm SignalR push.
- **MenuKey typo** — TS không check vì menu.key là string. Luôn dùng `MenuKeys.Contracts` const, không hardcode `"Contracts"`.
- **FE cache menu trong localStorage** → sau user được assign role mới, FE thấy menu cũ. Login lại fix.
- **Hai role conflict** (1 cho, 1 cấm): union OR → có ít nhất 1 role cho là được.
- **403 ở API nhưng FE không hide button** → FE guard chỉ UX, BE phải là source of truth. Phải apply `[Authorize(Policy = "X.Y")]` ở controller.
## Phase tiếp theo
- **Phase 3:** SignalR notify khi permission đổi → FE tự refetch `/api/menus/me`
- **Phase 4:** Per-user override (ngoài role) — thêm bảng `UserPermissionOverrides`
- **Phase 4:** Invalidate JWT khi role đổi (rare event, nhưng secure)