From 1b5ef2ed515db64e690e9598c199e92605425a8d Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Tue, 21 Apr 2026 13:15:14 +0700 Subject: [PATCH] [CLAUDE] Phase5.1/3.2: IDOR filter + SLA auto-approve job + admin password warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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) --- docs/STATUS.md | 19 ++-- docs/changelog/migration-todos.md | 17 +-- .../sessions/2026-04-21-1730-idor-sla-job.md | 77 +++++++++++++ .../Contracts/ContractFeatures.cs | 48 +++++++- .../DependencyInjection.cs | 4 + .../HostedServices/SlaExpiryJob.cs | 106 ++++++++++++++++++ .../Persistence/DbInitializer.cs | 14 +++ .../SolutionErp.Infrastructure.csproj | 1 + 8 files changed, 267 insertions(+), 19 deletions(-) create mode 100644 docs/changelog/sessions/2026-04-21-1730-idor-sla-job.md create mode 100644 src/Backend/SolutionErp.Infrastructure/HostedServices/SlaExpiryJob.cs diff --git a/docs/STATUS.md b/docs/STATUS.md index 9e1dd31..a05ba3a 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -2,9 +2,9 @@ > **Update rule:** trước khi bắt đầu 1 task → ghi row vào `🔥 In Progress`. Xong → chuyển sang `✅ Recently Done`. -**Last updated:** 2026-04-21 16:30 +**Last updated:** 2026-04-21 17:30 -## 📍 Phase hiện tại: **Phase 5.1 Security + Users Mgmt xong** — chờ Gitea URL để deploy Phase 5 prod +## 📍 Phase hiện tại: **IDOR + SLA Job xong** — gần đủ feature, chờ Gitea URL cho Phase 5 deploy prod ## 🔥 In Progress @@ -14,7 +14,8 @@ _(không có)_ | Ngày | Ai | Task | Commit | |---|---|---|---| -| 2026-04-21 | Claude | **Phase 5.1 Security + Users Mgmt** — Security headers middleware (CSP, X-Frame-Options, Referrer-Policy, Permissions-Policy) + Identity account lockout (5 fail → 15min) + LoginHandler check IsLockedOut + AccessFailedAsync. BE Users CQRS 8 feature + UsersController 7 endpoint. FE admin `/system/users` — list + create + gán role + reset password + unlock + toggle active | (sắp commit) | +| 2026-04-21 | Claude | **IDOR + SLA Job + Admin warning** — ContractsController List/GetDetail filter theo role (non-admin chỉ thấy HĐ mình là Drafter hoặc role eligible phase). SlaExpiryJob BackgroundService auto-approve quá hạn mỗi 15min với Decision=AutoApprove. DbInitializer warn log khi admin vẫn dùng password default | (sắp commit) | +| 2026-04-21 | Claude | **Phase 5.1 Security + Users Mgmt** — Security headers + Identity lockout + LoginHandler check + Users CQRS + UsersController + FE `/system/users` | `11e61c9` | | 2026-04-21 | Claude | **Phase 5 Prep** — BE rate limit + health check + Serilog file + HSTS + scripts deploy-iis/backup-sql + .gitea/workflows/deploy.yml + 4 guides + FE refresh token queue pattern | `46a2cab` | | 2026-04-21 | Claude | **Phase 4 Report MVP + Docs Consolidation** — Dashboard KPI + Excel export + rules.md + architecture.md + schema-diagram.md + gotchas update 26 pitfalls | `fe7ad8e` | | 2026-04-21 | Claude | **Phase 3 Workflow MVP** — 9 phase state machine + gen mã HĐ RG-001 | `7e957a7` | @@ -47,16 +48,18 @@ Session logs: [P0](changelog/sessions/2026-04-21-1045-phase0-scaffold.md) · [P1 - [ ] Smoke test end-to-end prod - [ ] UAT 1 tuần 2-3 user thật -### Phase 5.1 Security — xong gần hết +### Phase 5.1 Security — hầu như xong - [x] Security headers middleware (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, CSP) - [x] Identity account lockout (5 fail → 15min, config-driven) -- [x] Password policy config-driven (default 8 dev, override prod `Identity:Password:RequiredLength`) +- [x] Password policy config-driven - [x] LoginHandler check lockout + AccessFailedAsync + reset on success -- [x] BE Users management + FE admin UsersPage (tạo user test permission non-admin) -- [ ] IDOR check ContractsController (user không xem HĐ không liên quan) +- [x] BE Users management + FE admin UsersPage +- [x] IDOR check ContractsController (non-admin chỉ thấy HĐ mình/role eligible) +- [x] Admin password warning log startup +- [x] SLA Expiry BackgroundService auto-approve - [ ] Dependencies scan CI (`dotnet list package --vulnerable` + `npm audit`) -- [ ] Admin mặc định warning log force đổi password +- [ ] Roles CRUD — optional ### Polish iterations diff --git a/docs/changelog/migration-todos.md b/docs/changelog/migration-todos.md index d5664a7..fb744f5 100644 --- a/docs/changelog/migration-todos.md +++ b/docs/changelog/migration-todos.md @@ -141,20 +141,21 @@ - [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 — optional) +### Iteration 2 (polish) -- [ ] `Infrastructure/HostedServices/SlaExpiryJob` — check mỗi 15min, auto-approve quá hạn với Decision=AutoApprove -- [ ] Warning notification khi còn 20% SLA +- [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) -- [ ] E2E test với non-admin user (Drafter/CCM/BOD role) - [ ] Filter Inbox theo phase ở FE - [ ] E2E test: reject → quay về DangSoanThao -- [ ] E2E test: SLA expired → auto-approve + log +- [ ] E2E test: SLA expired → auto-approve + log (test thật qua set SlaDeadline past) ## Phase 4 — Reporting + Polish (T10-11) @@ -219,10 +220,10 @@ - [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 -- [ ] IDOR check ContractsController — user Drafter chỉ xem HĐ mình tạo hoặc có role giữ phase +- [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`) -- [ ] Admin mặc định: warning log force đổi password -- [ ] BE Roles CRUD (Create/Rename/Delete custom role) + FE `/system/roles` +- [ ] BE Roles CRUD (Create/Rename/Delete custom role) + FE `/system/roles` — optional, 12 role seed đủ dùng ## Post-launch (Phase 6+ — future) diff --git a/docs/changelog/sessions/2026-04-21-1730-idor-sla-job.md b/docs/changelog/sessions/2026-04-21-1730-idor-sla-job.md new file mode 100644 index 0000000..49b742b --- /dev/null +++ b/docs/changelog/sessions/2026-04-21-1730-idor-sla-job.md @@ -0,0 +1,77 @@ +# Session 2026-04-21 17:30 — IDOR filter + SLA auto-approve job + admin warning + +**Dev:** Claude (Opus 4.7) +**Duration:** ~45m +**Base commit:** `11e61c9` + +## Làm được + +### Chunk X — IDOR filter ContractsController + +- `ListContractsQueryHandler`: thêm `ICurrentUser` dep. Non-admin user chỉ thấy: + - HĐ mình là `DrafterUserId` (người tạo), HOẶC + - HĐ có `Phase ∈ eligiblePhases` (role eligible xử lý phase đó) +- `GetContractQueryHandler`: thêm IDOR guard — non-admin truy cập HĐ không liên quan → `ForbiddenException` +- `GetEligiblePhases()` helper `internal static` trong `ListContractsQueryHandler` — mirror `GetMyInboxQueryHandler.PhaseActorRoles`, shared qua assembly + +### Chunk Y — SLA auto-approve BackgroundService + +- `Infrastructure/HostedServices/SlaExpiryJob.cs` MỚI: + - `BackgroundService` chạy mỗi **15 phút** (delay 30s khi app start) + - Query `Contracts WHERE SlaDeadline < UtcNow AND Phase NOT IN (DaPhatHanh, TuChoi)` + - Gọi `IContractWorkflowService.TransitionAsync` với `actorUserId=null`, `Decision=AutoApprove` + - Map phase → next (happy path): DangSoanThao → DangGopY → … → DangDongDau → DaPhatHanh + - Log structured: `auto-approved contract {Id} {From} → {To}` + - Try-catch từng contract — 1 fail không block cả batch +- NuGet: `Microsoft.Extensions.Hosting` (add vào Infrastructure) +- DI register `services.AddHostedService()` + +### Admin password warning (Phase 5.1) + +- `DbInitializer.WarnDefaultAdminPasswordAsync`: check `CheckPasswordAsync` với default "Admin@123456" → log `WRN` warning force đổi prod +- Thêm vào chain `InitializeAsync` +- Test: API khởi động → thấy log `⚠️ Admin user 'admin@solutionerp.local' vẫn dùng password mặc định. ĐỔI NGAY trong production!` + +## E2E verified + +```bash +# Admin +GET /api/contracts → total: 1 (see all) + +# Drafter (test.drafter@solutionerp.local) +GET /api/contracts → total: 0 (IDOR filter — drafter chưa tạo HĐ nào) + +# SlaExpiryJob +Start API → log "SlaExpiryJob: no expired contracts" sau ~15 min (vì SLA của HĐ test là +7d) + Admin warning log xuất hiện: "⚠️ Admin user vẫn dùng password mặc định..." + +# Build + TS: pass +``` + +## Handoff cho session tiếp theo + +### Còn lại Phase 5.1 (nhỏ) + +- [ ] Dependencies scan vào CI workflow (`dotnet list package --vulnerable --include-transitive`, `npm audit --audit-level=high`) +- [ ] BE Roles CRUD (custom role) — optional, 12 role seed đủ dùng + +### Phase 3 iteration 2 còn + +- [ ] Warning notification khi còn 20% SLA (track `SlaWarningSent` flag) +- [ ] Email notification (MailKit) khi chuyển phase +- [ ] In-app notification (DB-backed + polling) — cần table Notifications +- [ ] Upload attachment endpoint + FE multipart +- [ ] RowVersion optimistic concurrency + +### Phase 5 deploy thật + +Chờ Gitea URL để push + CI/CD test. + +## Thông số cumulative + +| | P5.1 prev | **This session** | +|---|---:|---:| +| BE LOC | ~3700 | **~3900** (+SlaExpiryJob 90 + IDOR + admin warning) | +| API endpoints | ~42 | 42 | +| HostedServices | 0 | **1** (SlaExpiryJob) | +| Commits | 10 | **11** (sắp) | diff --git a/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs b/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs index 4348ed4..cc11f91 100644 --- a/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs +++ b/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs @@ -188,7 +188,8 @@ public record ListContractsQuery( Guid? SupplierId = null, Guid? ProjectId = null) : PagedRequest, IRequest>; -public class ListContractsQueryHandler(IApplicationDbContext db) : IRequestHandler> +public class ListContractsQueryHandler(IApplicationDbContext db, ICurrentUser currentUser) + : IRequestHandler> { public async Task> Handle(ListContractsQuery request, CancellationToken ct) { @@ -197,6 +198,14 @@ public class ListContractsQueryHandler(IApplicationDbContext db) : IRequestHandl join p in db.Projects.AsNoTracking() on c.ProjectId equals p.Id select new { c, s, p }; + // IDOR: non-admin chỉ thấy HĐ mình là Drafter hoặc role eligible phase hiện tại + if (!currentUser.Roles.Contains(AppRoles.Admin)) + { + var userId = currentUser.UserId; + var eligiblePhases = GetEligiblePhases(currentUser.Roles); + q = q.Where(x => x.c.DrafterUserId == userId || eligiblePhases.Contains(x.c.Phase)); + } + if (request.Phase is not null) q = q.Where(x => x.c.Phase == request.Phase); if (request.SupplierId is not null) q = q.Where(x => x.c.SupplierId == request.SupplierId); if (request.ProjectId is not null) q = q.Where(x => x.c.ProjectId == request.ProjectId); @@ -224,6 +233,26 @@ public class ListContractsQueryHandler(IApplicationDbContext db) : IRequestHandl return new PagedResult(items, total, request.Page, request.PageSize); } + + // Mirror GetMyInboxQueryHandler.PhaseActorRoles — shared qua internal static + internal static List GetEligiblePhases(IReadOnlyList userRoles) + { + var phases = new HashSet(); + void AddIfAny(string[] required, params ContractPhase[] toAdd) + { + if (userRoles.Any(r => required.Contains(r))) + foreach (var p in toAdd) phases.Add(p); + } + AddIfAny([AppRoles.Drafter, AppRoles.DeptManager], + ContractPhase.DangSoanThao, ContractPhase.DangDamPhan, ContractPhase.DangInKy); + AddIfAny([AppRoles.ProjectManager, AppRoles.Procurement, AppRoles.CostControl, + AppRoles.Finance, AppRoles.Accounting, AppRoles.Equipment], + ContractPhase.DangGopY); + AddIfAny([AppRoles.CostControl], ContractPhase.DangKiemTraCCM); + AddIfAny([AppRoles.Director, AppRoles.AuthorizedSigner], ContractPhase.DangTrinhKy); + AddIfAny([AppRoles.HrAdmin], ContractPhase.DangDongDau); + return phases.ToList(); + } } // ========== INBOX — HĐ chờ role/tôi xử lý ========== @@ -281,8 +310,10 @@ public class GetMyInboxQueryHandler( public record GetContractQuery(Guid Id) : IRequest; -public class GetContractQueryHandler(IApplicationDbContext db, UserManager userManager) - : IRequestHandler +public class GetContractQueryHandler( + IApplicationDbContext db, + UserManager userManager, + ICurrentUser currentUser) : IRequestHandler { public async Task Handle(GetContractQuery request, CancellationToken ct) { @@ -293,6 +324,17 @@ public class GetContractQueryHandler(IApplicationDbContext db, UserManager .FirstOrDefaultAsync(x => x.Id == request.Id, ct) ?? throw new NotFoundException("Contract", request.Id); + // IDOR guard: non-admin chỉ xem được HĐ mình là Drafter hoặc role eligible phase hiện tại + var isAdmin = currentUser.Roles.Contains(AppRoles.Admin); + if (!isAdmin) + { + var isDrafter = c.DrafterUserId == currentUser.UserId; + var eligiblePhases = ListContractsQueryHandler.GetEligiblePhases(currentUser.Roles); + var isEligibleByRole = eligiblePhases.Contains(c.Phase); + if (!isDrafter && !isEligibleByRole) + throw new ForbiddenException("Bạn không có quyền xem HĐ này."); + } + var supplier = await db.Suppliers.AsNoTracking().FirstOrDefaultAsync(s => s.Id == c.SupplierId, ct); var project = await db.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == c.ProjectId, ct); var department = c.DepartmentId is null ? null : await db.Departments.AsNoTracking().FirstOrDefaultAsync(d => d.Id == c.DepartmentId, ct); diff --git a/src/Backend/SolutionErp.Infrastructure/DependencyInjection.cs b/src/Backend/SolutionErp.Infrastructure/DependencyInjection.cs index 6d3f4d6..5ea990d 100644 --- a/src/Backend/SolutionErp.Infrastructure/DependencyInjection.cs +++ b/src/Backend/SolutionErp.Infrastructure/DependencyInjection.cs @@ -8,6 +8,7 @@ using SolutionErp.Application.Forms.Services; using SolutionErp.Application.Reports.Services; using SolutionErp.Domain.Identity; using SolutionErp.Infrastructure.Forms; +using SolutionErp.Infrastructure.HostedServices; using SolutionErp.Infrastructure.Identity; using SolutionErp.Infrastructure.Persistence; using SolutionErp.Infrastructure.Persistence.Interceptors; @@ -30,6 +31,9 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); + // Phase 3 iteration 2 — SLA auto-approve background service + services.AddHostedService(); + services.AddScoped(); services.AddDbContext((sp, options) => diff --git a/src/Backend/SolutionErp.Infrastructure/HostedServices/SlaExpiryJob.cs b/src/Backend/SolutionErp.Infrastructure/HostedServices/SlaExpiryJob.cs new file mode 100644 index 0000000..79e92f3 --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/HostedServices/SlaExpiryJob.cs @@ -0,0 +1,106 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using SolutionErp.Application.Common.Interfaces; +using SolutionErp.Application.Contracts.Services; +using SolutionErp.Domain.Contracts; + +namespace SolutionErp.Infrastructure.HostedServices; + +// Background service chạy mỗi 15 phút, auto-approve HĐ quá SLA. +// Xem docs/flows/sla-expiry-flow.md cho spec full. +// Phase 3 iteration 2 MVP — chưa có notification, chỉ log + ContractApproval với Decision=AutoApprove. +public class SlaExpiryJob : BackgroundService +{ + private readonly IServiceProvider _sp; + private readonly ILogger _logger; + private static readonly TimeSpan Interval = TimeSpan.FromMinutes(15); + + // Map phase → phase tiếp (happy path). Nếu null → không auto-approve (final/TuChoi). + private static readonly Dictionary NextPhase = new() + { + [ContractPhase.DangSoanThao] = ContractPhase.DangGopY, + [ContractPhase.DangGopY] = ContractPhase.DangDamPhan, + [ContractPhase.DangDamPhan] = ContractPhase.DangInKy, + [ContractPhase.DangInKy] = ContractPhase.DangKiemTraCCM, + [ContractPhase.DangKiemTraCCM] = ContractPhase.DangTrinhKy, + [ContractPhase.DangTrinhKy] = ContractPhase.DangDongDau, + [ContractPhase.DangDongDau] = ContractPhase.DaPhatHanh, + }; + + public SlaExpiryJob(IServiceProvider sp, ILogger logger) + { + _sp = sp; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + // Wait 30s sau khi app start — tránh race với DbInitializer migrate + await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + await ProcessAsync(stoppingToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "SlaExpiryJob iteration failed"); + } + await Task.Delay(Interval, stoppingToken); + } + } + + private async Task ProcessAsync(CancellationToken ct) + { + await using var scope = _sp.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var workflow = scope.ServiceProvider.GetRequiredService(); + var dateTime = scope.ServiceProvider.GetRequiredService(); + + var now = dateTime.UtcNow; + var expired = await db.Contracts + .Where(c => c.SlaDeadline != null && c.SlaDeadline < now) + .Where(c => c.Phase != ContractPhase.DaPhatHanh && c.Phase != ContractPhase.TuChoi) + .ToListAsync(ct); + + if (expired.Count == 0) + { + _logger.LogDebug("SlaExpiryJob: no expired contracts"); + return; + } + + _logger.LogInformation("SlaExpiryJob: {Count} expired contracts", expired.Count); + + foreach (var contract in expired) + { + if (!NextPhase.TryGetValue(contract.Phase, out var next) || next is null) + { + _logger.LogWarning("SlaExpiryJob: phase {Phase} không có next, skip contract {Id}", contract.Phase, contract.Id); + continue; + } + + try + { + var hoursOver = (int)(now - contract.SlaDeadline!.Value).TotalHours; + await workflow.TransitionAsync( + contract, + next.Value, + actorUserId: null, // system + actorRoles: new List(), // system bypass check trong workflow + decision: ApprovalDecision.AutoApprove, + comment: $"[AUTO] Hết SLA phase {contract.Phase} ({hoursOver}h quá hạn). Tự động chuyển sang {next.Value}.", + ct: ct); + _logger.LogInformation("SlaExpiryJob: auto-approved contract {Id} {From} → {To}", + contract.Id, contract.Phase, next.Value); + } + catch (Exception ex) + { + _logger.LogError(ex, "SlaExpiryJob: failed auto-approve {Id}", contract.Id); + } + } + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs index d262bea..71588ba 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs @@ -30,6 +30,20 @@ public static class DbInitializer await SeedMenuTreeAsync(db, logger); await SeedAdminPermissionsAsync(db, roleManager, logger); await SeedContractTemplatesAsync(db, logger); + await WarnDefaultAdminPasswordAsync(userManager, logger); + } + + // Phase 5.1 security: log warning nếu admin vẫn dùng password mặc định sau deploy production. + private static async Task WarnDefaultAdminPasswordAsync(UserManager userManager, ILogger logger) + { + var admin = await userManager.FindByEmailAsync(AdminEmail); + if (admin is null) return; + if (await userManager.CheckPasswordAsync(admin, AdminPassword)) + { + logger.LogWarning( + "⚠️ Admin user '{Email}' vẫn dùng password mặc định. ĐỔI NGAY trong production! " + + "Xem docs/guides/security-checklist.md.", AdminEmail); + } } private static async Task SeedRolesAsync(RoleManager roleManager, ILogger logger) diff --git a/src/Backend/SolutionErp.Infrastructure/SolutionErp.Infrastructure.csproj b/src/Backend/SolutionErp.Infrastructure/SolutionErp.Infrastructure.csproj index 2b5e9ec..bdc85ab 100644 --- a/src/Backend/SolutionErp.Infrastructure/SolutionErp.Infrastructure.csproj +++ b/src/Backend/SolutionErp.Infrastructure/SolutionErp.Infrastructure.csproj @@ -13,6 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all +