Files
solution-erp/docs/changelog/migration-todos.md
pqhuy1987 1b5ef2ed51 [CLAUDE] Phase5.1/3.2: IDOR filter + SLA auto-approve job + admin password warning
IDOR filter ContractsController:
- ListContractsQueryHandler + ICurrentUser: non-admin chi thay HD minh la Drafter hoac role eligible phase hien tai
- GetContractQueryHandler + ICurrentUser: throw ForbiddenException neu truy cap HD khong lien quan
- GetEligiblePhases() internal static trong ListContractsQueryHandler — mirror GetMyInboxQueryHandler.PhaseActorRoles (Drafter/DeptManager → DangSoanThao/DangDamPhan/DangInKy, ProjectManager+PRO+CCM+FIN+ACT+EQU → DangGopY, CostControl → DangKiemTraCCM, Director+AuthorizedSigner → DangTrinhKy, HrAdmin → DangDongDau)

SLA Expiry BackgroundService (Phase 3 iteration 2 partial):
- Infrastructure/HostedServices/SlaExpiryJob MOI: BackgroundService moi 15 phut (delay 30s startup)
- Query Contracts WHERE SlaDeadline < UtcNow AND Phase NOT IN (DaPhatHanh, TuChoi)
- Map phase → next (happy path). Goi IContractWorkflowService.TransitionAsync voi actorUserId=null + Decision=AutoApprove + comment 'AUTO: het SLA phase X (Nh qua han)'
- Try-catch tung contract, 1 fail khong block batch
- Log structured: 'SlaExpiryJob: auto-approved contract {Id} {From} → {To}'
- Package Microsoft.Extensions.Hosting added to Infrastructure
- DI register AddHostedService<SlaExpiryJob>

Admin password warning (Phase 5.1):
- DbInitializer.WarnDefaultAdminPasswordAsync: check CheckPasswordAsync voi AdminPassword default → log WRN '⚠️  Admin user vẫn dùng password mặc định. ĐỔI NGAY trong production!'
- Chain vao InitializeAsync sau cac seed

E2E verified:
- Admin GET /contracts → total 1 (see all)
- Drafter GET /contracts → total 0 (IDOR filter, chua tao HD nao)
- API startup log: '⚠️  Admin user admin@solutionerp.local vẫn dùng password mặc định'
- Build + TS check → pass

Docs:
- STATUS.md: Phase 5.1 hau nhu xong (IDOR + admin warning + SLA job tick), cumulative BE 3900 LOC
- migration-todos.md: tick Phase 5.1 IDOR + admin warning, Phase 3 iter 2 SlaExpiryJob + E2E non-admin + admin warning
- session log 2026-04-21-1730-idor-sla-job.md

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

235 lines
14 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.

