feat: change password (revokes other sessions) and logout/login regression test
All checks were successful
/ checks (push) Successful in 40s
/ deploy (push) Successful in 8s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nholtware 2026-11-05 13:47:37 -06:00
parent afbc5e5254
commit e69a02c6df
5 changed files with 247 additions and 10 deletions

View file

@ -61,6 +61,14 @@ export class PgSessionStore extends session.Store {
);
}
/** Sign a user out everywhere except `exceptSid` (e.g. after a password change). */
async destroyForUser(userId: string, exceptSid: string): Promise<void> {
await this.db.query(
"DELETE FROM sessions WHERE sess->'passport'->>'user' = $1 AND sid <> $2",
[userId, exceptSid],
);
}
destroy(sid: string, cb?: (err?: unknown) => void) {
this.db.query("DELETE FROM sessions WHERE sid = $1", [sid]).then(
() => cb?.(),
@ -68,3 +76,24 @@ export class PgSessionStore extends session.Store {
);
}
}
/** In-memory sessions (tests / no database), with the same revoke hook. */
export class MemorySessionStore extends session.MemoryStore {
async destroyForUser(userId: string, exceptSid: string): Promise<void> {
const all = await new Promise<Record<string, session.SessionData>>(
(resolve, reject) =>
this.all((err, sessions) =>
err ? reject(err) : resolve(sessions ?? {}),
),
);
for (const [sid, sess] of Object.entries(all)) {
const passport = (sess as { passport?: { user?: string } }).passport;
if (sid !== exceptSid && passport?.user === userId) {
await new Promise<void>((r) => this.destroy(sid, () => r()));
}
}
}
}
export type RevocableStore = (PgSessionStore | MemorySessionStore) &
session.Store;