JB/web/src/app/api/auth/forgot-password/route.ts

76 lines
3 KiB
TypeScript

import { NextResponse } from "next/server";
import crypto from "crypto";
import { prisma } from "@/lib/prisma";
import { sendEmail } from "@/lib/email";
import { rateLimit } from "@/lib/rateLimit";
export async function POST(req: Request) {
// Rate limit: max 5 forgot password requests per 15 mins per IP
const rateLimitResponse = rateLimit(req, { limit: 5, windowMs: 15 * 60 * 1000 });
if (rateLimitResponse) return rateLimitResponse;
try {
const { email } = await req.json();
if (!email || typeof email !== "string") {
return NextResponse.json({ error: "Email is required" }, { status: 400 });
}
const cleanEmail = email.toLowerCase().trim();
const user = await prisma.user.findUnique({
where: { email: cleanEmail },
});
// Return generic message regardless of user existence to prevent email enumeration
const genericSuccess = NextResponse.json({
success: true,
message: "If an account exists for this email, password reset instructions have been sent.",
});
if (!user) {
return genericSuccess;
}
// Generate crypto token valid for 1 hour
const token = crypto.randomBytes(32).toString("hex");
const expiresAt = new Date(Date.now() + 60 * 60 * 1000);
await prisma.passwordResetToken.create({
data: {
userId: user.id,
token,
expiresAt,
},
});
const baseUrl = process.env.NEXTAUTH_URL || "http://localhost:3000";
const resetUrl = `${baseUrl}/reset-password?token=${token}`;
await sendEmail({
to: user.email,
subject: "🔒 JobsBoard: Reset Your Password",
html: `
<div style="font-family: system-ui, -apple-system, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
<h2 style="margin-top: 0; color: #0f172a;">Password Reset Request</h2>
<p style="color: #334155; font-size: 14px; line-height: 1.5;">
Hi ${user.name || "there"}, we received a request to reset your password for your JobsBoard account.
</p>
<p style="color: #334155; font-size: 14px; line-height: 1.5;">
Click the button below to set a new password. This link is valid for <strong>1 hour</strong> and can only be used once.
</p>
<div style="text-align: center; margin: 24px 0;">
<a href="${resetUrl}" style="background-color: #0f172a; color: white; padding: 10px 20px; border-radius: 6px; text-decoration: none; font-weight: 600; font-size: 14px;">Reset Password ↗</a>
</div>
<p style="color: #64748b; font-size: 12px;">
If you did not request a password reset, you can safely ignore this email. Your password will remain unchanged.
</p>
</div>
`,
});
return genericSuccess;
} catch (err: any) {
console.error("POST /api/auth/forgot-password error:", err);
return NextResponse.json({ error: "Failed to process request" }, { status: 500 });
}
}