Appearance
Console Security and Accounts
Role matrix
| Capability | MARKETING | ADMIN | SUPER_ADMIN |
|---|---|---|---|
| Catalogue/gallery/metadata/Benchmark | Yes | Yes | Yes |
| View catalogue image bytes | No | Yes | Yes |
| Propose catalogue media | Yes | Yes | Yes |
| Publish or restore media | No | Yes | Yes |
| Exact-user scan counter support | No | Yes | Yes |
| Mobile account support and scan statistics | No | No | Yes |
| Inspect synchronised private user collections | No | No | Yes |
| Manage staff role/activity | No | No | Yes |
| Change own password | Yes | Yes | Yes |
| Observability links | No | Yes | Yes |
| Generic database/table editing | No | No | No |
Hiding navigation is only a usability measure. Server endpoints and SQL capabilities enforce authorisation again. Creating a staff account never creates a mobile account. The staff namespace is separate, although both can exist on the same PostgreSQL instance.
Passwords
Passwords are stored as salted scrypt hashes, not plaintext. Each hash uses a fresh random salt and constant-time verification. A password is 14 to 256 characters. Unknown-account verification follows an equal-cost hash path. Passwords, hashes and database connection strings must not appear in responses, request logs, screenshots, tickets or repository files.
Operators create initial passwords through the interactive staff command or a controlled ephemeral provisioning process. Deliver an initial password privately; clipboard delivery is local and should be replaced by the recipient promptly. Do not paste it into the shared conversation. No universal bootstrap password exists.
Self-service change
Mon compte requires the current password, the new password and exact confirmation. The server determines the actor from the staff session and refuses caller-supplied target identifiers. All active staff roles may change their own password, not another staff member's password.
The restricted SQL function compares the previously verified hash under a lock to prevent a stale/concurrent password change. It only updates that actor's hash and writes an audit with actor/time, never password values or hashes. The runtime reader receives function execution, not arbitrary staff writes.
Successful changes revoke all sessions, clear the current cookie and return the browser to login. A non-secret password version makes any old session unusable even if an in-flight request re-saves it. Sessions predating this release require a new login. Operator password resets retain the same version invalidation.
Sessions and HTTP controls
- PostgreSQL-backed staff sessions, separate from mobile tokens.
- Rolling 30-minute session cookie, HttpOnly, Secure in production, SameSite Strict and scoped to the admin path.
- Exact configured Origin required for unsafe requests; forwarded host headers do not grant trust.
- Logout cross-site requests are refused when their fetch-site header indicates a foreign origin.
- Active role and password version checked against the database on authenticated requests.
- Responses containing console data use private/no-store caching.
- Login attempts limited to 10 per IP per 15 minutes, API requests to 120 per IP per minute, asset posts to 10 per minute.
- Password changes limited to 5 attempts per staff account per 15 minutes.
These limits are process-local, not a distributed brute-force service. Shared IPs can affect colleagues. There is no claim that rate limits alone prevent all abuse. Trust only the actual reverse-proxy source; do not globally enable trust for arbitrary clients.
Database and media boundaries
The reader role can read restricted catalogue views and staff authentication records, manage its session store and execute explicit support functions. The media publisher is separate. Neither is intended as a general database owner. SECURITY DEFINER functions pin their search path and revoke PUBLIC execution.
The SQL actor parameter is supplied by the trusted server. Theft of runtime database credentials is a separate serious threat; scoped functions do not substitute for protecting environment files and host access.
Private proposals and catalogue images must be distinct canonical directories. Private scan/debug directories are not mounted as catalogue assets. Source JSON is escaped and paths are validated. Uploads are decoded rather than accepted solely from their extension/MIME header.
Authenticated catalogue images
The console serves card and sealed artwork through same-origin /admin/catalogue/{cards|sealed}/{recordId}/images/{cataloguePath} URLs. The browser sends its existing HttpOnly staff cookie; no password or bearer token is placed in the URL. Anonymous requests receive 401; MARKETING receives 403. Active ADMIN and SUPER_ADMIN sessions may retrieve only images belonging to the requested catalogue record. Staff revocation and password-version checks apply to image requests too.
Paths are validated and resolved canonically inside the catalogue mount. Escaping symlinks, traversal, cross-record paths and private scan paths are refused. Sealed publication-rights eligibility remains a separate requirement: authentication does not change licensing metadata. Responses use Cache-Control: private, no-store. This console correction needs no schema migration, source-file change or mobile API/worker restart.
Mobile image delivery is a separate user-token authentication flow. A User-Agent, an app identity string or a public URL is not proof of authorisation. Until that flow is implemented and deployed, ingress blocks the old public image endpoint; older internal mobile builds can lose their artwork. Do not remove that block to repair the console: its staff-session route restores browser access independently. Shared links do not automatically grant access to catalogue images.
Previously downloaded or cached public images cannot be recalled. An authorised viewer can still save image bytes; authentication is access control, not DRM.
Private collection inspection
The CTO approved SUPER_ADMIN-only, read-only inspection of synchronised user holdings. This is separate from shared catalogue access and never impersonates a mobile user. ADMIN and MARKETING receive no collection access, even through a manually crafted API request.
The server takes the actor from its live-validated staff session. A POST endpoint requires the exact configured Origin, a target UUID and bounded filters; each response is private/no-store and limited to 25 live entries from that account. A narrowly granted SQL function checks active SUPER_ADMIN again and filters by the target user. Runtime receives no general SELECT/UPDATE on collection tables. As with existing support functions, the SQL actor parameter trusts the server; protect the runtime database credentials.
Successful reads append actor, target UUID, timestamp, kind, offset and returned count to a restricted journal. It contains no copied holdings or search text. If journaling fails, the read fails. Runtime cannot read, edit or delete journal rows. Define retention and restrict operator access; physical account deletion cascades to its journal entries.
Private notes, certification numbers, raw item JSON, tokens and scan photos are excluded. Only catalogue images are referenced, through their existing protected routes and rights restrictions. No export or collection-edit action is added.
Observability access
Grafana links do not replace its authentication. Public subpath access retains its login, secure cookies and disabled anonymous access/self-signup. Loki and Alloy endpoints remain private. Existing staff cookies are scoped to the admin application path, not the Grafana subpath. Users must have an explicitly provisioned Grafana account to read its dashboards/logs; do not infer that a console role creates one.
MFA and exposure decision
The CTO explicitly chose to expose the console with native staff authentication while deferring Authelia/MFA temporarily. Authelia was an additional protective layer, not an existing dependency that must be invented or configured silently.
Without MFA, a stolen staff password can grant its full role. Keep roles minimal, require unique passwords, disable departing users promptly and review audits. The SUPER_ADMIN role is especially sensitive. Revisit MFA before broader access or higher-risk administration features.
Caddy's control API remains disabled. It must not be exposed publicly to work around a reload failure. The application remains on a private listener behind HTTPS; public DNS is not permission to open PostgreSQL or the console port.
Legal and retention limitations
Audits may themselves contain personal identifiers. Restrict access and define a documented retention policy for staff records, sessions, media source/rights attestations, backups and support audits. Preserve lawful evidence without indefinite unnecessary storage.
Reversible deletion, TLS and role controls do not by themselves establish GDPR compliance. Data subject requests, complete erasure, retention, vendor contracts and incident response need their own verified procedures. These guides are operational documentation, not a legal certification.