127 lines
3.6 KiB
TypeScript
127 lines
3.6 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { 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");
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
try {
|
|
const body = (await request.json()) as {
|
|
token?: string;
|
|
email?: string;
|
|
password?: string;
|
|
};
|
|
|
|
const token = body.token || "";
|
|
const email = (body.email || "").trim().toLowerCase();
|
|
const password = body.password || "";
|
|
|
|
if (!token || !email || !password) {
|
|
return NextResponse.json(
|
|
{ error: "Token, email, and password are required" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
if (password.length < 8) {
|
|
return NextResponse.json(
|
|
{ error: "Password must be at least 8 characters" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const supabase = getServiceSupabase();
|
|
const tokenHash = hashToken(token);
|
|
const now = new Date().toISOString();
|
|
|
|
// Find valid token with user info
|
|
const { data: resetTokenRaw } = await supabase
|
|
.from("password_reset_tokens")
|
|
.select("id, user_id, users(email, name)")
|
|
.eq("token_hash", tokenHash)
|
|
.eq("used", false)
|
|
.gt("expires_at", now)
|
|
.maybeSingle();
|
|
|
|
const resetToken = resetTokenRaw as
|
|
| {
|
|
id: string;
|
|
user_id: string;
|
|
users?:
|
|
| {
|
|
email?: string;
|
|
name?: string;
|
|
}
|
|
| Array<{
|
|
email?: string;
|
|
name?: string;
|
|
}>;
|
|
}
|
|
| null;
|
|
|
|
if (!resetToken) {
|
|
return NextResponse.json(
|
|
{ error: "Invalid or expired reset token" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Get user email from the nested users object
|
|
const userEmail = Array.isArray(resetToken.users)
|
|
? resetToken.users[0]?.email
|
|
: resetToken.users?.email;
|
|
const userName = Array.isArray(resetToken.users)
|
|
? resetToken.users[0]?.name
|
|
: resetToken.users?.name;
|
|
|
|
if (userEmail?.toLowerCase() !== email) {
|
|
return NextResponse.json({ error: "Invalid reset token" }, { status: 400 });
|
|
}
|
|
|
|
// Update Supabase Auth password. If auth user doesn't exist yet (legacy migration),
|
|
// create it with the same UUID so app foreign keys remain valid.
|
|
const { error: updateError } = await supabase.auth.admin.updateUserById(resetToken.user_id, {
|
|
password,
|
|
});
|
|
|
|
if (updateError) {
|
|
const updateMessage = updateError.message || "";
|
|
if (updateMessage.toLowerCase().includes("not found")) {
|
|
const { error: createError } = await supabase.auth.admin.createUser({
|
|
id: resetToken.user_id,
|
|
email,
|
|
password,
|
|
email_confirm: true,
|
|
user_metadata: {
|
|
name: typeof userName === "string" && userName.trim().length > 0 ? userName : undefined,
|
|
},
|
|
});
|
|
if (createError) throw createError;
|
|
} else {
|
|
throw updateError;
|
|
}
|
|
}
|
|
|
|
// Mark token as used
|
|
await supabase
|
|
.from("password_reset_tokens")
|
|
.update({ used: true })
|
|
.eq("id", resetToken.id);
|
|
|
|
// Delete all sessions for this user (force re-login)
|
|
await supabase.from("sessions").delete().eq("user_id", resetToken.user_id);
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: "Password reset successfully",
|
|
});
|
|
} catch (error) {
|
|
console.error("Reset password error:", error);
|
|
return NextResponse.json({ error: "Failed to reset password" }, { status: 500 });
|
|
}
|
|
}
|