# Migration To-dos — Atomic Roadmap
> Mỗi item là 1 task atomic (~2-8h work). Tick `[x]` khi xong. Link session log nếu có.
## Phase 0 — Draft Scaffold (T1)
- [x] Tạo cấu trúc thư mục `SOLUTION_ERP/`
- [x] Scaffold .NET 10 solution `SolutionErp.slnx`
- [x] Scaffold 4 project: `SolutionErp.{Domain, Application, Infrastructure, Api}`
- [x] Wire Clean Arch references (Api → App/Infra, Infra → App, App → Domain)
- [x] Install NuGet base: MediatR, FluentValidation, AutoMapper, EF Core SqlServer, Identity, JWT, Swagger, Serilog
- [x] Scaffold 2 React + Vite apps `fe-admin` + `fe-user` với TS template
- [x] Config vite.config.ts: port, strictPort, proxy `/api`, alias `@`
- [x] Pin Node `>=20` trong package.json + `.nvmrc` cho CI
- [x] Parse 8 form → `docs/forms-spec.md`
- [x] Parse quy trình → `docs/workflow-contract.md`
- [x] Viết `docs/{CLAUDE,STATUS,PROJECT-MAP}.md`
- [x] Viết `docs/database/database-guide.md` (conventions + schema + ERD + migration workflow)
- [x] Viết `docs/flows/` — README + 6 flow doc (auth, permission, contract-create, contract-approve, form-render, sla-expiry)
- [x] Viết `.gitignore`, `README.md`, `global.json`, `docker-compose.yml`
- [x] Tạo placeholder skill folders: `contract-workflow`, `form-engine`, `permission-matrix`
- [x] `git init` + commit đầu (`25dad7f`)
- [ ] Push Gitea remote (chờ URL từ user)
## Phase 1 — Alpha Core (T2-4)
### Foundation (đã xong Session 2)
- [x] `Domain/Common/BaseEntity.cs` (Id Guid, CreatedAt, UpdatedAt, CreatedBy, UpdatedBy)
- [x] `Domain/Common/AuditableEntity.cs` (IsDeleted, DeletedAt, DeletedBy)
- [x] `Domain/Contracts/` Enums: `ContractType`, `ContractPhase` (9 state), `ApprovalDecision`
- [x] `Domain/Identity/User.cs` (IdentityUser<Guid> + FullName + RefreshToken + IsActive)
- [x] `Domain/Identity/Role.cs` (IdentityRole<Guid> + Description)
- [x] `Domain/Identity/AppRoles.cs` — 12 role constants
- [x] `Application/Common/Interfaces/`: IApplicationDbContext, ICurrentUser, IDateTime, IJwtTokenService
- [x] `Application/Common/Exceptions/*`
- [x] `Application/Common/Behaviors/ValidationBehavior.cs`
- [x] `Application/DependencyInjection.cs` — MediatR + FluentValidation
- [x] `Infrastructure/Persistence/ApplicationDbContext.cs : IdentityDbContext`
- [x] `Infrastructure/Persistence/Interceptors/AuditingInterceptor.cs`
- [x] `Infrastructure/Persistence/DbInitializer.cs` — seed 12 role + admin
- [x] `Infrastructure/Persistence/DesignTimeDbContextFactory.cs`
- [x] `Infrastructure/Identity/{JwtSettings, JwtTokenService}.cs`
- [x] `Infrastructure/Services/DateTimeService.cs`
- [x] `Infrastructure/DependencyInjection.cs`
- [x] `Api/Services/CurrentUserService.cs`
- [x] `Api/Middleware/GlobalExceptionMiddleware.cs`
- [x] `Api/Controllers/AuthController.cs` (login, refresh, me, logout)
- [x] `Api/Program.cs` (Serilog, JWT, CORS, Swagger, middleware)
- [x] `Api/appsettings.{json, Development.json}` + `launchSettings.json` (port 5443)
- [x] Migration 1 `Init` + apply to `SolutionErp_Dev` LocalDB
- [x] FE: Vite config (Tailwind 4 + proxy + alias)
- [x] FE: `src/{index.css, lib/api.ts, lib/cn.ts, types/auth.ts}` cho 2 app
- [x] FE: `src/contexts/AuthContext.tsx`, `components/{ProtectedRoute, Layout}.tsx`
- [x] FE: `components/ui/{Button, Input, Label}.tsx`
- [x] FE: `pages/LoginPage.tsx`, `pages/DashboardPage.tsx` (admin) + `pages/InboxPage.tsx` (user)
- [x] FE: `App.tsx` với Router + AuthProvider + Toaster
- [x] FE: `main.tsx` với QueryClient (TanStack Query)
- [x] E2E verified: login qua Vite proxy cả 2 app → JWT + user info
### Phase 1 đợt 2 — CRUD master + Permission Matrix (sắp tới)
- [x] `Domain/Master/Supplier` (+ SupplierType enum 5 loại) / `Project` / `Department` (AuditableEntity)
- [x] EF `IEntityTypeConfiguration<T>` cho mỗi entity (unique Code + query filter IsDeleted)
- [x] CQRS CRUD: Create/Update/Delete/GetById/List (PagedResult) cho 3 entity
- [x] `Api/Controllers/{SuppliersController, ProjectsController, DepartmentsController}`
- [x] Migration 2: `AddMasterData`
- [x] `Domain/Identity/MenuItem` (Key PK, Label, ParentKey, Order, Icon) + `MenuKeys` const class
- [x] `Domain/Identity/Permission` (RoleId, MenuKey, CanRead/Create/Update/Delete)
- [x] Seed default menu tree (12 menu) + admin full access trong DbInitializer
- [x] `Application/Permissions/Queries/GetMyMenuTreeQuery` — resolve per-user, union OR, tree filter
- [x] `Api/Controllers/{MenusController, RolesController, PermissionsController}`
- [x] Migration 3: `AddPermissions`
- [x] Authorization handler `MenuPermissionHandler` + register 48 policy `{menu}.{action}`
- [ ] `Domain/Entities/Contract` skeleton (Id, Type, SupplierId, ProjectId, Phase=DangChon, DraftData JSON) — deferred Phase 2/3
- [ ] Contract CRUD draft only (không workflow Phase 3) — deferred
- [x] FE: `<PermissionGuard menuKey="Suppliers" action="Update">` + `usePermission()` hook
- [x] FE Admin: 3 trang CRUD Supplier/Project/Department với DataTable + Dialog modal + search/sort/paging
- [x] FE Admin: Permission Matrix grid page (role × menu × CRUD checkbox)
- [x] FE Admin: Layout menu động từ `/api/menus/me`
- [ ] FE User: trang "HĐ của tôi" list + filter — Phase 3
- [ ] FE Admin: Users management page (tạo user + gán role) — sắp tới
- [ ] FE Admin: Roles CRUD — sắp tới
- [ ] Route guard theo role admin-only — có PermissionGuard ở button, route cần thêm
### Exit criteria Phase 1
- [ ] Admin login → tạo NCC/Project → tạo role "Nhân viên CCM" → gán permission menu "Contracts.Read"
- [ ] User CCM login → thấy menu Contracts, không thấy menu Admin
- [ ] Tạo Contract draft → list hiển thị, không bị 403 sai
## Phase 2 — Form Engine (T5-6)
### MVP xong (Phase 2 iteration 1)
- [x] Khảo sát: chọn **OpenXml + ClosedXML** (free, không cần license)
- [x] `Domain/Forms/ContractTemplate` (Id, FormCode, Name, ContractType, FileName, StoragePath, Format, FieldSpec JSON, IsActive)
- [x] `Domain/Forms/ContractClause` skeleton
- [x] EF config + Migration `AddForms`
- [x] `Application/Forms/Services/IFormRenderer` interface
- [x] `Infrastructure/Forms/DocxRenderer` (OpenXml, handle placeholder split runs)
- [x] `Infrastructure/Forms/XlsxRenderer` (ClosedXML)
- [x] `Application/Forms/FormFeatures.cs` — List/Get/Render CQRS
- [x] `Api/Controllers/FormsController` — GET templates, GET single, POST render
- [x] Copy 5 .docx/.xlsx template → `wwwroot/templates/`
- [x] Seed 8 ContractTemplate rows (5 IsActive=true, 3 chờ convert)
- [x] FE admin: `FormsPage` — list + render dialog điền JSON + download
- [x] E2E verified: render FO-002.05 → file .docx 482KB mở được bằng Word
### Iteration 2 (optional — enhance)
- [ ] Convert 3 file `.doc``.docx` (retry Word COM với `DisplayAlerts=0` + timeout, hoặc LibreOffice headless)
- [ ] Parse chi tiết field của 5 template HĐ — mỗi form thành JSON `FieldSpec`
- [ ] Support `{{#loop}}...{{/loop}}` block cho table lặp (hạng mục HĐ giao khoán, PO)
- [ ] FE user: form builder dynamic — render từ fieldSpec thay vì điền JSON tay
- [ ] FE admin: upload template mới qua UI (POST multipart) + edit field mapping
- [ ] Lưu `ContractClause` (FO-002.04) dạng rich text, admin edit qua TipTap/TinyMCE
- [ ] PDF convert via LibreOffice headless (`soffice --headless --convert-to pdf`)
- [ ] Import/export template (backup/restore)
- [ ] Format helpers: number → `150,000,000 VND`, date → `dd/MM/yyyy`
- [ ] Content preservation test: render → diff layout với template gốc
## Phase 3 — Workflow State Machine (T7-9)
### MVP xong (iteration 1)
- [x] `Domain/Contracts/Contract` (Phase, SlaDeadline, BypassProcurementAndCCM, MaHopDong, DraftData, SlaWarningSent)
- [x] `Domain/Contracts/ContractApproval` (FromPhase, ToPhase, ApproverUserId, Decision, Comment)
- [x] `Domain/Contracts/ContractComment` + `ContractAttachment` (+ AttachmentPurpose enum)
- [x] `Domain/Contracts/ContractCodeSequence` (Prefix PK, LastSeq)
- [x] EF config + unique MaHopDong filtered + indexes Phase/Supplier/Project/SlaDeadline + cascade delete
- [x] DbSets (5) + `IApplicationDbContext` update
- [x] Migration `AddContractsWorkflow`
- [x] `Application/Contracts/Services/IContractWorkflowService` + `IContractCodeGenerator`
- [x] `Infrastructure/Services/ContractWorkflowService` — adjacency 9 phase + role guard + Admin bypass + system actor + bypass CCM (Chủ đầu tư)
- [x] `Infrastructure/Services/ContractCodeGenerator` — 7 format RG-001 + transaction SERIALIZABLE
- [x] CQRS: Create/UpdateDraft/Transition/AddComment/List/Inbox/GetDetail/Delete (8 feature)
- [x] `Api/Controllers/ContractsController` — 8 endpoint REST
- [x] FE admin: ContractsListPage + ContractDetailPage (timeline + action dialog)
- [x] FE user: InboxPage + ContractCreatePage + ContractDetailPage + MyContractsPage
- [x] PhaseBadge component + color map
- [x] E2E verified: tạo HĐ → chạy 9 phase → gen mã `FLOCK 01/HĐGK/SOL&PVL2026/01`
### Iteration 2 (polish)
- [x] `Infrastructure/HostedServices/SlaExpiryJob` — check mỗi 15min, auto-approve quá hạn với Decision=AutoApprove (+30s delay startup)
- [x] E2E test với non-admin user (Drafter role) — IDOR filter verified
- [x] Admin password warning log khi vẫn dùng default
- [ ] Warning notification khi còn 20% SLA (track `SlaWarningSent` flag đã có)
- [ ] `Infrastructure/Services/NotificationService` — email (MailKit) + in-app
- [ ] SignalR hub cho real-time notification badge
- [ ] MediatR `AuditBehavior` — log mọi command (ngoài ContractApprovals)
- [ ] Upload attachment endpoint (multipart) + FE upload UI (`wwwroot/uploads/contracts/{id}/`)
- [ ] RowVersion optimistic concurrency (2 user race → 409)
- [ ] Render HĐ docx lúc tạo (merge TemplateId + DraftData + ContractClause appendix)
- [ ] Filter Inbox theo phase ở FE
- [ ] E2E test: reject → quay về DangSoanThao
- [ ] E2E test: SLA expired → auto-approve + log (test thật qua set SlaDeadline past)
## Phase 4 — Reporting + Polish (T10-11)
### MVP xong (iteration 1)
- [x] Dashboard admin: 5 KPI (total/active/overdue/published this month/total value) + by phase + top 5 NCC + top 5 dự án + 12 tháng
- [x] Excel export HĐ theo filter (phase/supplier/project/date range) qua ClosedXML
- [x] BE `GetDashboardStatsQuery` + `ExportContractsToExcelCommand` + ReportsController
- [x] FE `DashboardPage` rewrite với `BarChart` tự build (Tailwind only, không thư viện ngoài)
- [x] FE `ReportsPage` filter + export
- [x] Docs consolidation: `rules.md` + `architecture.md` + `database/schema-diagram.md` + gotchas update
### Iteration 2 (polish — optional)
- [ ] SLA overdue report (by role / phase, export Excel)
- [ ] Contract audit log export (từng HĐ ra PDF)
- [ ] Dashboard user-specific (HĐ của tôi / role của tôi)
- [ ] Chart library recharts (nếu cần chart phức tạp)
- [ ] UX polish: skeleton loader cho mọi list, empty state có action, error boundary recovery
- [ ] Accessibility: keyboard nav, focus trap modal, aria labels
- [ ] Dark mode
- [ ] Performance: explicit index DB cho query hot đã identify
- [ ] Tài liệu user guide: quy trình tạo HĐ + duyệt
- [ ] UAT với 5-10 HĐ dữ liệu thật từ bộ phận Cung ứng
## Phase 5 — Production (T12-13)
### Prep xong (code + scripts + docs)
- [x] `docs/guides/cicd.md` — CI/CD runbook
- [x] Gitea Actions workflow `.gitea/workflows/deploy.yml` — build .NET + 2 FE, deploy IIS qua WinRM
- [x] Pin Node `.nvmrc` 20 (gotcha #5)
- [x] `scripts/deploy-iis.ps1` — stop pool → backup → xcopy → start → health check
- [x] `scripts/backup-sql.ps1` — daily BACKUP DATABASE + COMPRESSION + retention 30d
- [x] `appsettings.Production.json` template + user secrets pattern
- [x] Rate limiting (built-in .NET 10) — auth-login 5/min + global 300/min
- [x] Health check `/health/live` + `/health/ready` (DB probe)
- [x] Serilog File sink rolling daily retention 30d
- [x] HSTS production (1 year)
- [x] `docs/guides/deployment-iis.md` — setup lần đầu + troubleshooting
- [x] `docs/guides/security-checklist.md` — OWASP top 10
- [x] `docs/guides/runbook.md` — operations (restart, rollback, restore)
- [x] FE refresh token auto interceptor (queue pattern cả 2 app)
### Deploy thật (cần Gitea URL)
- [ ] Windows Server setup: IIS + URL Rewrite + ARR (reverse proxy FE → IIS)
- [ ] SQL Server prod + Task Scheduler trigger backup-sql.ps1
- [ ] HTTPS certificate (Let's Encrypt qua win-acme)
- [ ] Gitea remote setup + push all commits
- [ ] Set 5 Gitea Actions secrets (IIS_HOST/USER/PASSWORD/JWT_SECRET/DB_CONNECTION)
- [ ] Enable Gitea runner (Windows + Ubuntu)
- [ ] Test CI/CD workflow lần đầu staging
- [ ] UAT production 1 tuần với 2-3 user thật
- [ ] Go-live checklist: backup, rollback plan, on-call contact
### Phase 5.1 Security hardening + Users Mgmt
- [x] Security headers middleware (X-Content-Type-Options, X-Frame-Options, CSP, Referrer-Policy, Permissions-Policy)
- [x] Identity account lockout (5 fail → 15min, config-driven)
- [x] Password policy config-driven (default 8 dev, override `Identity:Password:RequiredLength` prod)
- [x] LoginCommand: check IsLockedOutAsync + AccessFailedAsync + reset on success
- [x] BE Users management: CQRS 8 feature + UsersController 7 endpoint (Users.Read/Create/Update policies)
- [x] FE admin `/system/users`: list + create + assign roles + reset password + unlock + toggle active
- [x] IDOR check ContractsController — user Drafter chỉ xem HĐ mình tạo hoặc role eligible phase (`ListContractsQueryHandler` + `GetContractQueryHandler`)
- [x] Admin mặc định warning log (`DbInitializer.WarnDefaultAdminPasswordAsync`)
- [ ] Dependencies scan vào CI (`dotnet list package --vulnerable --include-transitive`, `npm audit --audit-level=high`)
- [ ] BE Roles CRUD (Create/Rename/Delete custom role) + FE `/system/roles` — optional, 12 role seed đủ dùng
## Post-launch (Phase 6+ — future)
- [ ] E-signature integration (VNPT CA hoặc FPT CA)
- [ ] Tích hợp Bravo / SAP ERP import NCC
- [ ] Mobile app (React Native?) cho BOD duyệt ngoài giờ
- [ ] AI: gợi ý điền form dựa HĐ cũ, OCR scan HĐ đối tác
- [ ] Multi-tenant nếu có công ty thứ 2