feat: Add password checks for password-reset (#10042)

Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
sean-brydon
2023-07-10 12:13:20 -07:00
committed by GitHub
co-authored by zomars
parent 556bf346f0
commit e83647c652
3 changed files with 56 additions and 64 deletions
+19
View File
@@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { validPassword } from "@calcom/features/auth/lib/validPassword";
import prisma from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -36,6 +37,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(400).json({ message: "Couldn't find an account for this email" });
}
if (!validPassword(rawPassword)) {
return res.status(400).json({ message: "Password does not meet the requirements" });
}
const hashedPassword = await hashPassword(rawPassword);
await prisma.user.update({
@@ -47,9 +52,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
});
await expireResetPasswordRequest(rawRequestId);
return res.status(201).json({ message: "Password reset." });
} catch (reason) {
console.error(reason);
return res.status(500).json({ message: "Unable to create password reset request" });
}
}
async function expireResetPasswordRequest(rawRequestId: string) {
await prisma.resetPasswordRequest.update({
where: {
id: rawRequestId,
},
data: {
// We set the expiry to now to invalidate the request
expires: new Date(),
},
});
}
+34 -61
View File
@@ -1,16 +1,16 @@
import type { ResetPasswordRequest } from "@prisma/client";
import { debounce } from "lodash";
import type { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Link from "next/link";
import type { CSSProperties } from "react";
import React, { useMemo } from "react";
import { useForm } from "react-hook-form";
import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { Button, TextField } from "@calcom/ui";
import { Button, PasswordField, Form } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
import AuthContainer from "@components/ui/AuthContainer";
@@ -23,40 +23,22 @@ type Props = {
export default function Page({ resetPasswordRequest, csrfToken }: Props) {
const { t } = useLocale();
const [loading, setLoading] = React.useState(false);
const [, setError] = React.useState<{ message: string } | null>(null);
const [success, setSuccess] = React.useState(false);
const [password, setPassword] = React.useState("");
const formMethods = useForm<{ new_password: string }>();
const success = formMethods.formState.isSubmitSuccessful;
const loading = formMethods.formState.isSubmitting;
const submitChangePassword = async ({ password, requestId }: { password: string; requestId: string }) => {
try {
const res = await fetch("/api/auth/reset-password", {
method: "POST",
body: JSON.stringify({ requestId: requestId, password: password }),
headers: {
"Content-Type": "application/json",
},
});
const json = await res.json();
if (!res.ok) {
setError(json);
} else {
setSuccess(true);
}
return json;
} catch (reason) {
setError({ message: t("unexpected_error_try_again") });
} finally {
setLoading(false);
}
const res = await fetch("/api/auth/reset-password", {
method: "POST",
body: JSON.stringify({ requestId, password }),
headers: {
"Content-Type": "application/json",
},
});
const json = await res.json();
if (!res.ok) return formMethods.setError("new_password", { type: "server", message: json.message });
};
const debouncedChangePassword = debounce(submitChangePassword, 250);
const Success = () => {
return (
<>
@@ -109,8 +91,9 @@ export default function Page({ resetPasswordRequest, csrfToken }: Props) {
{isRequestExpired && <Expired />}
{!isRequestExpired && !success && (
<>
<form
<Form
className="space-y-6"
form={formMethods}
style={
{
"--cal-brand": "#111827",
@@ -119,36 +102,26 @@ export default function Page({ resetPasswordRequest, csrfToken }: Props) {
"--cal-brand-subtle": "#9CA3AF",
} as CSSProperties
}
onSubmit={async (e) => {
e.preventDefault();
if (!password) {
return;
}
if (loading) {
return;
}
setLoading(true);
setError(null);
setSuccess(false);
await debouncedChangePassword({ password, requestId: resetPasswordRequest.id });
}}
action="#">
handleSubmit={async (values) => {
await submitChangePassword({
password: values.new_password,
requestId: resetPasswordRequest.id,
});
}}>
<input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden />
<div className="mt-1">
<TextField
<PasswordField
{...formMethods.register("new_password", {
minLength: {
message: t("password_hint_min"),
value: 7, // We don't have user here so we can't check if they are admin or not
},
pattern: {
message: "Should contain a number, uppercase and lowercase letters",
value: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).*$/gm,
},
})}
label={t("new_password")}
onChange={(e) => {
setPassword(e.target.value);
}}
id="password"
name="password"
type="password"
autoComplete="password"
required
/>
</div>
@@ -162,7 +135,7 @@ export default function Page({ resetPasswordRequest, csrfToken }: Props) {
{t("reset_password")}
</Button>
</div>
</form>
</Form>
</>
)}
{!isRequestExpired && success && (
@@ -8,7 +8,7 @@ test("Can reset forgotten password", async ({ page, users }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(process.env.NEXT_PUBLIC_IS_E2E !== "1", "It shouldn't if we can't skip email");
const user = await users.create();
const newPassword = `${user.username}-123`;
const newPassword = `${user.username}-123CAL`; // To match the password policy
// Got to reset password flow
await page.goto("/auth/forgot-password");
@@ -24,8 +24,8 @@ test("Can reset forgotten password", async ({ page, users }) => {
// Wait for page to fully load
await page.waitForSelector("text=Reset Password");
// Fill input[name="password"]
await page.fill('input[name="password"]', newPassword);
// Fill input[name="new_password"]
await page.fill('input[name="new_password"]', newPassword);
// Click text=Submit
await page.click('button[type="submit"]');