All files / src/app/api/auth/reset-password route.ts

100% Statements 130/130
100% Branches 13/13
100% Functions 1/1
100% Lines 130/130

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 1321x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 19x 19x 19x 19x 19x 19x 19x 19x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 18x 18x 18x 18x 18x 19x 8x 8x 8x 8x 8x 8x 10x 10x 10x 10x 10x 10x 10x 10x 10x 9x 9x 9x 19x 7x 7x 6x 6x 6x 7x 9x 19x 3x 3x 3x 3x 3x 3x 3x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 5x 5x 5x 5x 5x 5x 19x 2x 2x 2x 2x 2x 2x 2x    
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { checkRateLimit, getRateLimitInfo } from "@/lib/security";
import { addSecurityHeaders } from "@/lib/security";
import { z } from "zod";
import bcryptjs from "bcryptjs";
import { logger } from "@/lib/logging";
import { PASSWORD_CONFIG } from "@/lib/security";
 
const ResetPasswordSchema = z.object({
  token: z.string().min(1, "Token is required"),
  password: z
    .string()
    .min(8, "Password must be at least 8 characters")
    .max(100, "Password too long")
    .regex(
      /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
      "Password must contain at least one uppercase letter, one lowercase letter, and one number"
    )});
 
/**
 * POST /api/auth/reset-password
 * Resets user password using valid reset token
 *
 * Security considerations:
 * - Token is verified by comparing hashes
 * - Token is invalidated after use
 * - Strong password requirements enforced
 * - Rate limited per IP
 */
export async function POST(request: NextRequest) {
  try {
    // Rate limiting: 10 requests per 15 minutes per IP
    const ip =
      request.headers.get("x-forwarded-for") ||
      request.headers.get("x-real-ip") ||
      "unknown";
 
    if (!checkRateLimit(`reset-password-${ip}`, 10, 15 * 60 * 1000)) {
//
      const rateLimitInfo = getRateLimitInfo(`reset-password-${ip}`, 10);
      const response = NextResponse.json(
        { error: "Too many attempts. Please try again later." },
        {
          status: 429,
          headers: {
            "X-RateLimit-Limit": rateLimitInfo.limit.toString(),
            "X-RateLimit-Remaining": rateLimitInfo.remaining.toString(),
            "X-RateLimit-Reset": rateLimitInfo.resetTime}}
      );
      return addSecurityHeaders(response);
    }
 
    const body = await request.json();
 
    // Validate input
    const result = ResetPasswordSchema.safeParse(body);
    if (!result.success) {
      const response = NextResponse.json(
        { error: "Invalid request", details: result.error.issues },
        { status: 400 }
      );
      return addSecurityHeaders(response);
    }
 
    const { token, password } = result.data;
 
    // Find all valid (not expired, not used) tokens
    const validTokens = await prisma.passwordResetToken.findMany({
      where: {
        expiresAt: { gt: new Date() },
        usedAt: null},
      include: { user: true }});
 
    // Find the matching token by comparing hashes
    let matchedToken = null;
    for (const resetToken of validTokens) {
      const isMatch = await bcryptjs.compare(token, resetToken.token);
      if (isMatch) {
        matchedToken = resetToken;
        break;
      }
    }
 
    if (!matchedToken) {
      logger.warn("Invalid or expired password reset token used", { category: "API" });
      const response = NextResponse.json(
        { error: "Invalid or expired reset link. Please request a new one." },
        { status: 400 }
      );
      return addSecurityHeaders(response);
    }
 
    // Hash new password
    const hashedPassword = await bcryptjs.hash(password, PASSWORD_CONFIG.BCRYPT_ROUNDS);
 
    // Update password and mark token as used in a transaction
    await prisma.$transaction([
      prisma.user.update({
        where: { id: matchedToken.userId },
        data: {
          password: hashedPassword,
          // Reset lockout on password change
          failedLoginAttempts: 0,
          lockedUntil: null}}),
      prisma.passwordResetToken.update({
        where: { id: matchedToken.id },
        data: { usedAt: new Date() }}),
      // Invalidate all other reset tokens for this user
      prisma.passwordResetToken.updateMany({
        where: {
          userId: matchedToken.userId,
          id: { not: matchedToken.id },
          usedAt: null},
        data: { usedAt: new Date() }}),
    ]);
 
    logger.info(`Password reset successful for user ${matchedToken.userId}`, { category: "API" });
 
    const response = NextResponse.json({
      message: "Password reset successful. You can now sign in with your new password."});
    return addSecurityHeaders(response);
  } catch (error) {
    logger.error("Reset password error", error instanceof Error ? error : new Error(String(error)), { category: "API" });
    const response = NextResponse.json(
      { error: "Failed to reset password" },
      { status: 500 }
    );
    return addSecurityHeaders(response);
  }
}