From e83647c652016fbd29c54d682037dd630fc8a8f4 Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Mon, 10 Jul 2023 20:13:20 +0100 Subject: [PATCH] feat: Add password checks for password-reset (#10042) Co-authored-by: zomars --- apps/web/pages/api/auth/reset-password.ts | 19 ++++ apps/web/pages/auth/forgot-password/[id].tsx | 95 +++++++------------ .../playwright/auth/forgot-password.e2e.ts | 6 +- 3 files changed, 56 insertions(+), 64 deletions(-) diff --git a/apps/web/pages/api/auth/reset-password.ts b/apps/web/pages/api/auth/reset-password.ts index 9cd81f2c7a..25dd1a2a35 100644 --- a/apps/web/pages/api/auth/reset-password.ts +++ b/apps/web/pages/api/auth/reset-password.ts @@ -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(), + }, + }); +} diff --git a/apps/web/pages/auth/forgot-password/[id].tsx b/apps/web/pages/auth/forgot-password/[id].tsx index d4549a5f0d..b76d52efeb 100644 --- a/apps/web/pages/auth/forgot-password/[id].tsx +++ b/apps/web/pages/auth/forgot-password/[id].tsx @@ -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 && } {!isRequestExpired && !success && ( <> -
{ - 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, + }); + }}>
- { - setPassword(e.target.value); - }} - id="password" - name="password" - type="password" - autoComplete="password" - required />
@@ -162,7 +135,7 @@ export default function Page({ resetPasswordRequest, csrfToken }: Props) { {t("reset_password")} -
+ )} {!isRequestExpired && success && ( diff --git a/apps/web/playwright/auth/forgot-password.e2e.ts b/apps/web/playwright/auth/forgot-password.e2e.ts index f18ae9cbce..ca87930187 100644 --- a/apps/web/playwright/auth/forgot-password.e2e.ts +++ b/apps/web/playwright/auth/forgot-password.e2e.ts @@ -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"]');