76 lines
3 KiB
TypeScript
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 });
|
|
}
|
|
}
|