# Session 2026-04-21 16:30 — Phase 5.1 Security + Users Management

**Dev:** Claude (Opus 4.7)
**Duration:** ~1h
**Base commit:** `46a2cab`

## Làm được

### Chunk T — Phase 5.1 Security hardening

- `Api/Middleware/SecurityHeadersMiddleware.cs` MỚI:
  - Remove server fingerprint: `Server`, `X-Powered-By`, `X-AspNet-Version`, `X-AspNetMvc-Version`
  - Security headers: `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy: strict-origin-when-cross-origin`, `X-Permitted-Cross-Domain-Policies: none`, `Permissions-Policy` (disable geolocation/mic/cam/payment)
  - CSP: `default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'` — skip ở `/swagger` (dùng inline script/style)
- `Program.cs` wire `app.UseMiddleware<SecurityHeadersMiddleware>()` đầu pipeline

- `Infrastructure/DependencyInjection.cs` Identity options:
  - Password `RequiredLength` đọc config (`Identity:Password:RequiredLength`) — default 8 dev, override 12+ prod
  - Lockout: `DefaultLockoutTimeSpan` (default 15 phút), `MaxFailedAccessAttempts` (default 5), `AllowedForNewUsers = true`
  - Override qua `appsettings.Production.json` Identity section

- `LoginCommand` handler update:
  - Check `IsLockedOutAsync` trước khi verify password → throw với deadline message
  - `AccessFailedAsync` khi sai password (tăng counter, auto-lock)
  - `ResetAccessFailedCountAsync` khi login thành công

### Chunk U — BE Users management (8 features)

- `Application/Users/UserFeatures.cs`:
  - `UserDto` (Id, Email, FullName, IsActive, IsLocked, CreatedAt, Roles)
  - `ListUsersQuery` với paging + search (email / fullName)
  - `GetUserQuery`
  - `CreateUserCommand` + Validator + Handler (check email unique, CreateAsync + AddToRoleAsync)
  - `UpdateUserCommand` (FullName + IsActive, với self-disable protection)
  - `AssignRolesCommand` (replace full set, diff add/remove, self-demote protection — không tự gỡ Admin)
  - `ResetPasswordCommand` (admin flow — Remove + Add password, invalidate refresh token, unlock)
  - `UnlockUserCommand`
- `Api/Controllers/UsersController.cs` — 7 endpoint với policy `Users.Read` / `Users.Create` / `Users.Update`

### Chunk V — FE admin Users page

- `types/users.ts`: User type + `AVAILABLE_ROLES` (12 role từ BE AppRoles.cs) + `RoleLabel` tiếng Việt
- `pages/system/UsersPage.tsx`:
  - DataTable list users với Email / Họ tên / Roles (badge chips) / Active / Locked / Created
  - Nút **Gán role** (icon Shield) → Dialog checkbox 12 role → PUT `/roles`
  - Nút **Reset password** (icon KeyRound) → Dialog với warning "user sẽ bị logout"
  - Nút **Unlock** (hiện khi isLocked=true)
  - Nút **Toggle Active** (XCircle / CheckCircle)
  - Dialog **Thêm user mới**: email / fullName / password / grid roles checkbox → POST `/users`
  - `<PermissionGuard menuKey="Users" action="Create|Update">` wrap các button
- Route `/system/users` vào App.tsx

## E2E verified

```bash
# Security headers
GET /api/users (với Bearer) → 200
  Response headers:
    X-Content-Type-Options: nosniff ✅
    X-Frame-Options: DENY ✅
    Referrer-Policy: strict-origin-when-cross-origin ✅
    Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=() ✅
    Content-Security-Policy: default-src 'self'; ... ✅

# Users CRUD
POST /api/users {email: test.drafter@..., roles: ["Drafter"]} → 201 + id
GET /api/users → Paged[1 admin only initially, now 2]

# TS check fe-admin → pass
```

## Bug gặp + fix

| Bug | Fix |
|---|---|
| `ValidationException` ambiguous FluentValidation vs Custom | `using ValidationException = ...Custom.ValidationException;` alias |
| Edit tool "File not read" tiếp tục bị hit sau system-reminder | Read lại → Edit — pattern đã quen |

## Tác động lên STATUS/HANDOFF/migration-todos/gotchas

- `migration-todos.md` Phase 5.1: tick 5 items done (security headers, account lockout, password policy, — còn IDOR check + dependencies scan)
- `gotchas.md`: không thêm mới (các issue lần này không có bẫy mới)
- `STATUS.md`: cumulative BE LOC 3300 → 3700 (+UserFeatures + SecurityHeaders)
- `HANDOFF.md`: thêm Phase 5.1 status row, FE pages 16 → 17 (+UsersPage)

## Handoff cho session tiếp theo

### Phase 5.1 còn

- [ ] IDOR check ContractsController: user Drafter chỉ thấy HĐ mình tạo hoặc có role giữ phase hiện tại (cần add helper `IsInvolvedInContract(userId, contractId)`)
- [ ] Dependencies scan CI: `dotnet list package --vulnerable --include-transitive` + `npm audit --audit-level=high`
- [ ] Admin mặc định: thêm warning log lần đầu start nếu admin password = `Admin@123456` (force đổi)

### Roles CRUD (quick win còn lại)

- [ ] BE: `CreateRoleCommand`, `UpdateRoleCommand` (rename + description), `DeleteRoleCommand` (block nếu còn user/permission)
- [ ] FE: `/system/roles` page

### Phase 3 iter 2 (big — optional)

- SlaExpiryJob BackgroundService
- Email/in-app notification
- Upload attachment endpoint + FE

### Blocker

- ⏳ Gitea URL — user sẽ cấp

## Thông số cumulative

| | Phase 5 prep | **Phase 5.1 Security + Users** |
|---|---:|---:|
| BE LOC | ~3300 | **~3700** (+UserFeatures ~250 + SecurityHeaders ~40 + LoginHandler update) |
| DB tables | 19 | 19 |
| API endpoints | ~35 | **~42** (+7 users) |
| FE pages | 16 | **17** (+UsersPage) |
| Middleware | 2 | **3** (+SecurityHeaders) |
| Commits | 9 | **10** (sắp) |
