bf7be65b03
Jenkins Production Deployment
Reviewed-on: #12 Co-authored-by: Patrick Müller <mail@pmueller.me> Co-committed-by: Patrick Müller <mail@pmueller.me>
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.