[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:
212
docs/flows/auth-flow.md
Normal file
212
docs/flows/auth-flow.md
Normal file
@ -0,0 +1,212 @@
|
||||
# Auth Flow — Login / Refresh / Logout / Me
|
||||
|
||||
> **Status:** ✅ Implemented (Phase 1 foundation, commit `702411f`)
|
||||
> **Actors:** Anyone (login) | Authenticated user (refresh, me, logout)
|
||||
|
||||
## 1. Tổng quan
|
||||
|
||||
- **Schema:** JWT Bearer (HS256) + refresh token (random 64-byte base64)
|
||||
- **Access token:** 1 giờ (config `Jwt:AccessTokenExpiryMinutes`)
|
||||
- **Refresh token:** 7 ngày (config `Jwt:RefreshTokenExpiryDays`)
|
||||
- **Storage FE:** `localStorage` — key `solution-erp-admin-token` (fe-admin) / `solution-erp-user-token` (fe-user)
|
||||
- **Truyền:** header `Authorization: Bearer <token>` (axios interceptor auto)
|
||||
|
||||
## 2. Login
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor U as User
|
||||
participant FE as Frontend<br/>(LoginPage)
|
||||
participant API as SolutionErp.Api<br/>AuthController
|
||||
participant M as MediatR<br/>LoginCommandHandler
|
||||
participant UM as UserManager<User>
|
||||
participant J as IJwtTokenService
|
||||
participant DB as SQL Server
|
||||
|
||||
U->>FE: Nhập email + password → Submit
|
||||
FE->>API: POST /api/auth/login<br/>{email, password}
|
||||
API->>M: Send(LoginCommand)
|
||||
|
||||
M->>M: Validate (FluentValidation)<br/>email format + password min 6
|
||||
alt Validation fail
|
||||
M-->>API: throw ValidationException
|
||||
API-->>FE: 400 ProblemDetails + errors{}
|
||||
FE->>U: Toast error
|
||||
end
|
||||
|
||||
M->>UM: FindByEmailAsync(email)
|
||||
UM->>DB: SELECT FROM Users WHERE Email = ?
|
||||
alt User not found OR IsActive = false
|
||||
M-->>API: throw UnauthorizedException
|
||||
API-->>FE: 401 "Email hoặc mật khẩu không đúng"
|
||||
end
|
||||
|
||||
M->>UM: CheckPasswordAsync(user, password)
|
||||
alt Password sai
|
||||
M-->>API: throw UnauthorizedException
|
||||
API-->>FE: 401
|
||||
end
|
||||
|
||||
M->>UM: GetRolesAsync(user)
|
||||
UM->>DB: SELECT Roles JOIN UserRoles
|
||||
UM-->>M: roles[]
|
||||
|
||||
M->>J: GenerateTokensAsync(user, roles)
|
||||
J->>J: Build claims (sub, email, jti, fullName, roles)
|
||||
J->>J: Sign HS256 + random refresh token
|
||||
J-->>M: (accessToken, refreshToken, refreshTokenExpiresAt)
|
||||
|
||||
M->>UM: UpdateAsync(user) — save refresh token + expiry
|
||||
UM->>DB: UPDATE Users SET RefreshToken=?, RefreshTokenExpiresAt=?
|
||||
|
||||
M-->>API: AuthResponseDto
|
||||
API-->>FE: 200 {accessToken, refreshToken, user: {id, email, fullName, roles}}
|
||||
|
||||
FE->>FE: localStorage.setItem('solution-erp-*-token', accessToken)<br/>+ lưu user JSON
|
||||
FE->>FE: setUser(user) via AuthContext
|
||||
FE->>U: Redirect /dashboard hoặc /inbox + toast success
|
||||
```
|
||||
|
||||
**Code pointers:**
|
||||
- FE: [`fe-admin/src/pages/LoginPage.tsx`](../../fe-admin/src/pages/LoginPage.tsx), [`fe-admin/src/contexts/AuthContext.tsx`](../../fe-admin/src/contexts/AuthContext.tsx)
|
||||
- BE controller: [`AuthController.Login`](../../src/Backend/SolutionErp.Api/Controllers/AuthController.cs)
|
||||
- Handler: [`LoginCommandHandler`](../../src/Backend/SolutionErp.Application/Auth/Commands/Login/LoginCommand.cs)
|
||||
- Token gen: [`JwtTokenService`](../../src/Backend/SolutionErp.Infrastructure/Identity/JwtTokenService.cs)
|
||||
|
||||
## 3. Authenticated request (mọi API khác)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant FE as Frontend
|
||||
participant AX as axios interceptor
|
||||
participant API as API
|
||||
participant JWT as JwtBearerMiddleware
|
||||
participant CTRL as Controller
|
||||
|
||||
FE->>AX: api.get('/suppliers')
|
||||
AX->>AX: Đọc localStorage token
|
||||
AX->>API: GET /api/suppliers<br/>Authorization: Bearer <token>
|
||||
|
||||
API->>JWT: ValidateToken
|
||||
alt Token invalid / expired
|
||||
JWT-->>API: 401
|
||||
API-->>AX: 401
|
||||
AX->>AX: Clear localStorage + redirect /login
|
||||
end
|
||||
|
||||
JWT->>API: HttpContext.User = ClaimsPrincipal
|
||||
API->>CTRL: Invoke action (có [Authorize])
|
||||
CTRL->>CTRL: ICurrentUser.UserId (từ claim "sub")
|
||||
CTRL-->>FE: 200 data
|
||||
```
|
||||
|
||||
**401 handling:** [`fe-admin/src/lib/api.ts`](../../fe-admin/src/lib/api.ts) interceptor catches 401 → clear localStorage → `window.location.href = '/login'`.
|
||||
|
||||
## 4. Refresh token
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant FE as Frontend
|
||||
participant API as AuthController.Refresh
|
||||
participant M as RefreshTokenCommandHandler
|
||||
participant UM as UserManager
|
||||
participant J as IJwtTokenService
|
||||
participant DB
|
||||
|
||||
Note over FE: Access token expired<br/>(server trả 401 từ /api/whatever)
|
||||
FE->>API: POST /api/auth/refresh<br/>{refreshToken}
|
||||
API->>M: Send(RefreshTokenCommand)
|
||||
|
||||
M->>M: Validate not empty
|
||||
M->>UM: Users.First(RefreshToken == ?)
|
||||
UM->>DB: SELECT
|
||||
alt Not found OR RefreshTokenExpiresAt < UtcNow
|
||||
M-->>API: throw UnauthorizedException
|
||||
API-->>FE: 401 "Refresh token không hợp lệ hoặc đã hết hạn"
|
||||
FE->>FE: Clear localStorage + redirect /login
|
||||
end
|
||||
|
||||
M->>UM: GetRolesAsync(user)
|
||||
M->>J: GenerateTokensAsync(user, roles)
|
||||
J-->>M: new (access, refresh, expiry)
|
||||
|
||||
M->>UM: UpdateAsync — rotate refresh token
|
||||
DB-->>UM: OK
|
||||
|
||||
M-->>API: AuthResponseDto
|
||||
API-->>FE: 200 new tokens
|
||||
FE->>FE: Update localStorage + retry original request
|
||||
```
|
||||
|
||||
**⚠️ Chưa implement ở FE:** hiện tại 401 → logout luôn. Phase 1 đợt 2 sẽ thêm logic auto-refresh trong axios interceptor (retry failed request sau khi refresh thành công, queue các request song song).
|
||||
|
||||
## 5. /me (lấy user hiện tại)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor U
|
||||
participant FE
|
||||
participant API as AuthController.Me
|
||||
participant M as GetMeQueryHandler
|
||||
participant CU as ICurrentUser
|
||||
participant UM as UserManager
|
||||
|
||||
U->>FE: App bootstrap (sau login)
|
||||
FE->>API: GET /api/auth/me<br/>Bearer <token>
|
||||
API->>M: Send(GetMeQuery)
|
||||
|
||||
M->>CU: UserId (from claim "sub")
|
||||
alt Not authenticated
|
||||
M-->>API: throw UnauthorizedException
|
||||
end
|
||||
|
||||
M->>UM: FindByIdAsync(userId.ToString())
|
||||
alt Not found
|
||||
M-->>API: throw UnauthorizedException
|
||||
end
|
||||
|
||||
M->>UM: GetRolesAsync(user)
|
||||
M-->>API: UserInfoDto{id, email, fullName, roles}
|
||||
API-->>FE: 200
|
||||
```
|
||||
|
||||
**Use case:** FE dùng để refresh user info sau page reload (nếu localStorage cũ nhưng role có thể thay đổi). Hiện Phase 1 FE lấy user từ localStorage trực tiếp — Phase 1 đợt 2 sẽ gọi `/me` ở mount AuthContext để verify token + sync role.
|
||||
|
||||
## 6. Logout
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor U
|
||||
participant FE
|
||||
participant API as AuthController.Logout
|
||||
|
||||
U->>FE: Click "Đăng xuất"
|
||||
FE->>FE: logout() in AuthContext:<br/>localStorage.removeItem(token + user)<br/>setUser(null)
|
||||
FE->>API: POST /api/auth/logout (optional)
|
||||
API-->>FE: 204 No Content
|
||||
FE->>FE: Redirect /login (via ProtectedRoute)
|
||||
```
|
||||
|
||||
**Lưu ý:** Hiện endpoint logout chỉ trả 204. Phase 3 sẽ upgrade: invalidate refresh token trong DB + log audit event.
|
||||
|
||||
## 7. Edge cases
|
||||
|
||||
| Case | Hiện tại handle | Phase nào fix |
|
||||
|---|---|---|
|
||||
| Access token expired mid-request | FE 401 → logout | Phase 1 đợt 2: auto refresh |
|
||||
| Refresh token expired | FE 401 → logout | OK — đúng hành vi |
|
||||
| User bị disable (`IsActive=false`) sau khi đã login | Token vẫn valid tới khi expire | Phase 4: check `IsActive` trong middleware |
|
||||
| Admin reset password | Refresh token cũ vẫn valid | Phase 4: invalidate refresh token khi reset password |
|
||||
| Multi-device login | Ghi đè refresh token → device cũ bị logout khi cần refresh | OK cho Phase 1; Phase 4 optional: bảng `UserRefreshTokens` riêng |
|
||||
| JWT key leak | Toàn bộ user có thể forge token | Phase 5: dùng user secrets prod + rotate annually |
|
||||
| Clock skew giữa API + client | `ClockSkew = 1 minute` đã config | OK |
|
||||
|
||||
## 8. Security checklist (Phase 5 review)
|
||||
|
||||
- [ ] `Jwt:Secret` trong user-secrets / env var prod (không trong appsettings)
|
||||
- [ ] HTTPS enforce (`RequireHttpsMetadata = true` production)
|
||||
- [ ] Rate limit `/api/auth/login` (5 attempts/min/IP) — prevent brute force
|
||||
- [ ] Account lockout sau N lần sai password (config `UserLockout` trong Identity)
|
||||
- [ ] Password policy production (min 12 chars, unique, etc.)
|
||||
- [ ] Audit log cho login success/fail
|
||||
- [ ] Refresh token rotation detection (nếu dùng refresh cũ sau khi đã rotate → compromise alert)
|
||||
Reference in New Issue
Block a user