import { echo } from "@atums/echo"; import { errorMessages, httpStatus, passwordHashing, successMessages, } from "#environment/constants"; import { sessionManager } from "#lib/auth"; import { cassandra } from "#lib/database"; import { isValidPassword } from "#lib/validation"; import type { ExtendedRequest, RouteDef, UpdatePasswordRequest, UpdatePasswordResponse, UserRow, } from "#types/server"; const routeDef: RouteDef = { method: ["PUT", "PATCH"], accepts: "application/json", returns: "application/json", needsBody: "json", }; async function handler( request: ExtendedRequest, requestBody: unknown, ): Promise { try { const { session } = request; if (!session) { const response: UpdatePasswordResponse = { code: httpStatus.UNAUTHORIZED, success: false, error: errorMessages.NOT_AUTHENTICATED, }; return Response.json(response, { status: httpStatus.UNAUTHORIZED }); } const { currentPassword, newPassword, logoutAllSessions } = requestBody as UpdatePasswordRequest; if (!currentPassword || !newPassword) { const response: UpdatePasswordResponse = { code: httpStatus.BAD_REQUEST, success: false, error: "Both currentPassword and newPassword are required", }; return Response.json(response, { status: httpStatus.BAD_REQUEST }); } const passwordValidation = isValidPassword(newPassword); if (!passwordValidation.valid) { const response: UpdatePasswordResponse = { code: httpStatus.BAD_REQUEST, success: false, error: passwordValidation.error || "Invalid new password", }; return Response.json(response, { status: httpStatus.BAD_REQUEST }); } if (currentPassword === newPassword) { const response: UpdatePasswordResponse = { code: httpStatus.BAD_REQUEST, success: false, error: errorMessages.PASSWORD_SAME_AS_CURRENT, }; return Response.json(response, { status: httpStatus.BAD_REQUEST }); } const userQuery = ` SELECT id, username, email, password, is_verified, created_at, updated_at FROM users WHERE id = ? LIMIT 1 `; const userResult = (await cassandra.execute(userQuery, [session.id])) as { rows: UserRow[]; }; if (!userResult?.rows || userResult.rows.length === 0) { await sessionManager.invalidateSession(request); const response: UpdatePasswordResponse = { code: httpStatus.NOT_FOUND, success: false, error: errorMessages.USER_NOT_FOUND, }; return Response.json(response, { status: httpStatus.NOT_FOUND }); } const user = userResult.rows[0]; if (!user) { const response: UpdatePasswordResponse = { code: httpStatus.NOT_FOUND, success: false, error: errorMessages.USER_NOT_FOUND, }; return Response.json(response, { status: httpStatus.NOT_FOUND }); } const isCurrentPasswordValid = await Bun.password.verify( currentPassword, user.password, ); if (!isCurrentPasswordValid) { const response: UpdatePasswordResponse = { code: httpStatus.UNAUTHORIZED, success: false, error: errorMessages.CURRENT_PASSWORD_INCORRECT, }; return Response.json(response, { status: httpStatus.UNAUTHORIZED }); } const hashedNewPassword = await Bun.password.hash( newPassword, passwordHashing, ); const updateQuery = ` UPDATE users SET password = ?, updated_at = ? WHERE id = ? `; await cassandra.execute(updateQuery, [ hashedNewPassword, new Date(), session.id, ]); if (logoutAllSessions === true) { const invalidatedCount = await sessionManager.invalidateAllSessionsForUser(session.id); const baseMessage = successMessages.PASSWORD_UPDATED; const sessionMessage = ` Logged out from ${invalidatedCount} session(s).`; const response: UpdatePasswordResponse = { code: httpStatus.OK, success: true, message: baseMessage + sessionMessage, loggedOutSessions: invalidatedCount, }; return Response.json(response, { status: httpStatus.OK, headers: { "Content-Type": "application/json", "Set-Cookie": "session=; Path=/; Max-Age=0; HttpOnly", }, }); } const allSessions = await sessionManager.getActiveSessionsForUser( session.id, ); const currentToken = request.headers .get("Cookie") ?.match(/session=([^;]+)/)?.[1]; let invalidatedCount = 0; if (currentToken) { for (const token of allSessions) { if (token !== currentToken) { await sessionManager.invalidateSessionByToken(token); invalidatedCount++; } } } const userAgent = request.headers.get("User-Agent") || "Unknown"; const updatedSessionPayload = { id: user.id, username: user.username, email: user.email, isVerified: user.is_verified, displayName: user.display_name, createdAt: user.created_at.toISOString(), updatedAt: new Date().toISOString(), }; const sessionCookie = await sessionManager.updateSession( request, updatedSessionPayload, userAgent, ); const baseMessage = successMessages.PASSWORD_UPDATED; const sessionMessage = invalidatedCount > 0 ? ` Logged out from ${invalidatedCount} other session(s).` : "."; const response: UpdatePasswordResponse = { code: httpStatus.OK, success: true, message: baseMessage + sessionMessage, loggedOutSessions: invalidatedCount, }; return Response.json(response, { status: httpStatus.OK, headers: { "Content-Type": "application/json", "Set-Cookie": sessionCookie, }, }); } catch (error) { echo.error({ message: "Error updating user password", error, }); const response: UpdatePasswordResponse = { code: httpStatus.INTERNAL_SERVER_ERROR, success: false, error: errorMessages.INTERNAL_SERVER_ERROR, }; return Response.json(response, { status: httpStatus.INTERNAL_SERVER_ERROR, }); } } export { handler, routeDef };