Files
sciagent/docs/user-profile-manager-state-machine-plan.md
Thinh Lam 688fac73e9
CI/CD / backend (push) Failing after 2m8s
CI/CD / frontend (push) Failing after 1m40s
CI/CD / deploy (push) Has been skipped
sciagent code + Gitea Actions CI/CD
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:38:30 +07:00

286 lines
16 KiB
Markdown
Raw Permalink 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.
# User profile manager — integration analysis and implementation state machine
This document analyzes how **`fe0/src/auth/LoginRegisterCard.tsx`** connects to the rest of the stack, clarifies boundaries with **PostgreSQL** and **MinIO**, and specifies a **state-machinedriven** plan for institutional profile fields, **admin verification**, and applicant **self-service** edits.
**Companion document (canonical DB engineering guidance):** [`user-profile-manager-db-review.md`](./user-profile-manager-db-review.md) — schema shape, DDL skeleton, indexes, concurrency, audit, and constraint-level enforcement. This plan **inherits** those decisions below so implementation does not fork.
Related: [`auth-registration-and-user-management.md`](./auth-registration-and-user-management.md) (broader auth; some paths may lag the tree).
---
## 1. How `LoginRegisterCard` connects today
### 1.1 Frontend wiring
| Layer | Role |
| ----- | ----- |
| `fe0/src/pages/Login.tsx` | Renders `LoginRegisterCard` only. |
| `fe0/src/auth/LoginRegisterCard.tsx` | Tabs (login/register); validates institutional email (`fe0/src/auth/institutionalEmail`); collects **full name**, **email**, **password** (+ confirm); calls `useAuth()` from **`fe0/src/contexts/AuthContext.tsx`**. |
| `AuthContext` | `login``authService.login`; `register``authService.register`; on success stores JWT via `authService`, builds session user via `fe0/src/auth/sessionUser.ts` (`buildUserFromAuthPayload`). |
| `fe0/src/lib/auth-service.ts` | HTTP to **`POST {API_URL}/api/v1/auth/register`** with JSON `{ fullName, email, password, passwordConfirm }`; stores `accessToken` in localStorage on success. |
| Routing | Successful login/register navigates with `resolvePostLoginPath` (`fe0/src/lib/dashboardNavigation.ts`) using the resolved role. |
Session user shape (`AuthSessionUser`): `id`, `email`, `name`, `phone`, `roles` (effective + available), computed `permissions`.
### 1.2 Backend wiring
| Layer | Role |
| ----- | ----- |
| `be0/src/auth_api.py` | **`POST /auth/register`** (mounted under `/api/v1`): normalizes `@ump.edu.vn` / `@umc.edu.vn`, enforces password policy, creates **`users`** row + **`user_roles`** row (**server-derived** `admin` vs `viewer`; client `role` ignored). Issues JWT embedding roles. Writes **`audit_log`** for registration. |
| `be0/src/initiative_db/models.py` **`User`** | Persists **`email`**, **`password_hash`**, **`full_name`**, optional **`phone`**, optional **`unit_id`**, **`is_active`**, timestamps — **no** institutional extended profile tables yet. |
### 1.3 Database today
- **`users`** + **`user_roles`:** credentials, display identity (`full_name`, `phone`), **`unit_id`** (catalog hook — see §2), `is_active`, RBAC-like roles (`admin`, `editor`, `viewer`, …).
### 1.4 MinIO — explicit boundary
**Registration and profile HTTP APIs must not persist profile scalars in MinIO.** Object storage (**attachments**, **exports**, **quarantine**) lives in **`be0/src/minio/storage.py`** — **binary blobs** only.
| Concern | Store |
| ------- | ----- |
| Profile fields, verification status, timestamps | **PostgreSQL** — preferably **`user_staff_profiles`** + catalog tables (§5), not widening hot `users` rows for HR text (see DB review §2). |
| Optional future HR **proof uploads** | MinIO keys + metadata rows referencing `user_staff_profiles`; out of scope until product demands it |
---
## 2. Gap analysis vs desired product
### 2.1 Registration (`LoginRegisterCard`)
| Field | UI | Persistence / naming |
| ----- | --- | ----- |
| Họ và tên | `<input>` | Stays **`users.full_name`** (already collected as `fullName`). |
| Mã số nhân sự | `<input>` | **`user_staff_profiles.employee_id`** — format + uniqueness at DB (**partial unique index** when non-NULL — see DB review §3.1 / §9); Pydantic + **`CHECK`** in migration. **`NULL`/required policy decided before DDL** ([§9 open questions](#9-open-questions-before-merge)). |
| Học hàm, học vị | dropdown + “Khác” `<input>` | **`academic_title_code`** FK → **`academic_titles` lookup table** (not raw Postgres ENUM for title list — avoids DDL drift); **`academic_title_other`** mandatory iff `code='other'` — enforce with **`CHECK`** ([DB review §3.2](user-profile-manager-db-review.md)). |
| Đơn vị công tác | `<input>` or catalog | **Must reconcile `users.unit_id`:** either **catalog only** (`unit_id` FK, drop parallel free-text) **or** staged **`unit_id` XOR `unit_name_freetext`** with `CHECK`; **never** ship orphan `department` text alongside a real **`units`** catalog ([DB review §3.3](user-profile-manager-db-review.md)). |
| Chức vụ (job) | `<input>` | **`job_title`** in DB/API — never overload JWT **`roles`**. Nullable for non-person accounts; **length capped** (`CHECK` or varchar). |
| Email / SĐT | `<input>` | Email unchanged (authoritative normalization on server); phone aligns with **`auth_api`** PATCH rules. |
**Backend:** **`INSERT users` + `INSERT user_staff_profiles`** + **`user_roles`** in **one transaction** — profile row created at registration (possibly all NULL + `draft`) or seeded in a follow-on migration backfill ([DB review §8](user-profile-manager-db-review.md)).
### 2.2 Applicant profile (`ApplicantProfileView`)
**Today:** **`GET .../auth/me`**, **`PATCH .../auth/profile`** — **`fullName`**, **`phone`** only.
**Target:** Joint reads join **`User`** ↔ **`UserStaffProfile`** for `/me` (or dedicated profile projection). PATCH applicant-owned staff fields bumps **`UserStaffProfiles.version`** (optimistic concurrency for admin verify — §4.4). Policy: **freeze after `verified`** vs **material edits → `pending`** — pick one ([§9](#9-open-questions-before-merge)).
### 2.3 Admin “Users” (`DashboardSidebar`)
`fe0/src/components/admin/DashboardSidebar.tsx` links **`/dashboard/users`** via management menu items (~`MenuItem` + `Link`).
**Routing gap:** `fe0/src/App.tsx` has **no** `users` route → **404** until **User Profile Manager** ships.
---
## 3. Cross-cutting correctness (security, concurrency, audit)
1. **Server validation:** domain, enums, lengths, **`employee_id`** shape — mirror in **`CHECK`** where feasible (DB review §3).
2. **RBAC vs job title:** API names **`job_title`**; JWT / `user_roles` = **`admin`** | **`viewer`** | … only.
3. **Authz:** Applicants **read/update own** staff profile subset; admins **list / verify / reject** via **`/admin/...`**; same patterns as existing audit actors.
4. **Two axes:** **`users.is_active`** (login disabled) **orthogonal** to **`profile_verification_status`** (HR trust) — do not collapse.
5. **`TIMESTAMPTZ`** everywhere for `_at` columns (DB review §3.6).
6. **Admin idempotency:** **Verify twice** → choose **409 Conflict** vs **204/200 idempotent no-op**; document next to handler; conditional `UPDATE` makes **lost races** observable ([§4.4](#44-concurrency-two-admins-patch-while-reviewing)).
**Audit ([DB review §6](user-profile-manager-db-review.md)):**
- **`JSONB`** before/after snapshots with a **whitelist** of fields — never blacklist (avoids leaking `password_hash` or future secrets).
- **Same transaction** as profile state transitions; rollback state if audit insert fails.
- **Ordering:** predictable sort key (`BIGSERIAL`/monotonic **`event_id`** + `created_at`); index **`(entity_type, entity_id, event_id)`** for history timelines.
- **Growth:** retention policy + partitioning (e.g. monthly `RANGE` on `created_at`) for append-only volumes.
---
## 4. State machines
Treat the **diagram** as the **logical spec**. The database **implements invariants** (status type + cross-column `CHECK`s); illegal rows must **fail to commit** ([DB review §4](user-profile-manager-db-review.md)).
### 4.1 Profile verification lifecycle
| State | Meaning |
| ----- | ------- |
| `draft` | Account exists; profile incomplete or not submitted for verification. |
| `pending` | Submitted queue; awaits admin decision. |
| `verified` | Admin approved → **`verified_at`** + **`verified_by_user_id`** required (DB **`CHECK`**). |
| `rejected` | Admin declined → **`rejection_reason`** non-empty required (DB **`CHECK`**). |
**Cross-column constraints (PostgreSQL)** — align code with DDL in [DB review §4.2 / §9](user-profile-manager-db-review.md), e.g.:
- Verified ⇒ `verified_at` + `verified_by_user_id` non-NULL
- Rejected ⇒ trimmed `rejection_reason`
- Draft/pending ⇒ verification metadata cleared
**Transitions hard to encode as CHECK** (e.g. no direct `verified → draft`): use **`UPDATE … WHERE user_id=$1 AND profile_verification_status=$expected AND version=$etag`** — **zero rows** ⇒ illegal transition **or stale read****409** (DB review §4.3).
#### Mermaid — verification lifecycle
```mermaid
stateDiagram-v2
[*] --> draft: Registration + empty staff profile / backfill
draft --> pending: Submit for review meets completeness rules
pending --> verified: Admin approves conditional UPDATE succeeds
pending --> rejected: Admin rejects with reason
rejected --> pending: Applicant resubmits clears reason per policy
verified --> pending: Material edit triggers re-verification strict policy optional
verified --> verified: Cosmetic-only edit under pragmatic policy optional
note right of verified
DB VERIFY CHECK verified_at verifier NOT NULL rejection cleared
Applicant PATCH increments version optimistic lock
end note
```
**Product/policy encoded in tests** (mirror diagram + illegal edges): see [§8](#8-testing-matrix).
### 4.2 Request / ownership (actors)
```mermaid
stateDiagram-v2
[*] --> Anonymous
Anonymous --> AuthenticatedApplicant: Register or Login JWT
state AuthenticatedApplicant {
[*] --> SelfRead
SelfRead --> SelfPatch: PATCH own profile increments version optional
SelfPatch --> SelfRead
}
AuthenticatedApplicant --> pending: Submit transitions draft→pending
state AdminAuthenticated {
[*] --> Listing
Listing --> Detail: Open profile sends version seen
Detail --> Listing: VERIFY REJECT conditional on prior status + version
}
```
### 4.3 Data flow (target shape)
```mermaid
flowchart LR
subgraph fe0
LR[LoginRegisterCard]
AS[auth-service]
LR --> AS
end
subgraph be0
API[auth_api register/profile]
U[(users + user_roles)]
P[user_staff_profiles]
AU[audit same txn]
API --> U
API --> P
API --> AU
end
AS -->|HTTPS JSON| API
subgraph minio_optional
S3[(MinIO blobs)]
end
S3 -.->|not profile scalars| API
```
### 4.4 Concurrency: two admins, PATCH while reviewing
| Scenario | Required behavior |
| -------- | ----------------- |
| Two admins verify same `pending` row | Conditional `UPDATE` — second gets **0 rows****`409 Conflict`** **or** explicit idempotent **200**; **never** pretend success while row unchanged inconsistently ([DB review §5](user-profile-manager-db-review.md)). |
| Applicant PATCH while admin reads | If verify must bind to reviewed snapshot, admin submits **`expected_version`** (etag from detail API); mismatch → **409** reload. Bump **`version`** on every applicant mutating PATCH. |
---
## 5. Canonical database shape (summary)
**Decision:** **`user_staff_profiles`** as **1:1 child****`PRIMARY KEY (user_id)` REFERENCES users(id) ON DELETE CASCADE`** — **do not widen `users`** for HR fields ([DB review §2](user-profile-manager-db-review.md)).
| Concern | Approach |
| ------- | -------- |
| Academic titles | Table **`academic_titles`** (`code`, `label_vi`, …); FK **`academic_title_code`**; **`other`** invariant `CHECK`. |
| `employee_id` | Partial **`UNIQUE WHERE employee_id IS NOT NULL`**; shape `CHECK`; NULL/required/product rule in [§9](#9-open-questions-before-merge). |
| Department | **`users.unit_id`** vs **`unit_name_freetext`** — single story; XOR `CHECK` during transition ([DB review §3.3](user-profile-manager-db-review.md)). |
| Status typing | Postgres **`ENUM`** or `CHECK (... IN (...))` — no free-text status. |
| Optimistic lock | **`version INTEGER NOT NULL DEFAULT 1`** on `user_staff_profiles`; bump every applicant-facing mutating UPDATE. |
**Indexing (day-one hot paths)** — detail in [DB review §7](user-profile-manager-db-review.md):
- Partial index: **`WHERE profile_verification_status = 'pending'`** + `verification_submitted_at`.
- Composite filters (unit/status/submitted_at) when query shape is confirmed.
- **`(verified_by_user_id, verified_at DESC)`** for verifier dashboards.
**DDL skeleton:** [DB review §9](user-profile-manager-db-review.md) — use as migration author starter; reconcile column names (`academic_title_code`, `unit_name_freetext`) with API DTO naming in code.
---
## 6. Migration mechanics ([DB review §8](user-profile-manager-db-review.md))
1. **`user_staff_profiles` migration is additive** — avoids long **`ACCESS EXCLUSIVE`** on **`users`** for nullable-widen patterns.
2. **Backfill:** `INSERT … SELECT FROM users` with defaults **`draft`**, institutional columns NULL — same migration if row count modest; batched otherwise.
3. **NOT NULL rollout:** nullable column → **backfill** → `SET NOT NULL` in steps.
4. **`downgrade()`:** explicit (e.g. `DROP TABLE user_staff_profiles`), accepting HR data loss on rollback.
---
## 7. Implementation backlog (ordered)
1. **DDL + seed `academic_titles`** (+ migration conventions + **`TIMESTAMPTZ`** audit columns if partitioning introduced).
2. **SQLAlchemy models** — `UserStaffProfile`, relationship from `User`; avoid `SELECT *` antipatterns on wide auth paths.
3. **Transactional registration** — `User` + `UserStaffProfile` + `UserRoleRow` + registration audit branch.
4. **Applicant APIs** — extend **`RegisterBody`**, **`/me`**, **`PATCH /profile`**; join profile; **`version`**/`etag` semantics for PATCH.
5. **Admin APIs** — queue list indexes; verify/reject with **conditional UPDATE** + **409**/`200` policy; JSONB whitelist audit **in same txn**.
6. **Frontend** — `LoginRegisterCard`, `ApplicantProfileView`, **`/dashboard/users`**, sidebar permission alignment.
7. **Observability** — document idempotency and conflict responses in OpenAPI or internal README.
**MinIO:** unchanged unless attaching proof blobs later.
---
## 8. Testing matrix
Beyond API/integration tests originally listed:
| Class | Goal |
| ----- | ----- |
| **Constraint violations** | `INSERT`/`UPDATE` rows breaking `CHECK` / FK / partial unique → expect DB error surfaced as **4xx**/mapped. |
| **Transition matrix** | Every legal Mermaid transition succeeds; illegal transitions (**0-row UPDATE**) fail as specified. |
| **Concurrency** | Two DB connections / threads race verify → one wins; second behavior matches documented idempotency. |
| **Migration round-trip** | `upgrade → downgrade → upgrade` on seeded DB ([DB review §10](user-profile-manager-db-review.md)). |
---
## 9. Open questions (before merge)
Blocking “migration done”; align product then freeze DDL ([DB review §11](user-profile-manager-db-review.md)):
1. **`employee_id`:** required at register, optional, or required-before-`pending`?
2. **Re-verification:** strict (any PATCH from `verified` → `pending`) vs pragmatic (field whitelist).
3. **Duplicate verify:** idempotent **200** vs **409** — document in handler + client.
4. **`units` catalog:** authoritative? If yes, drop free-text arm; else XOR + backfill strategy.
5. **Audit:** retention horizon + partition cadence.
6. **`verified_until` / expiry:** periodic re-verification for HR compliance — schema + job vs defer.
---
## 10. File reference map
| Area | Path |
| ---- | ---- |
| Register UI | `fe0/src/auth/LoginRegisterCard.tsx` |
| Auth state | `fe0/src/contexts/AuthContext.tsx` |
| HTTP client | `fe0/src/lib/auth-service.ts` |
| Applicant profile UI | `fe0/src/components/applicant/profile/ApplicantProfileView.tsx` |
| Admin sidebar (Users link) | `fe0/src/components/admin/DashboardSidebar.tsx` |
| Routes | `fe0/src/App.tsx` |
| Register / login / profile API | `be0/src/auth_api.py` |
| User ORM | `be0/src/initiative_db/models.py` |
| MinIO abstraction | `be0/src/minio/storage.py` |
| DB engineering review | [`docs/user-profile-manager-db-review.md`](./user-profile-manager-db-review.md) |
---
*Refined against [`user-profile-manager-db-review.md`](./user-profile-manager-db-review.md): committed schema (`user_staff_profiles`), DB-level state invariants, concurrency (`version`/conditional UPDATE), audit JSONB whitelist + same-transaction writes, indexing and migration posture, expanded testing and open questions.*