import { type ActionFunction, json } from "@remix-run/node"; import { type User, TokenType } from "@prisma/client"; import db from "~/utils/db.server"; import { type FormError, validate } from "~/utils/validation.server"; import { sendForgotPasswordEmail } from "~/mailers/forgot-password-mailer.server"; import { generateToken, hashToken } from "~/utils/token.server"; import { ForgotPassword } from "../validations"; const RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS = 24; type ForgotPasswordFailureActionData = { errors: FormError; submitted?: never }; type ForgotPasswordSuccessfulActionData = { errors?: never; submitted: true }; export type ForgotPasswordActionData = ForgotPasswordFailureActionData | ForgotPasswordSuccessfulActionData; const action: ActionFunction = async ({ request }) => { const formData = Object.fromEntries(await request.formData()); const validation = validate(ForgotPassword, formData); if (validation.errors) { return json({ errors: validation.errors }); } const { email } = validation.data; const user = await db.user.findUnique({ where: { email: email.toLowerCase() } }); // always wait the same amount of time so attackers can't tell the difference whether a user is found await Promise.all([updatePassword(user), new Promise((resolve) => setTimeout(resolve, 750))]); // return the same result whether a password reset email was sent or not return json({ submitted: true }); }; export default action; async function updatePassword(user: User | null) { const membership = await db.membership.findFirst({ where: { userId: user?.id } }); if (!user || !membership) { return; } const token = generateToken(); const hashedToken = hashToken(token); const expiresAt = new Date(); expiresAt.setHours(expiresAt.getHours() + RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS); await db.token.deleteMany({ where: { type: TokenType.RESET_PASSWORD, userId: user.id } }); await db.token.create({ data: { user: { connect: { id: user.id } }, membership: { connect: { id: membership.id } }, type: TokenType.RESET_PASSWORD, expiresAt, hashedToken, sentTo: user.email, }, }); await sendForgotPasswordEmail({ to: user.email, token, userName: user.fullName, }); }