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:
@@ -23,11 +23,11 @@ import * as EventService from '../../src/models/calendar/events/events.service.j
|
||||
|
||||
/**
|
||||
* Step 3 of docs/calendar-auth-migration.md. The property under test is that
|
||||
* an event's creator resolves from whichever of its three possible sources is
|
||||
* strongest - the live admin name, then the snapshot from migration 002, then
|
||||
* the legacy join - and that a failure to reach the admin database costs a name
|
||||
* rather than the whole response: the public calendar is read anonymously by
|
||||
* the website and has never depended on the admin database being up.
|
||||
* an event's creator resolves from the stronger of its two remaining sources -
|
||||
* the live admin name, else the name archived before the legacy users table was
|
||||
* removed - and that a failure to reach the admin database costs a name rather
|
||||
* than the whole response: the public calendar is read anonymously by the
|
||||
* website and has never depended on the admin database being up.
|
||||
*/
|
||||
|
||||
// One row of the shape the shared SELECT produces.
|
||||
@@ -42,14 +42,10 @@ const row = (over: Record<string, unknown> = {}) => ({
|
||||
created_date: new Date('2026-01-01T00:00:00Z'),
|
||||
version_created_at: new Date('2026-01-02T00:00:00Z'),
|
||||
location: '',
|
||||
created_by_id: 7,
|
||||
created_by_user_id: null,
|
||||
created_by_name: null,
|
||||
legacy_created_by_name: 'Legacy Person',
|
||||
version_created_by_id: 7,
|
||||
created_by_name: 'Archived Person',
|
||||
version_created_by_user_id: null,
|
||||
version_created_by_name: null,
|
||||
legacy_last_modified_by_name: 'Legacy Person',
|
||||
version_created_by_name: 'Archived Person',
|
||||
url: '',
|
||||
whole_day: 0,
|
||||
repeat_frequency: '',
|
||||
@@ -72,13 +68,12 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
describe('creator names', () => {
|
||||
it('uses the legacy join when the row has no admin id', async () => {
|
||||
it('uses the archived name when the row has no admin id', async () => {
|
||||
givenEvents(row());
|
||||
|
||||
const events = await EventService.getAllEvents(1);
|
||||
|
||||
expect(events[0].createdBy).toBe('Legacy Person');
|
||||
expect(events[0].createdById).toBe(7);
|
||||
expect(events[0].createdBy).toBe('Archived Person');
|
||||
expect(events[0].createdByUserId).toBeNull();
|
||||
// Nothing to resolve, so the admin database is not touched at all.
|
||||
expect(AdminUsersService.findDisplayNames).not.toHaveBeenCalled();
|
||||
@@ -97,23 +92,9 @@ describe('creator names', () => {
|
||||
|
||||
expect(events[0].createdBy).toBe('Neue Person');
|
||||
expect(events[0].lastModifiedBy).toBe('Andere Person');
|
||||
// The legacy id is still reported during the transition.
|
||||
expect(events[0].createdById).toBe(7);
|
||||
expect(events[0].createdByUserId).toBe('admin-1');
|
||||
});
|
||||
|
||||
it('prefers the snapshot over the legacy join', async () => {
|
||||
givenEvents(row({
|
||||
created_by_name: 'Archived Person',
|
||||
version_created_by_name: 'Archived Person'
|
||||
}));
|
||||
|
||||
const events = await EventService.getAllEvents(1);
|
||||
|
||||
expect(events[0].createdBy).toBe('Archived Person');
|
||||
expect(events[0].lastModifiedBy).toBe('Archived Person');
|
||||
});
|
||||
|
||||
it('prefers the live admin name over the snapshot', async () => {
|
||||
// A renamed account has to win over an archive that was correct when it
|
||||
// was taken - otherwise renaming someone would leave stale names behind.
|
||||
@@ -125,29 +106,26 @@ describe('creator names', () => {
|
||||
expect(events[0].createdBy).toBe('Neue Person');
|
||||
});
|
||||
|
||||
it('keeps the snapshot when step 5 has removed the legacy join', async () => {
|
||||
// What a post-step-5 row looks like: no legacy id, no join, snapshot only.
|
||||
givenEvents(row({
|
||||
created_by_id: null,
|
||||
legacy_created_by_name: undefined,
|
||||
legacy_last_modified_by_name: undefined,
|
||||
created_by_name: 'Archived Person',
|
||||
version_created_by_name: 'Archived Person'
|
||||
}));
|
||||
|
||||
const events = await EventService.getAllEvents(1);
|
||||
|
||||
expect(events[0].createdBy).toBe('Archived Person');
|
||||
expect(events[0].lastModifiedBy).toBe('Archived Person');
|
||||
});
|
||||
|
||||
it('falls back to the legacy name when the admin account is gone', async () => {
|
||||
it('falls back to the archived name when the admin account is gone', async () => {
|
||||
givenEvents(row({created_by_user_id: 'deleted'}));
|
||||
(AdminUsersService.findDisplayNames as any).mockResolvedValue(new Map());
|
||||
|
||||
const events = await EventService.getAllEvents(1);
|
||||
|
||||
expect(events[0].createdBy).toBe('Legacy Person');
|
||||
expect(events[0].createdBy).toBe('Archived Person');
|
||||
});
|
||||
|
||||
it('leaves the name blank when a row has neither source', async () => {
|
||||
// A post-cutover event whose author was later deleted from the admin
|
||||
// module: no snapshot was ever taken for it, and the id resolves to
|
||||
// nothing. Blank is the designed outcome - the creator is decoration.
|
||||
givenEvents(row({created_by_user_id: 'deleted', created_by_name: null}));
|
||||
(AdminUsersService.findDisplayNames as any).mockResolvedValue(new Map());
|
||||
|
||||
const events = await EventService.getAllEvents(1);
|
||||
|
||||
expect(events[0].createdBy).toBeNull();
|
||||
expect(events).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('resolves a mixed result set in a single lookup', async () => {
|
||||
@@ -160,7 +138,7 @@ describe('creator names', () => {
|
||||
|
||||
const events = await EventService.getAllEvents(1);
|
||||
|
||||
expect(events.map(e => e.createdBy)).toEqual(['Legacy Person', 'Neue Person', 'Neue Person']);
|
||||
expect(events.map(e => e.createdBy)).toEqual(['Archived Person', 'Neue Person', 'Neue Person']);
|
||||
expect(AdminUsersService.findDisplayNames).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -172,8 +150,8 @@ describe('creator names', () => {
|
||||
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].name).toBe('Konzert');
|
||||
// Degrades to the legacy name rather than failing the request.
|
||||
expect(events[0].createdBy).toBe('Legacy Person');
|
||||
// Degrades to the archived name rather than failing the request.
|
||||
expect(events[0].createdBy).toBe('Archived Person');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user