import { json, redirect, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { Form, useActionData, useLoaderData, useNavigation } from "@remix-run/react"; import bcrypt from "bcryptjs"; import crypto from "crypto"; import { sendNotificationEmail } from "~/utils/mail.server"; import { prisma } from "~/utils/db.server"; export async function loader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); const token = url.searchParams.get("token"); if (!token) { return json({ showEmailForm: true, token: null, error: null }); } // Check if token is valid const resetToken = await prisma.passwordResetToken.findUnique({ where: { token }, include: { employee: true } }); if (!resetToken) { return json({ showEmailForm: false, token: null, error: "Invalid reset token. Please request a new password reset." }); } if (resetToken.used) { return json({ showEmailForm: false, token: null, error: "This reset token has already been used. Please request a new password reset." }); } if (new Date() > resetToken.expiresAt) { return json({ showEmailForm: false, token: null, error: "This reset token has expired. Please request a new password reset." }); } return json({ showEmailForm: false, token, error: null, employeeName: resetToken.employee.name }); } export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const intent = formData.get("intent"); if (intent === "send-reset-email") { const email = formData.get("email")?.toString(); if (!email) { return json({ error: "Email is required" }, { status: 400 }); } // Find employee by email const employee = await prisma.employee.findUnique({ where: { email } }); if (!employee) { // Don't reveal if email exists or not for security return json({ success: true, message: "If an account with this email exists, you will receive a password reset link." }); } // Generate reset token const token = crypto.randomBytes(32).toString("hex"); const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour from now // Save token to database await prisma.passwordResetToken.create({ data: { token, employeeId: employee.id, expiresAt } }); // Send reset email const resetUrl = `${new URL(request.url).origin}/reset-password?token=${token}`; const emailResult = await sendNotificationEmail( employee.email, "Password Reset Request", `Hello ${employee.name},\n\nYou requested a password reset. Click the link below to reset your password:\n\n${resetUrl}\n\nThis link will expire in 1 hour.\n\nIf you didn't request this, please ignore this email.`, false ); if (!emailResult.success) { return json({ error: "Failed to send reset email. Please try again." }, { status: 500 }); } return json({ success: true, message: "If an account with this email exists, you will receive a password reset link." }); } if (intent === "reset-password") { const token = formData.get("token")?.toString(); const password = formData.get("password")?.toString(); const confirmPassword = formData.get("confirmPassword")?.toString(); if (!token || !password || !confirmPassword) { return json({ error: "All fields are required" }, { status: 400 }); } if (password !== confirmPassword) { return json({ error: "Passwords do not match" }, { status: 400 }); } if (password.length < 6) { return json({ error: "Password must be at least 6 characters long" }, { status: 400 }); } // Verify token again const resetToken = await prisma.passwordResetToken.findUnique({ where: { token }, include: { employee: true } }); if (!resetToken || resetToken.used || new Date() > resetToken.expiresAt) { return json({ error: "Invalid or expired token" }, { status: 400 }); } // Hash new password const hashedPassword = await bcrypt.hash(password, 10); // Update password and mark token as used await prisma.$transaction([ prisma.employee.update({ where: { id: resetToken.employeeId }, data: { password: hashedPassword } }), prisma.passwordResetToken.update({ where: { id: resetToken.id }, data: { used: true } }) ]); return redirect("/signin?message=Password reset successful. Please sign in with your new password."); } return json({ error: "Invalid request" }, { status: 400 }); } export default function ResetPassword() { const loaderData = useLoaderData(); const actionData = useActionData(); const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting"; if (loaderData.error) { return (

Password Reset Error

{loaderData.error}

); } if (loaderData.showEmailForm) { return (

Reset your password

Enter your email address and we'll send you a link to reset your password.

{actionData?.error && (

{actionData.error}

)} {actionData?.success && (

{actionData.message}

)}
); } // Show password reset form return (

Set new password

{loaderData.employeeName && `Hello ${loaderData.employeeName}, `} Enter your new password below.

{actionData?.error && (

{actionData.error}

)}
); }