Skip to content

v0.4.6 — audit remediation, privacy retention, release polish

Pre-release
Pre-release

Choose a tag to compare

@solarssk solarssk released this 27 Jun 22:42
v0.4.6
be06717

[0.4.6] - 2026-06-27

Changed

  • Admin and operator check-in: desktop camera renders inline in the main panel (stats sidebar stays visible); mobile keeps fullscreen camera overlay; operator mobile autostart with "Use camera" button to reopen after close; desktop inline shows lookup errors in the scan bar area and renders full AttendeeCard (items, notes, undo) below the camera preview
  • Attendee note modal includes a one-line reminder not to record medical, dietary, or other sensitive personal data
  • Staff SPA Content-Security-Policy allows HTTPS branding logo URLs in setup wizard and settings preview (aligned with existing font-src https: for theme fonts)
  • Privacy docs: retention tables distinguish product-automated cleanup (sessions, email snapshots) from operator-controlled data (IP logs, attendee lists); OIDC IdP group membership documented; attendee erasure documented as API-only (no SPA delete button)
  • SECURITY.md: Trivy HIGH remediation SLA (30 days when fix available); blocking HIGH gate deferred to v1.0
  • deploy/README.md: container startup documents auth-state and email snapshot retention purge steps
  • README: MFA first-login flow notes backup-code acknowledgment is persisted in the database

