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>
7.9 KiB
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— keysolution-erp-admin-token(fe-admin) /solution-erp-user-token(fe-user) - Truyền: header
Authorization: Bearer <token>(axios interceptor auto)
2. Login
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/contexts/AuthContext.tsx - BE controller:
AuthController.Login - Handler:
LoginCommandHandler - Token gen:
JwtTokenService
3. Authenticated request (mọi API khác)
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 interceptor catches 401 → clear localStorage → window.location.href = '/login'.
4. Refresh token
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)
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
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:Secrettrong user-secrets / env var prod (không trong appsettings)- HTTPS enforce (
RequireHttpsMetadata = trueproduction) - Rate limit
/api/auth/login(5 attempts/min/IP) — prevent brute force - Account lockout sau N lần sai password (config
UserLockouttrong 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)