Releases: solarssk/admitto
Releases · solarssk/admitto
Release list
v0.4.12 — Identity SPA, overview command centre, and check-in hardening
[0.4.12] - 2026-07-06
Added
- Vitest code coverage (
npm run coverage) across workspaces with LCOV upload to Codecov on CI — reporting only, no coverage gate yet. - CI PR pipeline shortened: lint merged into
build-test, Semgrep and Docker build smoke run onmainmerge (not every PR);SECURITY.mddocuments when each control runs. - Identity providers + Cloudflare Access JSON API (
/api/admin/identity/providers*,/api/admin/identity/cf-access*) for the SPA Settings → Identity migration: list, get, create, update, toggle, discover, test, and CF Access get/update/test endpoints. Reuses@admitto/authlogic unchanged; gated byrequireAdminAccess(superadmin) to match the legacy HTML routes. PUT contract:mappingsis required on every PUT (replace-all, mirroring the HTML form; omitting it returnsmappings_requiredso editing other fields can never silently delete mappings) and omittinglogin_button_labelpreserves the stored value (null/""clears); create defaults omittedmappingsto[].mappingSchemaenforcesscope_idfororganization/eventscopes. Toggle uses a conditionalupdateManyand returns409on a concurrent toggle (TOCTOU-safe). CF Accesstestendpoint shares theadminAuthProviderOpsRateLimitbucket with OIDC discover/test. Schemas usez.strictObject()(zod v4). Legacy HTML routes remain until the SPA editor lands (#266). - Settings → Identity & SSO SPA overview (#266 slice 2): new
/admin/settings/identity/*routes under the existingInstanceSettingsShellrender the OIDC providers list (with optimistic enable/disable toggle) and a Cloudflare Access summary card. The Settings "Identity" tab and the legacy?tab=identityquery now hand off to the canonical/admin/settings/identity/providersroute, keeping the SPA shell consistent instead of jumping to raw HTML. Add/Edit provider and CF Access Manage still bridge to the legacy HTML editors until the SPA editor lands in slices 3–4. - Identity provider SPA editor — Basics, Endpoints, Claims, login button label (#266 slice 3a): new
/admin/settings/identity/providers/newand/identity/providers/:providerIdroutes render a full form editor under the SPA shell. Create POSTs a new provider; edit loads by id and PUTs the full form (mappings carried through unchanged until the repeater lands in slice 3b). Client-side validation mirrors the slice-1 Zod contract; the stored client secret is preserved on edit when left blank; a dirty guard warns on navigation. The providers list Add/Edit actions now SPA-navigate to these routes instead of the legacy HTML bridge. - Identity provider SPA editor — group→role mapping repeater, Discover/Test, SSO preview (#266 slice 3b): the editor gains an editable mapping repeater (add/remove rows, role + scope selects, conditional scope_id) with replace-all save semantics; Discover autofills OIDC endpoints from the issuer's
.well-knownconfig and Test probes the connection (edit mode only, both with 401 routing to login); a live SSO login button preview reflects the custom label or the product default. Mapping validation (group required, scope_id required for organization/event scopes) blocks save with inline row errors. - Identity provider SPA editor — draft test and discover in create mode (#266):
POST /api/admin/identity/providers/testandPOST /api/admin/identity/providers/discover-previeware stateless endpoints that probe OIDC connectivity and autofill endpoints without requiring a saved provider record; Discover and Test connection are now available in create mode (previously edit-only). Partial endpoint sets (fewer than all three of auth/token/jwks) fall back to discovery so the test always reflects the same endpoint resolution as save. Issuer URL is validated against the same SSRF guard as the save path before any explicit-endpoint test. Save and Cancel buttons are disabled while discovery is in flight; stale discover and test responses are silently discarded when the issuer or endpoint draft diverges mid-flight. - Cloudflare Access SPA editor (#266 slice 4): new
/admin/settings/identity/cloudflareroute renders the CF Zero Trust config editor under the SPA Identity sub-tab, replacing the slice-2 placeholder. Edits team URL, Application Audience (AUD), and protected URL paths (comma-separated lists), toggles enabled, and Test connection probes the team domain's JWKS endpoint (sends the draft team URL so operators can test before saving). Per-field env locks disable the locked inputs and show a "Locked by env" badge; the PUT body omits locked fields so the server keeps the env-managed value. A dirty guard (routeruseBlocker+beforeunload) and 401 routing to login match the OIDC provider editor patterns. The Identity overview "Manage" action now SPA-navigates here instead of bridging to the legacy HTML editor. - HTTP access log on
appcontainer stdout (LOG_HTTP_REQUESTS, on by default in deploy compose): one JSON line per request with method, redacted path, status, and duration — no IPs, query strings, or ticket/QR tokens; successful health probes are skipped. Documented per-container log expectations indeploy/README.md(#237). - Password strength meter on first-run
/setup, forced/change-password, and admin Account password change — text label plus segmented bar (not color-only); confirm fields show match feedback on setup and change-password pages (#226). - Overview page: per-event Pinned note — short operational sticky visible to all admins; editable inline, highlighted in the right column (#291).
- Overview page: per-event Key contacts — list of on-site contacts with name, role, phone and email action links; add/edit/delete inline (#291).
- Overview page: per-event Important links & files — list of linked documents and URLs with title and optional description; add/edit/delete inline, shows first 4 with "View all" toggle (#291).
- Audit log: all mutations to pinned note, key contacts, and important links are recorded in
AdminAuditLogwith actor, session, IP, and action type — visible to superadmins in the Audit viewer (#291).
Changed
- Settings shell unified for Identity IA (#266 slice 7b):
SettingsLayoutwraps/admin/settings/*so the primary tabs (General | Mail | Security | Archiving | Identity) remain visible on the Identity overview and on all detail views (add/edit provider, Cloudflare Access editor). The second tab row (Providers | Cloudflare Access) is removed — Identity overview is a single page with both cards. The?tab=identitylegacy query still redirects to the canonical/admin/settings/identity/providersroute. Editor dirty guards (useBlocker) still trigger when switching primary tabs or leaving detail views. - Docs: sync CI/security narrative (Semgrep on
main, Codecov data note, required merge checks), fix stale deploy examples (Node 24), README documentation map, identity SPA status in admin README, contributor coverage commands. - Overview page: redesigned as an event command center — "Quick actions" nav grid removed; new two-column layout with Needs attention alerts (email failures, queued tickets, missing operators), Event readiness checklist (attendees, tickets, delivery, operators), Email delivery breakdown, live Recent check-ins feed (SSE), and compact Event info block (#276).
- Account page: sidebar nav structure now consistent with AdminShell — "My account" link is in the nav area (not the footer), back link stays in the footer; empty aria-hidden placeholder div removed (#267).
- Reports: tile/panel spacing is now correct —
--space-md/sm/lgand--surface-elevatedwere undefined tokens resolving to 0/transparent; replaced with--space-4/3/5and--surface-sunkenthroughoutreports-page.css. Progress bars in "By ticket type" use--primaryconsistently (removed threshold-based traffic-light coloring that flagged <50% admission as a warning at the start of every event). Hourly chart is accessible by keyboard and screen reader — bars have individualaria-labelwith hour and count;role="img"removed so the accessibility tree is not flattened (#269). - Topbar: mailer status indicator now uses the shared
Badgecomponent (pill with dot) instead of bespoke markup — visually consistent with every other status badge in the app; label still hides at narrow viewports (#275). - Check-in:
AttendeeCardstatus display now differentiates positive, warning, and blocking-error states — VALID/PREVIEW show status inline in the identity header; ALREADY_CHECKED_IN uses a compact warning strip; REVOKED/INVALID show a unified tinted alert block with status and reason text merged into one message. Item action buttons (Give gift bag, Return headset) use chip-matching geometry without a border or background (#270). - Toast notifications: unified design-system stack (Tabler icons, deduplicated messages, bottom-right placement); admin pages and settings panels use
useToast()for save/load feedback instead of inline status text. - Account page: profile, password, and MFA success/error feedback uses toasts instead of inline status text; locale-change reminder stays until dismissed (#239).
- Toast stack z-index sits below check-in camera overlay so mobile lookup warnings do not cover overlay controls.
- Setup wizard mail step: provider select order/labels, per-provider field grouping (SMTP username+port grid), and test-send row aligned with design mockup.
- First-run routing: unauthenticated staff entry (
/,/login,/admin,/operator, and related HTML gates) redirects to/setupuntil the first user exists; login form is shown only after bootstrap. - Setup wizard system check: allow
http://127.0.0.1/localhostBASE_URL in production (local Docker smoke); non-loopback HTTP still fails. - First-run mail wizard: ignore deploy env placeholders for field locks and test send until setup wizard completes (
setup_complete). - Setup SSR (
/setup): mockup-aligned copy, login-aligned `autocompl...
v0.4.11 — CI stability patch
[0.4.11] - 2026-07-02
Fixed
- Redis rate-limit integration test: wait for fresh fixed window before asserting block (flaky
build-testonmainafter v0.4.10)
Deploy
- Container image:
ghcr.io/solarssk/admitto:0.4.11(rolling:0.4) - Migrations apply automatically on container start (with pre-migration backup when pending). No manual
migrate deploy.
v0.4.10 — Multiple superadmins, OIDC floor-guard, emergency CLI
[0.4.10] - 2026-07-02
Changed
- Remove instance-superadmin ceiling: multiple active
superadmin@instanceassignments are allowed; OIDC group→superadmin grants are no longer capped at one. The dropped index was redundant with the Serializableuser.count()guard inPOST /setup; first-run bootstrap protection is unchanged. - Consolidate rate-limit factories into declarative policy registry (no behavior change)
- Centralize MFA enroll rate-limit constants in
RATE_POLICIES; add registry edge-case and wiring tests - Extract inline-only rate limits to
INLINE_RATE_LIMITS(excluded fromRatePolicyName; compile-time guard againstrateLimit()misuse) - Remove unused SSE message variant and stale nginx metrics location; fix dangling ADR links in deploy docs
- Docs: superadmin runbook for multiple instance admins and OIDC offboarding prerequisites (
SECURITY-CONTROLS.md,deploy/README.md)
Security
- Fix event-settings GET authz-order oracle (404→403 for cross-org probing of non-existent events); add defense-in-depth
assertEventManageAccesstohandlePatchEventhandler body (route wrapper already enforced scope); reduce QR image cache TTL from 24h to 5min - Emergency CLI attendee export: enforce mode 0600 on overwrite; writable
emergency-exportsbind mount; reject--outunder publicUPLOAD_DIRor outsideEMERGENCY_EXPORT_DIRwhen those env vars are set; rejectEMERGENCY_EXPORT_DIRwhen it is a public alias underUPLOAD_DIRraw or realpath (including symlinked upload roots); require raw--outunder rawEMERGENCY_EXPORT_DIR(not only canonical realpath); resolve symlinks before path checks; write via validated canonical path withO_NOFOLLOW(loop until full buffer is written)
Fixed
POST /setupmaps Serializable transaction conflicts (P2034) to409 already_initializedwhen two first-run submissions race with different emailsbootstrap-superadmin --forcerecovery path works after removing the single-superadmin partial unique index- OIDC group-sync cannot revoke the last active instance superadmin (floor-guard with audit event
auth.oidc.superadmin_revoke_blocked; Serializable transaction on active instance-superadmin revoke; inactive owners skip the floor check; retriesP2034serialization losers so concurrent OIDC logins do not fail)
Added
- CLI:
admittoemergency ops binary (apps/cli) — checkin admit/lookup, attendees export, mail retry-failed, auth bootstrap-superadmin/reset-mfa, sessions revoke/purge, retention run - Automated retention cron (auth sessions, mail delivery snapshots) and nightly pg_dump backup sidecar in deploy compose
Deploy
- Container image:
ghcr.io/solarssk/admitto:0.4.10(rolling:0.4) - Migrations apply automatically on container start (with pre-migration backup when pending). No manual
migrate deploy.
v0.4.9 — SSE live check-in and multi-template mail
[0.4.9] - 2026-07-02
Added
- Admin: Instance URL setting (Settings → General) for email logo absolute URLs when BASE_URL env is unset
- Backend: live check-in SSE at
GET /api/checkin/events/:eventId/stream(operator/admincanPerformCheckInauth) - Backend: multiple email templates per event (
name/label, CRUD under/api/admin/events/:eventId/templates) - Backend: bulk send
POST /api/admin/events/:eventId/sendwithtemplateId, recipient filters, anddryRunrecipient count - Backend: send batch status
GET /api/admin/events/:eventId/send/status/:batchId - Backend: per-template test send
POST /api/admin/events/:eventId/templates/:templateId/test-send EmailDelivery.template_idforeign key toMailTemplate(delivery audit per template)- Admin SPA:
useEventStreamhook for live check-in SSE with reconnect and auth-error heuristic - Admin SPA: Check-in page live feed (prepend history, dedup, offline banner)
- Admin SPA: Event overview optimistic
admitted_countfrom SSE - Admin SPA: Communication page multi-template editor, bulk send dialog with dry-run and batch polling
Changed
- Docs: align contributor roadmap in
AGENTS.md,README.md, andVERSIONING.md(v0.5 ingest API → v0.6 Wallet → v0.7 RSVP);AGENTS.mdpoints atCHANGELOG.mdand the open GitHub milestone instead of a hardcoded active milestone
Fixed
- Admin: corrupt uploaded logo files clear the upload value in LogoUploadZone and show an error; external HTTPS URLs keep the value for manual correction
- Admin: attendee resend and bulk resend use DB instance URL for ticket links when BASE_URL env is unset
- Instance URL validation rejects bare
?or#delimiters (prevents malformed ticket and QR link paths) - Admin SPA: event overview reuses check-in TTL dedup map for SSE admits (no full clear on server refresh; TTL prune on poll keeps map bounded)
- Admin SPA: communication page refetches inherited ticket template on each virtual-ticket selection (avoids stale legacy cache)
- Admin SPA: communication page clears editor actions after delete when ticket fallback load fails (avoids targeting deleted template; re-select or create reloads editor)
- Admin: revoke and restore pass from attendees list
- Email templates (reminder and custom) can be deleted even after deliveries were sent (delivery log keeps rows; template reference cleared)
Deploy
- Container image:
ghcr.io/solarssk/admitto:0.4.9(rolling:0.4) - Migrations apply automatically on container start (with pre-migration backup when pending). No manual
migrate deploy.
v0.4.8 — capacity enforcement, branding upload, and admin UX
[0.4.8] - 2026-07-01
Fixed
- Event capacity: pass restore (
status: registered) respects capacity limits; manual create and import share an advisory lock to prevent concurrent over-capacity writes - PATCH reactivation from
cancelledorrevokedtoregisteredenforces capacity the same way as manual create - Event overview
attendee_countexcludes revoked attendees (aligned with capacity enforcement) - Event overview
admitted_countuses the same active scope asattendee_count(excludes revoked/cancelled) - Event capacity counts exclude
cancelledas well asrevokedpasses - CSV import capacity override (
?force=1) recordsforced: trueinattendees_importedaudit metadata - Import overwrite-only commits allowed when
toCreate === 0even if event is already over capacity - Event overview email card surfaces
email_bouncedseparately from failed deliveries - Branding upload validates magic bytes and uses async filesystem I/O
Added
- Admin: logo upload zone in setup wizard (server upload or external HTTPS URL)
- Admin: bounce alert on Communication page with link to delivery log
- Backend:
saveEventUploadhelper for event-scoped branding paths (/uploads/{orgId}/events/{eventId}/…; no HTTP endpoint yet) - Branding save accepts validated
/uploads/…logo paths in addition to HTTPS URLs - Email template render absolutizes
/uploads/…branding assets usingBASE_URL(required for logo in outbound mail) - Admin: revoke and restore pass on attendee detail (PATCH
status, capacity-aware restore) - Admin: CSV import shows
event_fullcapacity banner; superadmin can override with force commit - Admin: TOTP enrollment QR code on Account page
- Admin: device label pre-filled from browser user agent
- Admin: sidebar pin/unpin (desktop), lifecycle nav labels (Passes, Post-event), Administration section
- Event capacity enforcement on manual attendee create and CSV import commit: returns
409 event_fullwhen the limit would be exceeded; instance superadmin may override with?force=1(audited) PATCH /api/admin/events/:eventId/attendees/:idsupportsstatus: registered | revokedwithpass_revoked/pass_restoredattendee action log entries- Local branding upload API:
POST /api/admin/uploads(PNG/JPG/WebP, max 2 MB, superadmin-only) andGET /uploads/*static serve; Docker Compose volume for./uploads - Event overview: separate
email_bouncedcount distinct fromemail_failed(failed + rejected only) - Attendee status
revokedin database (migration) — revoked passes are not admittable at check-in
Deploy
- Container image:
ghcr.io/solarssk/admitto:0.4.8(rolling:0.4) - Migrations apply automatically on container start (with pre-migration backup when pending). No manual
migrate deploy.
v0.4.7 — overview, locale, requirements runtime, and export hardening
[0.4.7] - 2026-06-30
Security
- Rate-limit admin export endpoints: PII export 5/h, attendees/reports export 10/h per user per route (global across events)
Added
- Bulk Send tickets on the attendees list:
POST .../attendees/bulk-resendwithtargetunsent(default,purpose: initialwith atomic claim) orall(resend, max 500 per request); rate limit 3 requests per 10 minutes per admin; response reports provider-accepted (queued),skipped, andfailedcounts; confirmation modal in admin SPA; audit viamail_bulk_resendin attendee action log - CSV import preview: first 20 valid rows returned as
sampleRowswithattributeFieldLabels; admin Import page shows a scrollable "Data preview" table before commit (dynamic optional columns + event custom attributes) - Requirements v2 (admin): Tabler icon picker on event items (
EventItem.icon); contents metadata (type,required,options); ops-config flagsallow_manual_lookupandauto_advance_on_valid(defaults true) - Check-in runtime: enforces
allow_manual_lookup(403 on lookup API; UI hides manual lookup and blocks short-query lookup);auto_advance_on_validclears scan state after VALID admission;GET /api/checkin/ops-config; item icons on AttendeeCard (Tabler) - Contents metadata runtime: enforce
type/select/boolean/required/optionson attendee create and patch; type-aware admin fields; required markers and formatted values on check-in item detail - CSV import: dynamic event-item attribute columns (
source_fieldslugs) validated and stored incustom_data; template includes configured fields; export-style label headers accepted on re-import - Per-user preferred locale (
User.preferred_locale) with date-format picker on Account page; admin SPA date displays respect the stored locale via module-level locale store - Per-event IANA timezone on events — create/settings/wizard picker, reports/exports/mail preview use event timezone
- Event overview dashboard at
/admin/events/:id/overviewwith admission rate, email delivery stats, event countdown, and dedicatedGET /api/admin/events/:eventId/overviewendpoint
Fixed
- Event settings PATCH:
audit_failed500 uses{ error }shape - Communication template editor: cursor restored after inserting placeholders into subject/body fields
- Event overview: auto-refresh stats every 30s during event
- CSV import: ignore
source_fieldslugs that collide with standard import columns (email,company, etc.); event item contents API and admin form reject those reserved slugs on save - CSV import: validate merged
custom_dataat commit (including overwrite with existing attributes); return 400 when event attribute config has conflicting select options - Settings tabs preserve in-progress panel state (drafts, filters) when switching tabs without eager-loading every panel on first visit
Changed
- Settings: replace mixed SPA/SSR horizontal tabs with four grouped in-app tabs (General, Security, Archiving, Identity); OIDC and Cloudflare Access remain server-rendered manage links
- Admin timestamp display clarity: event operational times use event timezone with abbreviation; admin/system times (audit log, mail deliveries, sessions, archived_at) always show UTC with label
client-ip/ healthz rate-limit helpers importresolveTrustProxyfrom lightweightenv-flagsmodule (avoids flaky CI load of@admitto/authbarrel →@admitto/tickets→ Prisma singleton)- Shared
@admitto/sharedlocale whitelist (SUPPORTED_LOCALE_TAGS) used by API validation and Account picker; invalid DB values sanitized on read - Audit log date filters use UTC calendar-day bounds (aligned with UTC table display)
- Existing events migrated to UTC — update timezone in Event Settings after deploy
- Known limitation: Account page TOTP enrollment still shows an
otpauth://URI string (HTML/mfa/enrollshows QR; SPA QR deferred to v0.5)
Deploy
- Container image:
ghcr.io/solarssk/admitto:0.4.7(rolling:0.4) - Migrations apply automatically on container start (with pre-migration backup when pending). No manual
migrate deploy.
v0.4.6 — audit remediation, privacy retention, release polish
[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.0deploy/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
/accountfor all signed-in staff: profile, password change (re-auth), TOTP enrollment/reset, and session management (/api/account/*) - Nullable
User.password_hashfor 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/usersand 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-assignmentsfor non-instance role grantsmust_change_passwordonUserwith server-rendered/change-passwordflow after login or admin password reset- First-run setup: server-rendered
/setupfor empty-database superadmin bootstrap; 5-step React onboarding wizard (system checks, mail, org branding, first event, completion) gated bysetup_completeinSystemSettings - Settings audit log viewer: superadmin-only paginated table of
AdminAuditLogentries 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 viaGET /api/admin/events/:eventId/reportsand/reports/export; exports writereports_exportedto the event audit log - Admin attendee erasure now has a GDPR-ready
DELETE /api/admin/events/:eventId/attendees/:idpath 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-describedbyfor screen readers - Ops health/readiness rate limiters import audit logging via
@admitto/auth/auditso unit tests do not load the full auth barrel (flakyPrismaClientinit in CI) - Account page (
/account): profile Save disabled when unchanged; live password-confirm mismatch feedback; Spinner loading states; SPALinknavigation to account from staff/operator shells @admitto/authrunInTransactionno longer value-importsPrismaClient, avoiding flakyhealthz-rate-limitunit tests when the auth barrel loads beforeprisma generate- First-run
POST /setupbootstrap race: Serializable transaction re-checks empty user table so only one superadmin can be created - IAM anti-lockout guards (
last_superadminon 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
/operator—setup_completeincluded on/api/auth/mefor 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
AdminAuditLognow 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_refbackfill 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_atupdates (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_statusnow has a database CHECK constraint matching the application enum, preventing invalid RSVP states from raw SQL or future scripts (DATA-002)OidcRoleGrantnow uses partial unique indexes for scoped and instance grants, correctly enforcing uniqueness whenscope_idisNULL(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 FROMDML 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_refbackfill 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
SessionandTrustedDevicerows after migrations/backfills with a 120-second timeout; operators can also runnpm run cli -w @admitto/auth -- purge-auth-retention --dry-runto preview counts, reducing stale auth-state retention (DATA-001, PRIV-005) - Container startup now nullifies stale
EmailDelivery.rendered_html/rendered_subjectsnapshots on terminal deliveries older than 60 days (configurable viaEMAIL_DELIVERY_SNAPSHOT_RETENTION_DAYS); operators can preview counts withnpm 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_requiredsession 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 previousnext: change_passwordhint 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@instanceto a second user now returns HTTP 409single_superadmin_limitinstead 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.errorsanitization 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
DELETEAPI (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/enrollshows 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.
v0.4.5 — settings, sessions, and event admin
[0.4.5] - 2026-06-24
Added
- MFA enrollment split into three steps: TOTP QR/setup-key confirm → dedicated backup-codes page → full session; new
backup_codes_requiredsession stage and DB migration POST /mfa/enroll/backup-codesroute andhandleTotpBackupCodesCompleteAPI endpoint; session promoted toFULLonly after backup-codes acknowledgment- 6-digit centered OTP input field with auto-focus, paste support, keyboard navigation, and backup-recovery-code toggle
GET /smart redirect: authenticated users land on/admin(or/operator), unauthenticated users on/login- Admitto favicon set: SVG, 32 × 32 PNG, Apple touch icon, and ICO fallback served from
/favicon.* - SSO button label per identity provider (
login_button_label); default falls back to "Continue with SSO" - Admin sidebar: checkmark brand mark (correct
admitto-mark.svg),All events+Settingsitems always pinned to footer - Settings sub-navigation: horizontal tab bar (General · Identity providers · Cloudflare Access) rendered on both SPA and SSR settings pages
- Identity-provider and Cloudflare Access admin pages rendered with full sidebar shell matching the SPA layout (sticky layout, design-system CSS tokens)
- Event card: date icon, location pin icon, attendee count stat with user icon; hover lift effect
- Event overview landing page at
/admin/events/:id/overviewwith quick stats and navigation links to live admin sections - Shared admin shell (
StaffShell): sidebar with independent scroll, slim topbar, optional settings subnav, and mobile drawer navigation - Staff topbar: mailer status indicator (configured dot + provider label), role badge (SA/AD/OP), icon-only sign out
- Sidebar footer: Users & roles link for org admins and superadmins (page ships in a follow-up PR)
- Dev-only demo bar (
import.meta.env.DEV) to trigger sample toasts from the admin shell - Admin check-in v2: split layout with scan bar, connection banner, stats/progress sidebar, color-dot recent scans, and fullscreen camera overlay with QR viewfinder
- Create event UI:
POST /api/admin/events, New event modal (title, slug, date, location), redirect to attendees after create - Events picker visual polish: active status badge, responsive 2/3-column grid, empty state with Create event CTA
- Attendees v2:
rsvp_statusmigration, wider list table (STATUS/MAIL/CHECK-IN/actions), manualPOST /attendees, full attendee detail page with activity log, Add attendee modal - Event settings page at
/admin/events/:id/settings: edit title, date, location, capacity; superadmin PII CSV export; archive/unarchive from danger zone;capacityfield on Event
Changed
GET /api/admin/meincludesmailer_status(provider presence only — no credentials);/api/auth/meunchanged for operator sessions- Toast stack position: top-right below the staff topbar (
--topbar-h) instead of bottom-right - Admin check-in: keyboard wedge
inputMode="none", auto-submit for long tokens, Esc to clear result state - README: local dev onboarding (
Run locally), Nodeenginesalignment,infra/vsdeploy/distinction; newapps/admin/README.md - Admin Vite dev proxy: forward
/mfato@admitto/webso MFA enrollment works on:5173 - Login page
<title>fixed to "Admitto"; addedapplication-name,og:site_name, anddescriptionmeta tags for password-manager naming - MFA page heading changed from
<h1>to<p class="auth-page-action">to preserve correct document semantics; Admitto brand uses<h1> - CSRF fix: Nginx forwards
$http_host(with port) inHostandX-Forwarded-Hostheaders so127.0.0.1:8080logins no longer return 403 - Superadmin instance settings at
/admin/settingswith branding panel — live theme preview, anti-lockout guards, and links to OIDC / Cloudflare Access admin pages (#96) - Settings → Mail transport panel: configure provider, masked secrets, env-locked fields, and test send (#99)
- Settings → Sessions and Security panels: list/revoke staff sessions, bulk operator revoke, session TTL and MFA policy;
GET/PATCH /api/admin/system-settings(#112) - Event archiving:
Event.archived_athides completed events from default lists; superadmin archive/unarchive; archived events read-only on admin mutating APIs; Active/Archived tabs; check-in stays available (ADR 0022) (#116) - Admin shell layout: single main scroll region (subnav + page content),
100dvhviewport, Overview in live event sidebar segments; events picker opens archived tab when no active events remain; active event cards are fully clickable - MFA enrollment and verify: step progress indicator (
Step X of 3), no OTP autofocus on the QR step, auto-submit after six digits, and submit loading state on auth forms - Settings horizontal subnav uses consistent styling across SPA and SSR; Identity providers and Cloudflare Access open via full-page navigation to SSR admin pages
- Admin sidebar chrome trimmed: redundant context labels and duplicate Instance settings header action removed
@admitto/uidesign system:Spinner,EmptyState,Skeleton, andToastProvider/useToast(#120)- Admin app root wrapped with
ToastProvider; recoverableErrorBoundaryon render errors (#97, #120) - Admin UX micro-fixes: import column reference table and CSV template download; delivery log purpose filter; compose dirty-state guard; attendee drawer discard confirmation; check-in stats admitted/total (#121)
POST /mfa/enroll/download-codes— backup codes as.txtduring enrollment (#117)- Check-in:
NoteModalreplaceswindow.promptfor attendee notes (#114) - Runtime upgraded to Node 24 LTS; React 18 → 19 across admin and web (#111, #110)
- Login, MFA, and superadmin identity-provider HTML pages aligned with Admitto design tokens (#117)
- OIDC group mapping role picker uses a select; provider list supports inline enable/disable (#117)
- Cloudflare Access admin form shows status badge, fall-through explanation, and enable warning (#117)
- Requirements and Communication panels use
ConfirmDialoginstead of nativewindow.confirm(#97)
Fixed
- Guest ticket page now prints correctly (white background, no wallet buttons, no shadows)
- Export: sanitize dynamic attribute column headers against formula injection (#97)
- Local dev: login/MFA CSRF when
Origin/Refererabsent (Safari); admin SPA dist path afternpm run build -w @admitto/admin(#115) - Admin page shell document title uses event name prefix without regressing the visible
h1(#118) - SSO failure on
/loginshows a dedicated fallback banner; removed placeholder “SSO coming soon” when no IdP is configured (#117) - Check-in card coloured left border per scan status (#114)
- Login page title and heading: “Sign in to Admitto” (#114)
- Sidebar Overview section shows “Soon” until built (v1.0) (#114)
- Settings subnav active tab uses path prefix matching instead of exact pathname equality
- Sidebar “Soon” badges render with correct styling (
.nav-item--soon,.nav-item__badge) - Global link hover underline no longer appears on sidebar brand, navigation items, or button-styled links (SPA and SSR settings shell)
- OIDC admin form: URL fields use
type="url"; group→role mapping rows can be added/removed; scope type select; SSO button live preview on provider form; failed save re-renders submitted mapping drafts - Cloudflare Access settings: clearer operator copy and field hints; test action labeled “Test connection”
- Events picker: search by title/location; content width capped at 1100px; grid capped at three columns on wide screens
- Check-in camera: removed fullscreen toggle (browser instability); debounce repeated ZXing decodes; extract ticket token from QR URLs with trailing slash or query (
packages/tickets) - Check-in invalid/revoked scans show dedicated feedback card instead of silent failure
- Requirements:
@admitto/uiSwitch missing thumb restored; item table uses name + auto-generated key; drawer layout cleanup - Event picker cards: removed hover lift/underline noise
- Vercel Git deploys disabled via root
vercel.json(self-hosted Docker only)
Security
- Branding
font_family_nameallowlist on save and ticket-page render (blocks CSS/HTML injection via custom fonts) (#96) deploy/validate-env.shpre-flight fordeploy/.env; production boot fails fast whenREDIS_URLis missing, unauthenticated, orENCRYPTION_KEYis invalid- PENtest hardening: structured audit events for rate limits, MFA, OIDC login, logout, admin 403, and settings changes — ISO
tson each event (#123) - Production
BASE_URLmust usehttps://(exceptlocalhost/127.0.0.1smoke) (#123) - OIDC ID token verification restricts JWT algorithms to RS/ES/PS family (no
none) (#123) - Deploy Redis requires
REDIS_PASSWORD; compose wires authenticatedREDIS_URL(#123) - Nginx proxy baseline security headers (HSTS, nosniff, frame deny); Docker bridge gateway for RealIP behind NPM (#123)
/healthzand/readyzresponses include baseline security headers (#123)- GitHub Releases for
v0.x.yare pre-release untilv1.0.0;publish-containersets the flag automatically (#123) - PENtest follow-up: rate limits on MFA enroll,
/healthz, admin import/template preview, and OIDC provider discover/test; OIDC outbound fetch resolves DNS before connect; malformedX-Forwarded-Forfalls back to socket IP (no sharedunknownbucket) docs/SECURITY-CONTROLS.md: rate-limit matrix,TRUST_PROXYtrust model, SSRF/DNS-rebind guards, PEN retest checklist for operators
Deploy
- Container image:
ghcr.io/solarssk/admitto:0.4.5(rolling:0.4) - Migrations apply automatically on container start (with pre-migration backup when pending). No manual
migrate deploy.
v0.4.4 — undici security and container workflow fix
[0.4.4] - 2026-06-19
Security
- Bump transitive
undici6.26.0 → 6.27.0; addresses CVE-2026-12151 (high), CVE-2026-9679 (moderate), CVE-2026-11525 and CVE-2026-6733 (low) (#94)
Fixed
- Container publish workflow:
workflow_dispatchon branch refs runs scan-only (Trivy SARIF, CRITICAL gate) without SBOM path or Docker metadata failures (#93) - GHCR push, provenance attestation, and release SBOM restricted to
refs/tags/v*.*.*semver refs; semver-shaped branch names can no longer trigger a publish
Deploy
- Container image:
ghcr.io/solarssk/admitto:0.4.4(rolling:0.4) - Migrations apply automatically on container start (with pre-migration backup when pending). No manual
migrate deploy.
v0.4.3 — admin export, dynamic attributes, corp readiness
[0.4.3] - 2026-06-19
Added
- Attendee list export to CSV, XLSX, and PDF with check-off column and formula-injection sanitization (#88)
- Dynamic custom attributes in admin drawer: edit fields driven by
EventItem.config.contentsinstead of hardcodedshirt_size(ADR 0030, #91) - Export columns follow
EventItem.config.contentsdefinitions; check-in parity preserved SECURITY-CONTROLS.md: configurable security capabilities table with TOTP and OIDC implementation detailCORPORATE-DEPLOYMENT.md: self-hosted model, customer-hosted stack, no SaaSARCHITECTURE-FOR-AUDITORS.md: scope, generic exposure overview, roadmap flowsGDPR-ONE-PAGER.md,SUBPROCESSORS.md: purposes, retention, subprocessor template, DSAR optionsDSAR-PROCEDURE.md: organizer-mediated access and erasure template (Option B)INCIDENT-RESPONSE.md: rotation, rollback, severity template; GDPR Art. 33/34 72-hour breach notification- Zod validation on
custom_data_fieldskeys; stable export column order (orderBy: key) - Duplicate Excel headers disambiguated as
Label (source_field); PDF column widths scale down when many attributes exceed printable width - Export integration tests use isolated events
Changed
DATA-PROTECTION.mdupdated with legal basis note (LIA for legitimate interest)
Fixed
- Drawer degrades gracefully when event-items API fails
Security
- Removed
|| truefrom Semgrep CI step — SAST findings now block pull requests (baseline verified at 0 findings before merge)
Deploy
- Container image:
ghcr.io/solarssk/admitto:0.4.3(rolling:0.4) - Migrations apply automatically on container start (with pre-migration backup when pending). No manual
migrate deploy.