mission-control/app/api/auth/forgot-password/route.ts

146 lines
4.8 KiB
TypeScript

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<void> {
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: `
<div style="font-family:Arial,sans-serif;line-height:1.5;color:#111827;">
<p>Password reset requested for your account.</p>
<p>
<a href="${params.resetUrl}" style="display:inline-block;padding:10px 14px;background:#111827;color:#ffffff;text-decoration:none;border-radius:6px;">
Reset Password
</a>
</p>
<p>This link expires at ${params.expiresAt}.</p>
<p>If you did not request this, you can ignore this email.</p>
</div>
`,
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 });
}
}