Move the calendar onto the shared session cookie
Step 4 of docs/calendar-auth-migration.md, and the close of
DEFERRED_SECURITY.md item 1: no calendar route reads sessionId/sessionKey from
the query string any more, so a live credential no longer travels through
access logs, browser history and Referer headers.
The four write routes sit behind requireAppAccess('calendar'), which also
narrows who may edit from "any activated @nachklang.art account" to an
explicit per-user permission. They answer 401 signed out and 403 without the
permission, where they previously answered 403 for both.
The three read routes cannot use the middleware: one URL serves an anonymous
visitor, an iCal subscription holding a shared password, and a signed-in
editor who should see drafts. They resolve the session optionally instead, and
a signed-in user without the calendar permission is treated as anonymous
rather than refused - so they keep the public calendar access anyone has.
That public calendar staying anonymous is load-bearing: nachklang.art reads it
to show the next upcoming event. It is now pinned at both the password-table
and the route level, and so is the rule that a shared password can never be
used to write.
credentials.service.ts loses its session half and becomes the password table
it always wanted to be. The shared passwords survive only for iCal clients,
which cannot send a cookie.
Writes record the author as an admin user id and no longer have a legacy int
to write, which is what migration 003 makes room for.
/calendar/users/* is left in place: nothing calls it and a session it mints
opens nothing, but they are still live password-accepting endpoints, so
removing them belongs with the rest of the legacy path in step 5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Migrating the Calendar domain onto the admin identity module
|
||||
|
||||
Status: **steps 1 and 3 done** (2026-09-06), step 2 dropped by decision, part of step 5
|
||||
brought forward, step 4 next.
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -77,24 +77,50 @@ Each step is meant to leave production working on its own.
|
||||
**Writes are not dual-written**, contrary to the original plan: before the cutover the
|
||||
request only ever carries a legacy session, so there is no admin id available to write.
|
||||
Writes start filling `created_by_user_id` (and stop filling `created_by_id`) in step 4.
|
||||
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.
|
||||
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`.~~ **Done 2026-09-06.** `DEFERRED_SECURITY.md` item 1 is closed:
|
||||
no route reads `sessionId`/`sessionKey` any more.
|
||||
|
||||
Two things this step has to carry that the original sequence put in step 5:
|
||||
How it came out, route by route:
|
||||
|
||||
- **`sql/calendar/003_*.sql` must make `events.created_by_id` nullable** (`MODIFY
|
||||
created_by_id INT NULL`). It is `NOT NULL` today, so the first event created after the
|
||||
- The four write routes sit behind `requireAppAccess('calendar')` as middleware. They
|
||||
answer 401 when signed out and 403 without the permission, where they used to answer 403
|
||||
for both.
|
||||
- The three read routes cannot use middleware - the same URL serves an anonymous visitor,
|
||||
an iCal subscription holding a shared password, and a signed-in editor who should see
|
||||
drafts. They call `resolveAccess` optionally instead (`signedInEditor` in the router),
|
||||
and a signed-in user *without* the calendar permission is treated as anonymous rather
|
||||
than refused, so they keep their access to the public calendar.
|
||||
- `credentials.service.ts` lost its session half entirely and is now just the password
|
||||
table. `hasAccess(calendar, password)`.
|
||||
- `/calendar/users/*` was left alone. Nothing calls it and a session it mints opens
|
||||
nothing, but they are live password-accepting endpoints - step 5 removes them.
|
||||
|
||||
Also: `calendar.nachklang.art` joined `DEFAULT_APP_ORIGINS` (better-auth `trustedOrigins`,
|
||||
without which sign-out from the calendar fails while everything else works), and
|
||||
`localhost:4200` joined the dev origins for the same reason.
|
||||
|
||||
Two things this step had to carry that the original sequence put in step 5:
|
||||
|
||||
- **`sql/calendar/003_allow_null_legacy_creator.sql` makes `events.created_by_id` nullable**
|
||||
(`MODIFY created_by_id INT NULL`). It is `NOT NULL` today, so the first event created after the
|
||||
cutover would otherwise fail to insert - there is no legacy int id to write any more.
|
||||
`event_versions.version_created_by_id` is already nullable. The foreign key can stay
|
||||
until step 5; it permits NULL. The same migration should re-run 002's idempotent
|
||||
name backfill, to catch anything created between the two deploys.
|
||||
- **The public calendar must stay anonymous.** `hasAccess('public')` returns true before
|
||||
any credential check, and nachklang.art reads `/calendar/events/public/json` and
|
||||
`/public/json/next` with no session at all. Pinned by
|
||||
`test/calendar/credentials.service.test.ts` so this cannot regress quietly.
|
||||
until step 5; it permits NULL. It also re-runs 002's idempotent name backfill, to catch
|
||||
anything created between the two migrations. Applying it early is safe - widening a
|
||||
column to accept NULL cannot break the running pre-cutover build.
|
||||
- **The public calendar stays anonymous.** `hasAccess('public')` returns true before any
|
||||
credential check, and nachklang.art reads `/calendar/events/public/json` and
|
||||
`/public/json/next` with no session at all. Pinned at both levels - the password table in
|
||||
`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.
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user