import { NextResponse } from "next/server"; import { randomBytes, createHash } from "crypto"; import { getServiceSupabase } from "@/lib/supabase/client"; export const runtime = "nodejs"; function hashToken(token: string): string { return createHash("sha256").update(token).digest("hex"); } function getAppOrigin(request: Request): string { const appUrl = process.env.NEXT_PUBLIC_APP_URL?.trim(); if (appUrl) return appUrl.replace(/\/$/, ""); const vercelUrl = process.env.VERCEL_URL?.trim(); if (vercelUrl) return `https://${vercelUrl}`; return new URL(request.url).origin; } async function sendResetEmail(params: { to: string; resetUrl: string; expiresAt: string; }): Promise { const resendApiKey = process.env.RESEND_API_KEY?.trim(); const resendFromEmail = ( process.env.RESEND_FROM_EMAIL || process.env.EMAIL_FROM )?.trim(); if (!resendApiKey || !resendFromEmail) { throw new Error("Resend is not configured. Missing RESEND_API_KEY or RESEND_FROM_EMAIL."); } const response = await fetch("https://api.resend.com/emails", { method: "POST", headers: { Authorization: `Bearer ${resendApiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ from: resendFromEmail, to: [params.to], subject: "Reset your password", html: `

Password reset requested for your account.

Reset Password

This link expires at ${params.expiresAt}.

If you did not request this, you can ignore this email.

`, text: `Password reset requested.\n\nReset link: ${params.resetUrl}\n\nThis link expires at ${params.expiresAt}.\n\nIf you did not request this, ignore this email.`, }), }); if (!response.ok) { const details = await response.text().catch(() => ""); throw new Error(`Resend API error (${response.status}): ${details}`); } } export async function POST(request: Request) { try { const body = (await request.json()) as { email?: string }; const email = (body.email || "").trim().toLowerCase(); if (!email) { return NextResponse.json({ error: "Email is required" }, { status: 400 }); } const supabase = getServiceSupabase(); // Check if user exists const { data: user } = await supabase .from("users") .select("id, email") .eq("email", email) .maybeSingle(); if (!user) { // Don't reveal if email exists or not for security return NextResponse.json({ success: true, message: "If an account exists with that email, a reset link has been sent.", }); } // Generate reset token const token = randomBytes(32).toString("hex"); const tokenHash = hashToken(token); const now = Date.now(); const expiresAt = new Date(now + 60 * 60 * 1000).toISOString(); // 1 hour const createdAt = new Date(now).toISOString(); // Invalidate old tokens for this user await supabase.from("password_reset_tokens").delete().eq("user_id", user.id); // Store new token await supabase.from("password_reset_tokens").insert({ user_id: user.id, token_hash: tokenHash, expires_at: expiresAt, created_at: createdAt, used: false, }); // Build an absolute reset URL for email delivery. const resetPath = `/reset-password?token=${token}&email=${encodeURIComponent(email)}`; const resetUrl = `${getAppOrigin(request)}${resetPath}`; try { await sendResetEmail({ to: email, resetUrl, expiresAt }); console.log(`Password reset email sent to ${email}`); } catch (sendError) { console.error("Failed to send password reset email:", sendError); // Keep local dev workflow usable even if email config is missing. if (process.env.NODE_ENV !== "production") { console.log(`\nšŸ” PASSWORD RESET REQUEST for ${email}`); console.log(` Reset URL: ${resetUrl}`); console.log(` Token expires: ${expiresAt}\n`); } else { return NextResponse.json( { error: "Unable to send reset email. Please try again later." }, { status: 500 } ); } } return NextResponse.json({ success: true, message: "If an account exists with that email, a reset link has been sent.", // In dev, include the reset URL ...(process.env.NODE_ENV !== "production" && { resetUrl }), }); } catch (error) { console.error("Forgot password error:", error); return NextResponse.json({ error: "Failed to process request" }, { status: 500 }); } }