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,26 +1,32 @@
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
if (user) {
// 4. Delete any existing password reset tokens
await db.token.deleteMany({ where: { type: "RESET_PASSWORD", userId: user.id } }); await db.token.deleteMany({ where: { type: "RESET_PASSWORD", userId: user.id } });
// 5. Save this new token in the database.
await db.token.create({ await db.token.create({
data: { data: {
user: { connect: { id: user.id } }, user: { connect: { id: user.id } },
@ -30,13 +36,5 @@ export default resolver.pipe(resolver.zod(ForgotPassword), async ({ email }) =>
sentTo: user.email, sentTo: user.email,
}, },
}); });
// 6. Send the email
await forgotPasswordMailer({ to: user.email, token }).send(); 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,30 +2,22 @@ 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>
<h1>Set a New Password</h1>
{isSuccess ? (
<div>
<h2>Password Reset Successfully</h2>
<p>
Go to the <Link href={Routes.LandingPage()}>homepage</Link>
</p>
</div>
) : (
<Form <Form
submitText="Reset Password" texts={{
title: isSuccess ? "Password reset successfully" : "Set a new password",
subtitle: "",
submit: "Reset password",
}}
schema={ResetPassword} schema={ResetPassword}
initialValues={{ initialValues={{
password: "", password: "",
@ -48,11 +40,17 @@ const ResetPasswordPage: BlitzPage = () => {
} }
}} }}
> >
{isSuccess ? (
<p>
Go to the <Link href={Routes.LandingPage()}>homepage</Link>
</p>
) : (
<>
<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>
); );
}; };