Drop the calendar's legacy authentication path

Step 5, the last one, of docs/calendar-auth-migration.md. Step 4 is deployed
and verified, which is what this was waiting on: it removes the fallbacks that
step 4 still leaned on.

Gone: src/models/calendar/users/ entirely - registration, login, activation,
both password-reset routes, and the session checking that the feedback and
tickets admin areas used to authenticate against - along with its mount. That
was the API's last unauthenticated account-creation and mail-sending endpoint.
A survey confirmed nothing outside that directory imported it and nothing else
touched its tables.

Also gone: the two joins against the calendar users table in events.service.ts
and the created_by_id / version_created_by_id columns they read, from the SQL,
the row mapper, the Event interface and the swagger schema; and X-Session-Id /
X-Session-Key from the CORS allowedHeaders, which nothing has read since the
first cutover and nothing has sent since the second.

An event's author still renders, because migration 002 snapshotted the names
before this could erase them. That was brought forward from this step on
purpose, and it is the reason 004 can rename the accounts aside at all.

The accounts are renamed rather than dropped - they still hold e-mail addresses
and password hashes, and a rename makes them unreachable without destroying
anything. InnoDB rewires the sessions foreign key to the new name; verified on
MariaDB 11, along with the whole 001-004 chain from the pre-cutover production
schema, which lands byte-identical to a fresh dev database.

Migration 004 must be applied AFTER deploying, not before - the reverse of step
4, whose migration only added things. Its own header and the runbook both say
so, since getting it wrong by analogy is the obvious mistake.

DEFERRED_SECURITY.md items 3 and 4 close with it: the activation and reset
tokens that never expired are gone along with the code that issued them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-06 23:32:59 +02:00
parent 13a0c07d1b
commit aa95ab2745
17 changed files with 253 additions and 1370 deletions
+15 -29
View File
@@ -24,9 +24,9 @@ Two things this did *not* change, both deliberate:
this is the one caller that genuinely needs a credential in the URL. It grants read access
to one calendar and nothing else - `test/calendar/events.router.test.ts` pins that it can
never be used to write.
- The legacy `/calendar/users/*` routes still exist. Nothing calls them any more, and a
legacy session they mint no longer opens anything, but they are still live
password-accepting endpoints. Step 5 removes them.
- ~~The legacy `/calendar/users/*` routes still exist.~~ Removed by step 5 on 2026-09-06,
along with the `users` and `sessions` tables they used - renamed aside rather than dropped,
so nothing was destroyed.
---
@@ -43,37 +43,23 @@ Currently any account holding the `calendar` permission can edit, move, or delet
---
## 3. Activation token has no expiry
## 3. Activation token has no expiry — CLOSED 2026-09-06
> **Superseded for new accounts (2026-09-05).** The admin module
> (`src/models/admin/`) replaced account creation for the feedback, tickets and admin
> apps: accounts now come from `invitations`, whose tokens expire after 7 days and are
> stored only as a SHA-256 hash. The item below still stands for the legacy calendar
> `users` table, which the admin module deliberately left alone - see
> `docs/calendar-auth-migration.md`.
**File:** ~~`src/models/calendar/users/users.service.ts`~~ — deleted.
**File:** `src/models/calendar/users/users.service.ts``createUser` / `activateUser`
The e-mail activation link was valid indefinitely. Closed not by adding an expiry but by
removing the thing that issued it: step 5 of `docs/calendar-auth-migration.md` deleted the
calendar's own account system. Accounts now come only from the admin module's `invitations`,
whose tokens expire after 7 days and are stored as a SHA-256 hash.
The email activation link is valid indefinitely. Acceptable for a small, trusted userbase.
**Fix:**
1. Add an `activation_expires` column to the `users` table (e.g. `DATETIME`).
2. Set it to `NOW() + INTERVAL 24 HOUR` in `createUser`.
3. Check `activation_expires > NOW()` in `activateUser` before accepting the token.
Any activation link still sitting in an inbox now 404s. It only ever activated a legacy
account, which no longer opens anything.
---
## 4. Password reset token has no expiry
## 4. Password reset token has no expiry — CLOSED 2026-09-06
> **Superseded for new accounts (2026-09-05).** Password resets for admin-module accounts
> go through better-auth, whose reset tokens expire after one hour. As with item 3, the
> text below still applies to the legacy calendar `users` table.
**File:** ~~`src/models/calendar/users/users.service.ts`~~ — deleted.
**File:** `src/models/calendar/users/users.service.ts``initiatePasswordReset` / `finalizePasswordReset`
The reset token stored in `pw_reset_token_hash` never expires. Acceptable for a small, trusted userbase.
**Fix:**
1. Add a `pw_reset_expires` column to the `users` table (e.g. `DATETIME`).
2. Set it to `NOW() + INTERVAL 15 MINUTE` in `initiatePasswordReset`.
3. Check `pw_reset_expires > NOW()` in `finalizePasswordReset` before accepting the token.
Same as item 3: `pw_reset_token_hash` never expired, and the code that set it no longer
exists. Password resets go through better-auth, whose reset tokens expire after one hour.