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 | 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 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 28x 28x 28x 28x 28x 32x 7x 7x 7x 7x 7x 7x 21x 21x 21x 21x 21x 21x 20x 32x 12x 12x 12x 12x 12x 12x 12x 12x 12x 11x 11x 11x 11x 11x 11x 11x 10x 10x 10x 10x 10x 12x 12x 12x 10x 12x 12x 32x 8x 8x 8x 18x 18x 18x 18x 18x 18x 32x 3x 3x 3x 3x 3x 3x 3x 32x | import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { emailService } from "@/lib/email/emailService";
import { checkRateLimit, getRateLimitInfo, rateLimitPresets } from "@/lib/security";
import { addSecurityHeaders } from "@/lib/security";
import { z } from "zod";
import crypto from "crypto";
import bcryptjs from "bcryptjs";
import { logger } from "@/lib/logging";
import { PASSWORD_CONFIG, TOKEN_CONFIG } from "@/lib/security";
const ForgotPasswordSchema = z.object({
email: z.string().email("Invalid email address")});
/**
* POST /api/auth/forgot-password
* Initiates password reset flow by sending reset email
*
* Security considerations:
* - Always returns success (prevents email enumeration)
* - Rate limited per IP
* - Token is hashed before storage
* - Token expires in 1 hour
*/
export async function POST(request: NextRequest) {
try {
// Rate limiting: 5 requests per hour per IP
const ip =
request.headers.get("x-forwarded-for") ||
request.headers.get("x-real-ip") ||
"unknown";
if (
!checkRateLimit(
`forgot-password-${ip}`,
rateLimitPresets.signup.limit,
rateLimitPresets.signup.windowMs
)
) {
const rateLimitInfo = getRateLimitInfo(
`forgot-password-${ip}`,
rateLimitPresets.signup.limit
);
const response = NextResponse.json(
{ error: "Too many requests. 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 = ForgotPasswordSchema.safeParse(body);
if (!result.success) {
const response = NextResponse.json(
{ error: "Invalid email address" },
{ status: 400 }
);
return addSecurityHeaders(response);
}
const { email } = result.data;
// Find user (but always return success to prevent email enumeration)
const user = await prisma.user.findUnique({
where: { email: email.toLowerCase() }});
if (user) {
// Generate secure random token
const token = crypto.randomBytes(32).toString("hex");
// Hash the token before storing (using security config for consistency)
const hashedToken = await bcryptjs.hash(token, PASSWORD_CONFIG.BCRYPT_ROUNDS);
// Delete any existing reset tokens for this user
await prisma.passwordResetToken.deleteMany({
where: { userId: user.id }});
// Create new reset token (expires based on config)
await prisma.passwordResetToken.create({
data: {
token: hashedToken,
userId: user.id,
expiresAt: new Date(Date.now() + TOKEN_CONFIG.PASSWORD_RESET_EXPIRY_MS)}});
// Send password reset email via queue (with unhashed token)
emailService.sendPasswordReset({
id: user.id,
email: email,
name: user.name || 'there',
resetToken: token
}).then(() => {
logger.info(`Password reset email queued for ${email}`, { category: "API" });
}).catch((err) => {
logger.error(`Failed to queue password reset email for ${email}`, err instanceof Error ? err : new Error(String(err)), { category: "API" });
});
} else {
// Log attempt for non-existent user (for security monitoring)
logger.info(`Password reset requested for non-existent email: ${email}`, { category: "API" });
}
// Always return the same response (prevents email enumeration)
const response = NextResponse.json({
message:
"If an account exists with this email, a password reset link has been sent."});
return addSecurityHeaders(response);
} catch (error) {
logger.error("Forgot password error", error instanceof Error ? error : new Error(String(error)), { category: "API" });
const response = NextResponse.json(
{ error: "Failed to process request" },
{ status: 500 }
);
return addSecurityHeaders(response);
}
}
|