| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104 |
- 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]" },
- );
|