7aac07a013
Introduces src/models/admin/, a dedicated identity and permissions module on its own nachklang_admin database, and the shared authenticator that feedback and tickets will move onto in the cutover step. Nothing swaps over yet: feedback.auth.ts and tickets.auth.ts still authenticate against the legacy calendar sessions, so production behaviour is unchanged. - better-auth 1.7 mounted at /admin/auth/*, sessions as httpOnly cookies scoped to .nachklang.art so one sign-in covers every *.nachklang.art app. - Accounts are invite-only: public sign-up is disabled, and the invitations plugin is the only code that creates users. Tokens are stored as SHA-256 hashes and travel in the request body, never in a URL. - Per-app permissions in user_app_permissions; requireAppAccess(app) queries the database on every request (no cookie cache) so disabling a user or revoking a session takes effect immediately. - ADMIN_BOOTSTRAP_EMAIL guarantees a way in on an empty database, idempotently and without crashing the API if the database is unreachable at boot. - Guards prevent an admin from removing their own admin permission, disabling themselves, or stripping the last active admin. The admin pool uses the callback-style mysql2, not mysql2/promise: Kysely's MysqlDialect drives the pool with callbacks, and the promise wrapper ignores them, so every query hangs silently. Only the integration tests caught this. Schema in sql/admin/001_init.sql, derived from getAuthTables() on the installed better-auth rather than the published CLI, which lags the library and omits account.issuer. app.ts is split into src/app.factory.ts so the integration tests drive the real middleware order rather than a copy of it. Tests: 131 unit, plus 41 integration tests against a throwaway MariaDB started by test/integration/setup.ts (docker or podman). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4.4 KiB
4.4 KiB
Migrating the Calendar domain onto the admin identity module
Status: not started. Written 2026-09-05 alongside the admin module (step 2 of
docs/plan-admin-auth.md in the nachklang-admin repo), which deliberately left the
calendar alone.
Why the calendar was left out
The admin module replaced authentication for feedback and tickets by swapping one middleware. The calendar cannot be done that way, because its user identity is woven into its data:
users/sessionslive in the calendar database and are the same tables the feedback and tickets admin areas used to authenticate against.events.created_by_idis an INT foreign key intousers.user_id. The admin module's user ids are VARCHAR(36) strings. Migrating identity means migrating that column and every query that joins it.- The Angular frontend passes
sessionId/sessionKeyas query parameters (DEFERRED_SECURITY.mditem 1). Cookie sessions remove the parameters entirely, so every calendar route signature and the frontend's HTTP layer change together. credentials.service.tsimplements a second, parallel authorisation model: theMEMBER_CREDENTIAL/CHOIR_CREDENTIAL/MANAGEMENT_CREDENTIALshared secrets that let non-users read specific calendars. That has no equivalent in the admin module and is not a per-user permission at all.
What already exists today: calendar is a value in the user_app_permissions.app enum, so
permissions can be granted before anything else moves.
What is in place to build on
- Cookie sessions across
*.nachklang.art, andrequireAppAccess('calendar')insrc/models/admin/admin.middleware.ts- usable the moment a calendar route wants it. res.locals.adminis{id, email, displayName, apps};idis the string user id.- Invitations, disable/enable and session revocation already cover calendar users, because they are properties of the account rather than of an app.
Suggested sequence
Each step is meant to leave production working on its own.
- Add a bridging column.
ALTER TABLE events ADD COLUMN created_by_user_id VARCHAR(36) NULL, indexed. Nothing reads it yet. - Map the accounts. For every legacy
usersrow that should survive, invite the person through the admin UI. On acceptance, backfillevents.created_by_user_idfromevents.created_by_idvia an email-to-new-id mapping. Everyone not re-invited keeps working on the legacy path until step 4. - Dual-read. Change
events.service.tsto prefercreated_by_user_idand fall back tocreated_by_id. Writes fill both. This is the only step that is temporary code, and it should carry a removal note pointing at step 5. - Switch the routes. Replace the query-parameter session checks in
events.router.tsandusers.router.tswithrequireAppAccess('calendar'), and change the Angular frontend towithCredentials: trueagainst the same origin list. Deploy the API first; the calendar frontend is broken between the two deploys, so pick a quiet time. This closesDEFERRED_SECURITY.mditem 1. - Drop the legacy path. Remove
users.service.ts's session handling, thesessionstable,created_by_id, and the dual-read from step 3. Legacy/calendar/users/*stays only if something still calls it - otherwise delete it too.X-Session-Id/X-Session-Keycan then come out of the CORSallowedHeaderslist insrc/app.factory.ts.
Open questions to settle before starting
- The shared calendar credentials. Do
MEMBER_CREDENTIALand friends stay as a separate mechanism (they serve people with no account at all, and iCal clients that cannot send headers), or do read-only accounts replace them? This is a product decision, not a technical one, and it decides how much ofcredentials.service.tssurvives. - The iCal export.
GET /calendar/events/{calendar}/icaltakes a password in the query string on purpose, because iCal clients cannot send headers. Cookie sessions do not help here; this endpoint likely keeps its own scheme. - Which legacy accounts to keep. Step 2 is the moment to not re-invite people who no longer need access.
event_versions.version_created_by_id. The same INT reference again, joined inevents.service.tsfor the "last modified by" name. It has to move withevents, and it is the reason step 1's bridging column needs a sibling onevent_versions.