# 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`/`sessions` live in the **calendar** database and are the same tables the feedback and tickets admin areas used to authenticate against. - `events.created_by_id` is an **INT** foreign key into `users.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`/`sessionKey` as **query parameters** (`DEFERRED_SECURITY.md` item 1). Cookie sessions remove the parameters entirely, so every calendar route signature and the frontend's HTTP layer change together. - `credentials.service.ts` implements a second, parallel authorisation model: the `MEMBER_CREDENTIAL` / `CHOIR_CREDENTIAL` / `MANAGEMENT_CREDENTIAL` shared 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`, and `requireAppAccess('calendar')` in `src/models/admin/admin.middleware.ts` - usable the moment a calendar route wants it. - `res.locals.admin` is `{id, email, displayName, apps}`; `id` is 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. 1. **Add a bridging column.** `ALTER TABLE events ADD COLUMN created_by_user_id VARCHAR(36) NULL`, indexed. Nothing reads it yet. 2. **Map the accounts.** For every legacy `users` row that should survive, invite the person through the admin UI. On acceptance, backfill `events.created_by_user_id` from `events.created_by_id` via an email-to-new-id mapping. Everyone not re-invited keeps working on the legacy path until step 4. 3. **Dual-read.** Change `events.service.ts` to prefer `created_by_user_id` and fall back to `created_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. 4. **Switch the routes.** Replace the query-parameter session checks in `events.router.ts` and `users.router.ts` with `requireAppAccess('calendar')`, and change the Angular frontend to `withCredentials: true` against the same origin list. Deploy the API first; the calendar frontend is broken between the two deploys, so pick a quiet time. This closes `DEFERRED_SECURITY.md` item 1. 5. **Drop the legacy path.** Remove `users.service.ts`'s session handling, the `sessions` table, `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-Key` can then come out of the CORS `allowedHeaders` list in `src/app.factory.ts`. ## Open questions to settle before starting - **The shared calendar credentials.** Do `MEMBER_CREDENTIAL` and 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 of `credentials.service.ts` survives. - **The iCal export.** `GET /calendar/events/{calendar}/ical` takes 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 in `events.service.ts` for the "last modified by" name. It has to move with `events`, and it is the reason step 1's bridging column needs a sibling on `event_versions`.