286 lines
16 KiB
Markdown
286 lines
16 KiB
Markdown
# 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-machine–driven** 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.*
|