Added

  • Account self-service at /account for all signed-in staff: profile, password change (re-auth), TOTP enrollment/reset, and session management (/api/account/*)
  • Nullable User.password_hash for OIDC-only accounts (additive migration; existing rows unchanged)
  • IAM Users & roles page at /admin/users: staff user table (search, filters, pagination), invite-user modal, edit-user modal with role management, reset MFA/password/sessions, and role-assignments tab
  • GET/POST/PATCH /api/admin/users and role grant/revoke, reset-2fa, reset-password, revoke-sessions endpoints with anti-lockout guards and audit logging (superadmin; org admin may grant/revoke operator@event only)
  • GET /api/admin/role-assignments for non-instance role grants
  • must_change_password on User with server-rendered /change-password flow after login or admin password reset
  • First-run setup: server-rendered /setup for empty-database superadmin bootstrap; 5-step React onboarding wizard (system checks, mail, org branding, first event, completion) gated by setup_complete in SystemSettings
  • Settings audit log viewer: superadmin-only paginated table of AdminAuditLog entries with action-type and date filters (GET /api/admin/audit-log)
  • Event reports page at /admin/events/:id/reports: admission stats, hourly CSS chart, ticket-type breakdown, paginated admission log, CSV export, and printable HTML/PDF export via GET /api/admin/events/:eventId/reports and /reports/export; exports write reports_exported to the event audit log
  • Admin attendee erasure now has a GDPR-ready DELETE /api/admin/events/:eventId/attendees/:id path that removes dependent delivery, wallet, and check-in rows in one transaction while preserving an event-scoped audit entry (PRIV-001, PRIV-004)

Fixed

  • Check-in camera result panel shows the actual admission timestamp on repeat scans instead of a hardcoded "Entered earlier today" subtitle
  • Attendee note modal privacy hint is linked to the textarea via aria-describedby for screen readers
  • Ops health/readiness rate limiters import audit logging via @admitto/auth/audit so unit tests do not load the full auth barrel (flaky PrismaClient init in CI)
  • Account page (/account): profile Save disabled when unchanged; live password-confirm mismatch feedback; Spinner loading states; SPA Link navigation to account from staff/operator shells
  • @admitto/auth runInTransaction no longer value-imports PrismaClient, avoiding flaky healthz-rate-limit unit tests when the auth barrel loads before prisma generate
  • First-run POST /setup bootstrap race: Serializable transaction re-checks empty user table so only one superadmin can be created
  • IAM anti-lockout guards (last_superadmin on role revoke and superadmin deactivation) run atomically in Serializable transactions; idempotent role DELETE returns 204 instead of 500 when the assignment is already gone; 404 when the assignment exists under a different user id in the URL
  • Setup wizard no longer bypassed on /operatorsetup_complete included on /api/auth/me for instance superadmins
  • Event slug helper truncates before trimming trailing dashes; wizard step 4 disables Continue when slug is empty and uses max slug length 80 (aligned with API and CreateEventModal)
  • DB partial unique index enforces at most one instance-scoped superadmin RoleAssignment
  • Admin mutations that write AdminAuditLog now persist audit rows in the same database transaction as the primary change (users IAM routes, mail settings PUT); audit failure rolls back the mutation instead of leaving inconsistent state (BE-001, BE-002)
  • Concurrent attendee import commits for the same event are serialized with a PostgreSQL advisory lock so duplicate bulk audit rows cannot be written (BE-004)
  • Agency public_ref backfill processes attendees in bounded batches instead of loading all rows at once (BE-005)
  • User deactivation revokes sessions after the Serializable user-update transaction commits, avoiding serialization conflicts with concurrent last_seen_at updates (Bugbot)
  • Import commit transaction timeout raised to 120s so a second commit can wait on the per-event advisory lock without aborting mid-queue (Bugbot)
  • Attendee.rsvp_status now has a database CHECK constraint matching the application enum, preventing invalid RSVP states from raw SQL or future scripts (DATA-002)
  • OidcRoleGrant now uses partial unique indexes for scoped and instance grants, correctly enforcing uniqueness when scope_id is NULL (DATA-004)
  • Attendee, check-in, and event item state status columns now have database CHECK constraints matching persisted application values (DATA-003)
  • Destructive migration scanning now flags DELETE FROM DML in new Prisma migrations unless explicitly approved (DATA-006)
  • Staff account creation audit metadata and import CLI skipped-row output now redact email addresses, and privacy/DSAR docs now document attendee-note special-category risk plus manual erasure FK ordering (PRIV-001, PRIV-002, PRIV-003, PRIV-004, PRIV-006)
  • Container startup now wraps the agency public_ref backfill in a 120-second timeout, and PR-Agent comments are limited to collaborators/members/owners to prevent public API-credit drain (INFRA-002, INFRA-003)
  • Container startup now attempts a best-effort purge of expired/revoked Session and TrustedDevice rows after migrations/backfills with a 120-second timeout; operators can also run npm run cli -w @admitto/auth -- purge-auth-retention --dry-run to preview counts, reducing stale auth-state retention (DATA-001, PRIV-005)
  • Container startup now nullifies stale EmailDelivery.rendered_html / rendered_subject snapshots on terminal deliveries older than 60 days (configurable via EMAIL_DELIVERY_SNAPSHOT_RETENTION_DAYS); operators can preview counts with npm run cli -w @admitto/mail-delivery -- nullify-delivery-snapshots --dry-run (DATA-005, PRIV-001)

Security

  • Forced password change is now enforced as a dedicated change_password_required session stage: a user whose password was reset by an admin cannot reach any protected route (API or UI) until they set a new password — the previous next: change_password hint was a UI directive only and could be ignored by any HTTP client (IAM-001)
  • Backup recovery codes must be acknowledged before a full session is granted, even after a fresh login or when the completion request lands on a different process: acknowledgment is now persisted on UserMfaMethod (backup_codes_acknowledged_at) instead of an in-memory stash, closing a bypass where a returning user could skip saving recovery codes (IAM-002)
  • Forced password-change form now enforces the same 12-character minimum as all other self-set passwords (was 8) (IAM-003)
  • Granting superadmin@instance to a second user now returns HTTP 409 single_superadmin_limit instead of an unhandled 500 (IAM-004)
  • Known limitation: OIDC group→role mappings are fully reconciled at OIDC login only; deployments using OIDC-managed admin/superadmin roles should set admin session TTL to 8h or less and follow the documented offboarding runbook (accepted risk, IAM-005)
  • Attendee CSV export now uses the shared CSV formula-injection sanitizer (covers newline-prefixed formulas and whitespace-padded =) across attendee, event-settings PII, and reports exports (SEC-001)
  • Printable HTML/PDF event report export now sends a restrictive Content-Security-Policy header (SEC-003)
  • EmailDelivery.error sanitization now redacts URLs (e.g. Power Automate webhooks) before persistence (BE-006)
  • REVOKED check-in audit rows are written inside a transaction for consistency with other check-in paths (BE-003)
  • Known limitation: automated post-event attendee PII purge deferred to v1.0 — use Attendees export + per-attendee DELETE API (no SPA delete button yet)
  • Known limitation: OIDC group→role reconciliation runs at OIDC login only — shorten admin session TTL and follow offboarding guidance in SECURITY-CONTROLS.md
  • Known limitation: Account page TOTP enrollment shows an otpauth:// URI string; HTML /mfa/enroll shows a QR code — SPA QR deferred to v0.4.7
  • Known limitation: ticket token may appear in server access logs when operators open ticket URLs — accepted risk; control via reverse-proxy log retention

Deploy

  • Container image: ghcr.io/solarssk/admitto:0.4.6 (rolling :0.4)
  • Migrations apply automatically on container start (with pre-migration backup when pending). No manual migrate deploy.

Full changelog