Fold the pre-deploy review findings into the calendar cutover

A fresh-context review before deploying found two things that would have
broken production, both in the runbook rather than the code.

The deploy order named only migration 003. Production has none of the three -
001 and 002 were only ever applied to the dev database - and the new API reads
the columns they add on every request, so following it literally would have
500'd every calendar call including the anonymous feed the public website
uses. Step 4 now carries a numbered checklist with a verification query.

APP_ORIGINS replaces the code's default list rather than adding to it, so
naming calendar.nachklang.art in DEFAULT_APP_ORIGINS is not enough if that
variable is set on the vhost - and its failure mode is the quiet one the
config already warns about, where everything works except sign-out. Added to
the same checklist.

Also from the review:

The two operands of the read guard on /json/next and /ical were swapped so the
password check short-circuits first. They are side-effect free, so the order
was free - but the old one put an admin-database query in front of the public
feed for any caller holding a .nachklang.art cookie, which is a dependency
that feed has never had. Two tests now assert the admin database is not
consulted at all.

/:calendar/json/next had no route-level test, despite being the endpoint the
public website actually calls and the property named as load-bearing. Covered
now, along with the rest of its credential matrix.

Migrations 001 and 002 gained IF NOT EXISTS. They are applied by hand with no
tracking table, so a partial re-run should be a no-op rather than an error
that aborts the rest of the paste. Verified by applying all three twice to a
throwaway container and diffing against the dev schema.

Swagger: two descriptions still claimed authentication was required where the
public calendar needs none, the calendar enum omitted `birthdays`, and a
`createdBy` request-body field was documented and read but never persisted -
misleading in a way that suggests a client can set authorship. Removed. The
CORS comment describing the calendar's query-parameter sessions is no longer
true and was rewritten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-06 22:41:49 +02:00
parent b848d6eab9
commit d960ac8e24
6 changed files with 129 additions and 40 deletions
+42 -7
View File
@@ -1,7 +1,12 @@
# Migrating the Calendar domain onto the admin identity module
Status: **steps 1-4 done** (2026-09-06), step 2 dropped by decision, part of step 5 brought
forward. Only step 5, the removal of the legacy path, is left.
Status: **steps 1-4 implemented 2026-09-06, not yet merged or deployed.** Step 2 dropped by
decision, part of step 5 brought forward. Only step 5, the removal of the legacy path, is
left to write.
> Read the deploy checklist under step 4 before applying anything. "Done" below means the
> code exists on a branch, **not** that production has it - and in particular production has
> none of the three migrations.
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. Steps 1-4 of that plan
@@ -116,11 +121,41 @@ Each step is meant to leave production working on its own.
`test/calendar/credentials.service.test.ts`, the routes themselves in
`test/calendar/events.router.test.ts` - so this cannot regress quietly.
**Deploy order:** migration 003, then the API, then the calendar frontend. The frontend is
broken between the last two (its old bundle sends query credentials the new API ignores),
so pick a quiet moment. Production also needs `calendar.nachklang.art` in the admin app's
`NEXT_PUBLIC_ALLOWED_REDIRECT_ORIGINS`, which is a **build-time** value: a rebuild, not a
restart.
### Deploy checklist
Production has **none** of the three migrations: 001 and 002 were only ever applied to the
dev database. The API build below selects `created_by_user_id` and `created_by_name` on
every read, so deploying it against a database missing them fails every calendar request
including the anonymous public feed the website uses. In order:
1. **Apply `sql/calendar/001`, `002`, `003`, in that order**, against `CALENDAR_DB`. All
three are re-runnable, so applying one that is already applied is a no-op. Verify
before continuing:
`SHOW COLUMNS FROM events LIKE '%by_user%'; SHOW COLUMNS FROM events LIKE '%by_name%';`
- four rows across the two tables, and `created_by_id` nullable.
2. **Check `APP_ORIGINS` on the API vhost.** `calendar.nachklang.art` is in the code's
default list, but the environment variable *replaces* that list rather than adding to
it - so if it is set at all (the tickets/feedback cutover may have set it), append
`https://calendar.nachklang.art` or the calendar's sign-out will 403 while everything
else works. That is the failure mode the comment in `admin.config.ts` warns about.
3. **Deploy the API.**
4. **Deploy the calendar frontend immediately after.** Do not leave a gap - see below.
5. **Re-run 002's two `UPDATE` statements.** Between step 1 and step 3 the old API was
still writing `created_by_id` with no snapshot; those few rows would otherwise lose
their author at step 5.
6. **Rebuild the admin app** if `NEXT_PUBLIC_ALLOWED_REDIRECT_ORIGINS` does not already
contain `https://calendar.nachklang.art`. It is a **build-time** value, so a restart
does nothing.
**The window between steps 3 and 4 does not look broken, which is the danger.** The old
Angular bundle starts by calling `POST /calendar/users/checkSessionValid`, and those
legacy routes are untouched - so it still succeeds and the page renders as signed in. What
the user then sees is an empty event table and saves that silently do nothing. It looks
like the calendar lost its data, not like a deploy in progress. Keep the gap to minutes,
or take the frontend offline for it.
**One-way door:** any iCal subscription whose URL carries `?sessionId=&sessionKey=` rather
than `?password=` stops working permanently. The shared-password URLs are unaffected.
5. **Drop the legacy path.** Remove `users.service.ts`'s session handling, the `sessions`
table, `created_by_id`, and the legacy half of the step 3 read (the `users` join and its
`legacy_*` aliases - the snapshot fallback stays, it is what makes dropping the table