import { sql, transaction } from "@/lib/db"; import { isDevOtpExposed, sendEmail } from "@/lib/mailer"; import { CODE_TTL_MINUTES, generateCode, hashCode, resetEmail, } from "@/lib/otp"; export async function POST(req: Request) { const { email } = await req.json(); if (!email?.trim()) { return Response.json({ error: "Email is required." }, { status: 400 }); } const normalized = email.trim().toLowerCase(); try { const users = await sql<{ id: string }>` SELECT id FROM users WHERE email = ${normalized} `; // Don't reveal whether the address is registered: always answer the same. if (!users[0]) { return Response.json({ data: { sent: false } }); } const code = await transaction(async (tx) => { const generated = generateCode(); await tx` INSERT INTO password_reset_codes (email, code_hash, expires_at) VALUES ( ${normalized}, ${hashCode(normalized, generated)}, CURRENT_TIMESTAMP + make_interval(mins => ${CODE_TTL_MINUTES}) ) ON CONFLICT (email) DO UPDATE SET code_hash = EXCLUDED.code_hash, expires_at = EXCLUDED.expires_at, attempts = 0 `; return generated; }); const mail = resetEmail(code); const delivered = await sendEmail(normalized, mail.subject, mail.text); return Response.json({ data: { sent: delivered, // Without SMTP configured there is nothing to receive, so surface the // code to keep the reset flow usable on a self-hosted box. Never // expose the code in production, even on delivery failure. ...(delivered || !isDevOtpExposed() ? {} : { devCode: code }), }, }); } catch (error) { console.error("[FORGOT_PASSWORD]: ", error); return Response.json({ error: "Internal Server Error" }, { status: 500 }); } }