add password expiry enforcement

This commit is contained in:
miloschwartz
2025-10-24 17:11:39 -07:00
parent 39d6b93d42
commit 1e70e4289b
17 changed files with 1028 additions and 71 deletions

View File

@@ -5,7 +5,6 @@ import { fromError } from "zod-validation-error";
import { z } from "zod";
import { db } from "@server/db";
import { User, users } from "@server/db";
import { eq } from "drizzle-orm";
import { response } from "@server/lib/response";
import {
hashPassword,
@@ -15,6 +14,8 @@ import { verifyTotpCode } from "@server/auth/totp";
import logger from "@server/logger";
import { unauthorized } from "@server/auth/unauthorizedResponse";
import { invalidateAllSessions } from "@server/auth/sessions/app";
import { sessions, resourceSessions } from "@server/db";
import { and, eq, ne, inArray } from "drizzle-orm";
import { passwordSchema } from "@server/auth/passwordSchema";
import { UserType } from "@server/types/UserTypes";
@@ -32,6 +33,46 @@ export type ChangePasswordResponse = {
codeRequested?: boolean;
};
async function invalidateAllSessionsExceptCurrent(
userId: string,
currentSessionId: string
): Promise<void> {
try {
await db.transaction(async (trx) => {
// Get all user sessions except the current one
const userSessions = await trx
.select()
.from(sessions)
.where(
and(
eq(sessions.userId, userId),
ne(sessions.sessionId, currentSessionId)
)
);
// Delete resource sessions for the sessions we're invalidating
if (userSessions.length > 0) {
await trx.delete(resourceSessions).where(
inArray(
resourceSessions.userSessionId,
userSessions.map((s) => s.sessionId)
)
);
}
// Delete the user sessions (except current)
await trx.delete(sessions).where(
and(
eq(sessions.userId, userId),
ne(sessions.sessionId, currentSessionId)
)
);
});
} catch (e) {
logger.error("Failed to invalidate user sessions except current", e);
}
}
export async function changePassword(
req: Request,
res: Response,
@@ -109,11 +150,13 @@ export async function changePassword(
await db
.update(users)
.set({
passwordHash: hash
passwordHash: hash,
lastPasswordChange: new Date().getTime()
})
.where(eq(users.userId, user.userId));
await invalidateAllSessions(user.userId);
// Invalidate all sessions except the current one
await invalidateAllSessionsExceptCurrent(user.userId, req.session.sessionId);
// TODO: send email to user confirming password change