style reset password form

This commit is contained in:
m5r 2021-09-25 22:05:39 +08:00
parent d8acd6c37c
commit 9d30930f96
2 changed files with 61 additions and 65 deletions

View File

@ -1,42 +1,40 @@
import { resolver, generateToken, hash256 } from "blitz"; import { resolver, generateToken, hash256 } from "blitz";
import db from "../../../db"; import db, { User } from "../../../db";
import { forgotPasswordMailer } from "../../../mailers/forgot-password-mailer"; import { forgotPasswordMailer } from "../../../mailers/forgot-password-mailer";
import { ForgotPassword } from "../validations"; import { ForgotPassword } from "../validations";
const RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS = 4; const RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS = 4;
export default resolver.pipe(resolver.zod(ForgotPassword), async ({ email }) => { export default resolver.pipe(resolver.zod(ForgotPassword), async ({ email }) => {
// 1. Get the user
const user = await db.user.findFirst({ where: { email: email.toLowerCase() } }); const user = await db.user.findFirst({ where: { email: email.toLowerCase() } });
// 2. Generate the token and expiration date. // 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;
});
async function updatePassword(user: User | null) {
if (!user) {
return;
}
const token = generateToken(); const token = generateToken();
const hashedToken = hash256(token); const hashedToken = hash256(token);
const expiresAt = new Date(); const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS); expiresAt.setHours(expiresAt.getHours() + RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS);
// 3. If user with this email was found await db.token.deleteMany({ where: { type: "RESET_PASSWORD", userId: user.id } });
if (user) { await db.token.create({
// 4. Delete any existing password reset tokens data: {
await db.token.deleteMany({ where: { type: "RESET_PASSWORD", userId: user.id } }); user: { connect: { id: user.id } },
// 5. Save this new token in the database. type: "RESET_PASSWORD",
await db.token.create({ expiresAt,
data: { hashedToken,
user: { connect: { id: user.id } }, sentTo: user.email,
type: "RESET_PASSWORD", },
expiresAt, });
hashedToken, await forgotPasswordMailer({ to: user.email, token }).send();
sentTo: user.email, }
},
});
// 6. Send the email
await forgotPasswordMailer({ to: user.email, token }).send();
} else {
// 7. If no user found wait the same time so attackers can't tell the difference
await new Promise((resolve) => setTimeout(resolve, 750));
}
// 8. Return the same result whether a password reset email was sent or not
return;
});

View File

@ -2,57 +2,55 @@ import type { BlitzPage, GetServerSideProps } from "blitz";
import { useRouterQuery, Link, useMutation, Routes } from "blitz"; import { useRouterQuery, Link, useMutation, Routes } from "blitz";
import BaseLayout from "../../core/layouts/base-layout"; import BaseLayout from "../../core/layouts/base-layout";
import { LabeledTextField } from "../../core/components/labeled-text-field"; import { AuthForm as Form, FORM_ERROR } from "../components/auth-form";
import { Form, FORM_ERROR } from "../../core/components/form"; import { LabeledTextField } from "../components/labeled-text-field";
import { ResetPassword } from "../validations"; import { ResetPassword } from "../validations";
import resetPassword from "../../auth/mutations/reset-password"; import resetPassword from "../../auth/mutations/reset-password";
const ResetPasswordPage: BlitzPage = () => { const ResetPasswordPage: BlitzPage = () => {
const query = useRouterQuery(); const query = useRouterQuery();
console.log("client query", query);
const [resetPasswordMutation, { isSuccess }] = useMutation(resetPassword); const [resetPasswordMutation, { isSuccess }] = useMutation(resetPassword);
return ( return (
<div> <Form
<h1>Set a New Password</h1> texts={{
title: isSuccess ? "Password reset successfully" : "Set a new password",
subtitle: "",
submit: "Reset password",
}}
schema={ResetPassword}
initialValues={{
password: "",
passwordConfirmation: "",
token: query.token as string,
}}
onSubmit={async (values) => {
try {
await resetPasswordMutation(values);
} catch (error: any) {
if (error.name === "ResetPasswordError") {
return {
[FORM_ERROR]: error.message,
};
} else {
return {
[FORM_ERROR]: "Sorry, we had an unexpected error. Please try again.",
};
}
}
}}
>
{isSuccess ? ( {isSuccess ? (
<div> <p>
<h2>Password Reset Successfully</h2> Go to the <Link href={Routes.LandingPage()}>homepage</Link>
<p> </p>
Go to the <Link href={Routes.LandingPage()}>homepage</Link>
</p>
</div>
) : ( ) : (
<Form <>
submitText="Reset Password"
schema={ResetPassword}
initialValues={{
password: "",
passwordConfirmation: "",
token: query.token as string,
}}
onSubmit={async (values) => {
try {
await resetPasswordMutation(values);
} catch (error: any) {
if (error.name === "ResetPasswordError") {
return {
[FORM_ERROR]: error.message,
};
} else {
return {
[FORM_ERROR]: "Sorry, we had an unexpected error. Please try again.",
};
}
}
}}
>
<LabeledTextField name="password" label="New Password" type="password" /> <LabeledTextField name="password" label="New Password" type="password" />
<LabeledTextField name="passwordConfirmation" label="Confirm New Password" type="password" /> <LabeledTextField name="passwordConfirmation" label="Confirm New Password" type="password" />
</Form> </>
)} )}
</div> </Form>
); );
}; };