Add curator roles/permissions with Users admin, and fix lineage branch joins.

Staff accounts use admin/curator roles and fine-grained flags; transitions connect source-to-target with color gradients and stream cutout masks so overlaps stay seamless.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-07-27 18:04:17 +03:00
co-authored by Cursor
parent bfa21989c9
commit 0466b77328
31 changed files with 1492 additions and 269 deletions
+57 -24
View File
@@ -30,7 +30,7 @@ curl.exe -sk https://devgallery.mysuperlab.netcraze.pro/api/bounds
## Authentication
Anonymous visitors have implicit role **`user`** (browse only). **Curator** accounts unlock debug mode, the Checkup page, and all mutating audit routes.
Anonymous visitors have implicit role **`user`** (browse only). Staff accounts in `users` are **`admin`** or **`curator`** with fine-grained **permissions**. Admins have all tools; curators only the flags assigned to them. Mutations are logged in `curator_audit_log` with `user_id`.
Sessions use an HTTP-only cookie (`gallery.sid`). The client sends `credentials: 'include'` on API requests.
@@ -42,19 +42,25 @@ Sessions use an HTTP-only cookie (`gallery.sid`). The client sends `credentials:
{ "role": "user" }
```
**Response (curator session)**
**Response (staff session)**
```json
{ "role": "curator", "username": "curator" }
{
"role": "admin",
"username": "curator",
"permissions": ["images", "checkup", "curator_notes", "translations", "influences", "tours", "users"]
}
```
`permissions` is the effective set (admins always receive the full list).
### `POST /api/auth/login`
**Body:** `{ "username": "curator", "password": "…" }`
**Response:** `{ "role": "curator", "username": "curator" }`
**Response:** same shape as `/me` for staff (`role`, `username`, `permissions`).
**Errors:** `401` invalid credentials, `400` missing fields.
**Errors:** `401` invalid credentials or disabled account, `400` missing fields.
### `POST /api/auth/logout`
@@ -62,30 +68,57 @@ Destroys the session cookie.
**Response:** `{ "ok": true }`
### Curator-only routes
### Permission flags
These return **`401`** with `{ "error": "Curator login required" }` without a valid curator session:
| Permission | Gates |
|------------|--------|
| `images` | Debug image/portrait fix/clear/upload/delete, debug search/proxy |
| `checkup` | Checkup page + checkup flag patches |
| `curator_notes` | `PATCH …/curator-notes` |
| `translations` | `/api/translations/*` |
| `influences` | `/api/influences/*` |
| `tours` | Tour admin CRUD |
| `users` | `/api/users/*` (Users page) |
| Route | Audit action (mutations only) |
|-------|-------------------------------|
| `GET /api/paintings/checkup` | — (read) |
| `GET /api/paintings/:id/debug-image-search` (+ `/more`) | — |
| `GET /api/artists/:id/debug-portrait-search` (+ `/more`) | — |
| `GET /api/debug/image-proxy` | — |
| `PATCH /api/paintings/:id/checkup-flags` | `painting.checkup_flags` |
| `PATCH /api/paintings/:id/curator-notes` | `painting.update_curator_notes` |
| `PATCH /api/artists/:id/checkup-flags` | `artist.checkup_flags` |
| `POST /api/paintings/:id/fix-image` | `painting.fix_image` |
| `POST /api/paintings/:id/clear-image` | `painting.clear_image` |
| `POST /api/paintings/:id/upload-image` | `painting.upload_image` |
| `DELETE /api/paintings/:id` | `painting.delete` |
| `POST /api/artists/:id/fix-portrait` | `artist.fix_portrait` |
| `POST /api/artists/:id/clear-portrait` | `artist.clear_portrait` |
| `POST /api/artists/:id/upload-portrait` | `artist.upload_portrait` |
Missing session → **`401`** `{ "error": "Curator login required" }`. Missing permission → **`403`** `{ "error": "Permission denied" }`.
### Users (admin / `users` permission)
| Method | Path | Notes |
|--------|------|-------|
| `GET` | `/api/users` | List users + known permission keys |
| `POST` | `/api/users` | Create `{ username, password, role, permissions }` |
| `PATCH` | `/api/users/:id` | Update `role`, `permissions`, `is_active` |
| `POST` | `/api/users/:id/password` | Set new password; clears that users sessions |
Only **admins** can create or promote **admin** accounts. Cannot deactivate/demote the last active admin. Audit: `user.create`, `user.update`, `user.reset_password`.
### Staff-gated routes
| Route | Permission | Audit action (mutations only) |
|-------|------------|-------------------------------|
| `GET /api/paintings/checkup` | `checkup` | — (read) |
| `GET /api/paintings/:id/debug-image-search` (+ `/more`) | `images` | — |
| `GET /api/artists/:id/debug-portrait-search` (+ `/more`) | `images` | — |
| `GET /api/debug/image-proxy` | `images` | — |
| `PATCH /api/paintings/:id/checkup-flags` | `checkup` | `painting.checkup_flags` |
| `PATCH /api/paintings/:id/curator-notes` | `curator_notes` | `painting.update_curator_notes` |
| `PATCH /api/artists/:id/checkup-flags` | `checkup` | `artist.checkup_flags` |
| `POST /api/paintings/:id/fix-image` | `images` | `painting.fix_image` |
| `POST /api/paintings/:id/clear-image` | `images` | `painting.clear_image` |
| `POST /api/paintings/:id/upload-image` | `images` | `painting.upload_image` |
| `DELETE /api/paintings/:id` | `images` | `painting.delete` |
| `POST /api/artists/:id/fix-portrait` | `images` | `artist.fix_portrait` |
| `POST /api/artists/:id/clear-portrait` | `images` | `artist.clear_portrait` |
| `POST /api/artists/:id/upload-portrait` | `images` | `artist.upload_portrait` |
| `/api/translations/*` | `translations` | `translation.*` |
| `/api/influences/*` | `influences` | `influence.*` |
| Tour admin (`/api/tours/admin`, POST/PATCH/DELETE, stops) | `tours` | `tour.*` |
| `/api/users/*` | `users` | `user.*` |
**Public** (no login): all catalog `GET` routes, `POST /api/artists/:id/preload-images` (local file linking for 3D halls), `/images`, SPA static.
Curator mutations are recorded in `curator_audit_log` (see [DB_structure.md](DB_structure.md)).
Staff mutations are recorded in `curator_audit_log` with `user_id` (see [DB_structure.md](DB_structure.md)).
---
+13 -6
View File
@@ -215,34 +215,41 @@ Unique index on `(painting_id, source_type, source_painting_id, source_artist_id
### `users`
Curator accounts (named logins). Anonymous site visitors do not have rows here.
Staff accounts (named logins). Anonymous site visitors do not have rows here. Migration: `db/migrate-auth.sql` + `db/migrate-user-roles.sql`.
| Column | Type | Notes |
|--------|------|-------|
| `id` | SERIAL PK | |
| `username` | VARCHAR(64) UNIQUE | Login name |
| `password_hash` | VARCHAR(255) | bcrypt hash |
| `role` | VARCHAR(32) | `admin` or `curator` (`users_role_check`) |
| `permissions` | TEXT[] | Fine-grained flags for `curator` accounts; admins are treated as having all |
| `is_active` | BOOLEAN | Soft-disable; inactive users cannot log in |
| `created_at` | TIMESTAMPTZ | |
| `last_login_at` | TIMESTAMPTZ | Updated on successful login |
First curator is bootstrapped on `npm run dev:migrate` when `users` is empty and `CURATOR_USERNAME` / `CURATOR_PASSWORD` are set in env.
**Permission keys:** `images`, `checkup`, `curator_notes`, `translations`, `influences`, `tours`, `users`.
- **`admin`** — all curator tools + **Users** management (role bypasses permission checks).
- **`curator`** — only assigned permission flags.
- First account is bootstrapped as **admin** on `npm run dev:migrate` when `users` is empty and `CURATOR_USERNAME` / `CURATOR_PASSWORD` are set. Manage additional accounts via the in-app **Users** page or `/api/users`.
### `curator_audit_log`
Append-only log of curator debug mutations (fix/clear/upload/delete, checkup flag changes).
Append-only log of staff mutations (fix/clear/upload/delete, checkup flags, translations, influences, tours, user management).
| Column | Type | Notes |
|--------|------|-------|
| `id` | BIGSERIAL PK | |
| `user_id` | FK → `users` | Who performed the action |
| `action` | VARCHAR(64) | e.g. `painting.fix_image`, `artist.upload_portrait` |
| `resource_type` | VARCHAR(32) | `painting` or `artist` |
| `action` | VARCHAR(64) | e.g. `painting.fix_image`, `user.create` |
| `resource_type` | VARCHAR(32) | `painting`, `artist`, `tour`, `user`, etc. |
| `resource_id` | INTEGER | Target row id |
| `details` | JSONB | Optional metadata (URL, mime type, flag values) |
| `ip_address` | VARCHAR(45) | Client IP (respects `TRUST_PROXY`) |
| `created_at` | TIMESTAMPTZ | |
**Logged `action` values:** `painting.fix_image`, `painting.clear_image`, `painting.upload_image`, `painting.delete`, `painting.checkup_flags`, `artist.fix_portrait`, `artist.clear_portrait`, `artist.upload_portrait`, `artist.checkup_flags`, `translation.upsert`, `translation.publish`, `influence.create`, `influence.update`, `influence.delete`, `influence.import`.
**Logged `action` values:** `painting.fix_image`, `painting.clear_image`, `painting.upload_image`, `painting.delete`, `painting.checkup_flags`, `painting.update_curator_notes`, `artist.fix_portrait`, `artist.clear_portrait`, `artist.upload_portrait`, `artist.checkup_flags`, `translation.upsert`, `translation.publish`, `influence.create`, `influence.update`, `influence.delete`, `influence.import`, `tour.create`, `tour.update`, `tour.delete`, `tour.stops`, `user.create`, `user.update`, `user.reset_password`.
Example query in pgAdmin:
+6 -3
View File
@@ -95,16 +95,19 @@ CURATOR_USERNAME=curator
CURATOR_PASSWORD=your-secure-password
```
Then open the gallery → **Curator login** (top-right) → use debug mode / Checkup / Translations / **Influences** / inline **curator notes** on painting detail. Mutations are logged in `curator_audit_log` (view in pgAdmin).
Then open the gallery → **Curator login** (top-right) → use tools allowed by your role/permissions (debug, Checkup, Translations, Influences, Tour editor, **Users**). Mutations are logged in `curator_audit_log` per user (view in pgAdmin).
If login fails after changing `CURATOR_PASSWORD` in `.env`, run `npm run dev:reset-curator` (bootstrap only runs when `users` is empty).
If login fails after changing `CURATOR_PASSWORD` in `.env`, run `npm run dev:reset-curator` (bootstrap only runs when `users` is empty; reset upserts the env account as **admin**).
**Roles:**
| Role | Access |
|------|--------|
| Guest (`user`) | Timeline, movement flow, 3D halls, painting detail, bios |
| Curator | Above + debug mode, Checkup, Translations, Influences (import/CRUD/graph), curator notes, image fix/upload/delete APIs |
| Curator | Public browse + assigned permission flags (`images`, `checkup`, `curator_notes`, `translations`, `influences`, `tours`, `users`) |
| Admin | All curator tools + **Users** page to create accounts with individual passwords and permissions |
**Users page:** after admin login, header → **Users** — create/edit staff, reset passwords, disable accounts.
**Audit log (pgAdmin on `gallery_dev` or `gallery_prod`):**
+2
View File
@@ -3,6 +3,7 @@ this file contains draft for future releases and features
## Standing requirements (do not regress)
- **Painting thumbnails:** any create/replace/clear of a paintings full picture must regenerate or remove its dedicated `paintings/thumbs/` file — never use the full image or a remote thumb URL as `thumbnail_path`. See [data-and-images.md — Painting thumbnail invariant](data-and-images.md#painting-thumbnail-invariant).
- **Staff auth:** mutating curator tools must check session + permission flags (`admin` bypasses flags); actions must log to `curator_audit_log` with `user_id`. See [basics.md — User roles](basics.md#user-roles-and-access) and [API.md — Authentication](API.md#authentication).
## Feature backlog
@@ -13,4 +14,5 @@ this file contains draft for future releases and features
5. ~~curator_audit_log should contain log of actions like fixit, checked, upload etc with details for which entity it was made and details what was the action and outcome~~ — done: table + `logCuratorAction` on fix/clear/upload/delete/checkup flags (and translation upsert/publish); see [DB_structure.md](DB_structure.md#curator_audit_log). (UI to browse logs is still item 3.)
6. ~~create search by entity (painting, artist, movement)~~ — done: timeline header + `GET /api/search`
7. ~~create guided tours (with text/extra infor, set of entities)~~ — done: `tours` / `tour_stops`, public Tours popup + 3D tour hall, curator Tour editor — [tours.md](tours.md)
8. ~~curator role + multi-user accounts with permissions~~ — done: `admin`/`curator` roles, permission flags, Users page + `/api/users`, per-user audit — [API.md](API.md#authentication) / [basics.md](basics.md#user-roles-and-access)
+19 -14
View File
@@ -247,8 +247,7 @@ Each visible movement is drawn as a **portrait-width curved stream** (~54 px str
|---------|----------------|
| Lineage layout | `client/src/data/movement-lineage.ts` — curated predecessor→successor pairs (Met / ArtStory / museum essays); multiple parents allowed |
| Vertical lanes | Movements whose time spans do **not** overlap (in the current zoom) share a horizontal lane; only concurrent spans stack into extra rows (`assignTemporalLanes`). A follow-up pass (`refineLanesForLineageCorridors`) pulls linked parent/child movements onto nearby or shared lanes when years allow, then displaces unrelated streams out of thick lineage branch corridors |
| Branch connectors | Smooth curves from fan-out points along a parent stream to the centre of each child stream; lanes are packed so those transitions do not cross through unconnected movements |
| Visual blending | Path-aligned SVG gradients with transparent fades at stream ends and branch junctions; streams draw on top of branches so overlap brightness stays uniform |
| Branch connectors | Smooth curves from fan-out points along a parent stream to the **left edge (start)** of each child stream; color gradients from parent → child at constant opacity; a stream-shaped mask hides branch ink under movements so translucent overlaps do not brighten the bands |
| Filtering | A movement is drawn when its **span overlaps** the visible year range **and** it has at least one catalogued artist — artists whose lifespan falls outside the window still keep their movement visible (their portraits simply do not render). Filtered client-side after initial load |
| Viewport layout | Row height and stream width scale from measured canvas size so every visible movement row fits in the remaining screen space |
@@ -441,28 +440,34 @@ Next to the toggle, **Show more** (checkbox, persisted in `localStorage`) opens
| Role | Who | Can do |
|------|-----|--------|
| **`user`** | Anonymous visitor (default) | Browse timeline, movement flow, 3D artist/movement halls, painting detail, artist bios, images |
| **`curator`** | Named account (`users` table) | Everything above + **Debug mode**, **Checkup**, **Translations**, **Influences**, **Tour editor**, debug API mutations |
| **`curator`** | Named staff account | Public browse + tools allowed by their **permission flags** |
| **`admin`** | Named staff account | All curator tools + **Users** management |
Curators sign in via **Curator login** in the site header. Sessions use an HTTP-only cookie (`gallery.sid`). The UI hides debug controls from guests; the server enforces the same rules on debug/checkup API routes (`401` without a valid session).
**Permission flags:** `images`, `checkup`, `curator_notes`, `translations`, `influences`, `tours`, `users`. Admins always have every flag.
Mutating debug actions (fix/clear/upload/delete, checkup flag changes) are appended to **`curator_audit_log`** with username, action, target id, optional JSON details, and client IP. Query in pgAdmin — see [DB_structure.md](DB_structure.md#curator_audit_log).
Staff sign in via **Curator login** in the site header (individual username/password). Sessions use an HTTP-only cookie (`gallery.sid`). The UI shows only tools the account may use; the server enforces the same rules (`401` without a session, `403` without permission).
Admins create and manage accounts on the **Users** page (`UsersPage.tsx` / `/api/users`). Bootstrap the first admin with `CURATOR_*` env vars + `npm run dev:migrate` (or `npm run dev:reset-curator`).
Mutating actions are appended to **`curator_audit_log`** with `user_id`, action, target id, optional JSON details, and client IP. Query in pgAdmin — see [DB_structure.md](DB_structure.md#curator_audit_log).
## Developer tools (image audit)
Curator-only workflow for reviewing and fixing local image files — not part of the public visitor experience.
Staff workflow for reviewing and fixing local image files (requires **`images`** permission) — not part of the public visitor experience.
| Feature | Where | Purpose |
|---------|--------|---------|
| **Catalog search** | Timeline header (all visitors) | Find artists, paintings, movements; `GET /api/search`; navigate to gallery or detail |
| **Curator login** | Home header (guests) | Username + password modal; unlocks debug tools |
| **Debug mode** | Home header toggle (curators only) | Persists in `localStorage`; enables debug panel on painting detail and artist bio |
| **Show more** | Home header checkbox (curators, when debug on) | Auto-opens the **More** modal on each painting / bio page load |
| **Checkup page** | Home header → **Checkup** (curators only) | Full-catalog table: gallery vs detail thumbnails, search, fix, review flags |
| **Translations** | Home header → **Translations** (curators only) | Review/publish Russian `entity_translations` |
| **Influences** | Home header → **Influences** (curators only) | List/CRUD influence edges, CSV/JSON/XLSX import wizard, neighborhood graph — [influence-import.md](influence-import.md) |
| **Tour editor** | Home header → **Tour editor** (curators only) | Create/publish guided tours and stop text — [tours.md](tours.md) |
| **Curator login** | Home header (guests) | Username + password modal; unlocks permitted tools |
| **Debug mode** | Home header toggle (`images`) | Persists in `localStorage`; enables debug panel on painting detail and artist bio |
| **Show more** | Home header checkbox (`images`, when debug on) | Auto-opens the **More** modal on each painting / bio page load |
| **Checkup page** | Home header → **Checkup** (`checkup`) | Full-catalog table: gallery vs detail thumbnails, search, fix, review flags |
| **Translations** | Home header → **Translations** (`translations`) | Review/publish Russian `entity_translations` |
| **Influences** | Home header → **Influences** (`influences`) | List/CRUD influence edges, CSV/JSON/XLSX import wizard, neighborhood graph — [influence-import.md](influence-import.md) |
| **Tour editor** | Home header → **Tour editor** (`tours`) | Create/publish guided tours and stop text — [tours.md](tours.md) |
| **Users** | Home header → **Users** (`users` / admin) | Create staff accounts, roles, permissions, reset passwords, disable accounts |
| **Tours** | Home header → **Tours** (everyone) | Open published tours in a 3D hall — [tours.md](tours.md) |
| **Logout** | Home header (curators) | Ends session; hides debug tools |
| **Logout** | Home header (staff) | Ends session; hides staff tools |
| **Debug panel** | Painting detail or artist bio (bottom-left, when debug mode on) | Search preview + action buttons (six on painting detail, five on artist bio) |
### Debug panel (painting detail and artist bio)
+1 -1
View File
@@ -117,7 +117,7 @@ npm run infra:db:split-dev-prod
CURATOR_PASSWORD=your-secure-password
```
Omit `SESSION_COOKIE_SECURE` so cookies follow the request scheme (`TRUST_PROXY` + HTTPS → Secure). Set `true`/`false` to force. `npm run dev:migrate` creates auth tables and bootstraps the first curator when `users` is empty. Reset password later with `npm run dev:reset-curator`.
Omit `SESSION_COOKIE_SECURE` so cookies follow the request scheme (`TRUST_PROXY` + HTTPS → Secure). Set `true`/`false` to force. `npm run dev:migrate` creates auth tables/roles and bootstraps the first **admin** when `users` is empty. Reset that account later with `npm run dev:reset-curator`. Create additional staff via the in-app **Users** page.
2. Run: