"use server"; import { createHash, randomBytes, timingSafeEqual } from "node:crypto"; import { redirect } from "next/navigation"; import { hashPassword } from "@/lib/auth/password"; import { prisma } from "@/lib/prisma"; import { sendMail } from "@/lib/services/email"; import { env } from "@/env"; const TOKEN_TTL_MS = 60 * 60 * 1000; // 1 hour function sha256(s: string): string { return createHash("sha256").update(s).digest("hex"); } export async function requestReset(formData: FormData): Promise { const email = String(formData.get("email") ?? "").trim().toLowerCase(); // Always respond the same way so we don't reveal which emails exist. if (/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { try { const user = await prisma.user.findFirst({ where: { mail: email }, select: { id: true } }); if (user) { const token = randomBytes(32).toString("hex"); await prisma.passwordReset.upsert({ where: { email }, update: { token: sha256(token), createdAt: new Date() }, create: { email, token: sha256(token), createdAt: new Date() }, }); const link = `${env.APP_URL}/reset?email=${encodeURIComponent(email)}&token=${token}`; await sendMail( email, `${env.HOTEL_NAME} — password reset`, `

Click to reset your password (valid 1 hour):

${link}

`, ); } } catch { // swallow — generic response below } } redirect("/forgot?sent=1"); } export async function resetPassword(formData: FormData): Promise { const email = String(formData.get("email") ?? "").trim().toLowerCase(); const token = String(formData.get("token") ?? "").trim(); const password = String(formData.get("password") ?? ""); let error: string | null = null; if (password.length < 6) error = "Password must be at least 6 characters"; if (!error) { try { const row = await prisma.passwordReset.findUnique({ where: { email } }); const fresh = row?.createdAt ? Date.now() - row.createdAt.getTime() < TOKEN_TTL_MS : false; const a = Buffer.from(sha256(token), "hex"); const b = row ? Buffer.from(row.token, "hex") : Buffer.alloc(a.length); const match = row != null && a.length === b.length && timingSafeEqual(a, b); if (!row || !fresh || !match) { error = "This reset link is invalid or has expired"; } else { const user = await prisma.user.findFirst({ where: { mail: email }, select: { id: true } }); if (!user) { error = "Account not found"; } else { await prisma.user.update({ where: { id: user.id }, data: { password: await hashPassword(password) }, }); await prisma.passwordReset.delete({ where: { email } }).catch(() => {}); } } } catch { error = "Could not reset the password — try again"; } } if (error) { redirect(`/reset?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}&error=${encodeURIComponent(error)}`); } redirect("/login?reset=1"); }