import bcrypt from "bcryptjs"; import User from "@/models/user"; import { getDb } from "@/lib/db"; import { getSession } from "@/lib/auth/session"; import { validateNewPassword } from "@/lib/auth/passwordPolicy"; import { withErrorHandling, json, badRequest, unauthorized, } from "@/lib/api/errors"; export const dynamic = "force-dynamic"; const BCRYPT_SALT_ROUNDS = 12; export const POST = withErrorHandling( async function POST(request) { const session = await getSession(); if (!session) { throw unauthorized("AUTH_UNAUTHENTICATED", "Unauthorized"); } let body; try { body = await request.json(); } catch { throw badRequest("VALIDATION_INVALID_JSON", "Invalid request body"); } if (!body || typeof body !== "object") { throw badRequest("VALIDATION_INVALID_BODY", "Invalid request body"); } const { currentPassword, newPassword } = body; const missing = []; if (typeof currentPassword !== "string" || !currentPassword.trim()) { missing.push("currentPassword"); } if (typeof newPassword !== "string" || !newPassword.trim()) { missing.push("newPassword"); } if (missing.length > 0) { throw badRequest( "VALIDATION_MISSING_FIELD", "Missing currentPassword or newPassword", { fields: missing }, ); } await getDb(); const user = await User.findById(session.userId).exec(); // Treat missing users like an invalid session (do not leak anything). if (!user) { throw unauthorized("AUTH_UNAUTHENTICATED", "Unauthorized"); } // Defensive: if hash is missing, treat as invalid credentials. if (typeof user.passwordHash !== "string" || !user.passwordHash) { throw unauthorized("AUTH_INVALID_CREDENTIALS", "Invalid credentials"); } const currentMatches = await bcrypt.compare( currentPassword, user.passwordHash, ); if (!currentMatches) { throw unauthorized("AUTH_INVALID_CREDENTIALS", "Invalid credentials"); } const policyCheck = validateNewPassword({ newPassword, currentPassword, }); if (!policyCheck.ok) { throw badRequest("VALIDATION_WEAK_PASSWORD", "Weak password", { ...policyCheck.policy, reasons: policyCheck.reasons, }); } const newHash = await bcrypt.hash(newPassword, BCRYPT_SALT_ROUNDS); user.passwordHash = newHash; user.mustChangePassword = false; // Defense-in-depth: invalidate any reset token when password changes. user.passwordResetToken = null; user.passwordResetExpiresAt = null; await user.save(); return json({ ok: true }, 200); }, { logPrefix: "[api/auth/change-password]" }, );