# 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 | `` | Stays **`users.full_name`** (already collected as `fullName`). | | Mã số nhân sự | `` | **`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” `` | **`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 | `` 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) | `` | **`job_title`** in DB/API — never overload JWT **`roles`**. Nullable for non-person accounts; **length capped** (`CHECK` or varchar). | | Email / SĐT | `` | 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.*