Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore/forgot password pages #168

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions apps/web/app/future/auth/forgot-password/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import SetNewUserPassword from "@pages/auth/forgot-password/[id]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";

import { getServerSideProps } from "@server/lib/forgot-password/[id]/getServerSideProps";

export const generateMetadata = async () => {
return await _generateMetadata(
(t) => t("reset_password"),
(t) => t("change_your_password")
);
};

export default WithLayout({
getLayout: null,
Page: SetNewUserPassword,
getData: withAppDirSsr(getServerSideProps),
})<"P">;
19 changes: 19 additions & 0 deletions apps/web/app/future/auth/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import ForgotPassword from "@pages/auth/forgot-password";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";

import { getServerSideProps } from "@server/lib/forgot-password/getServerSideProps";

export const generateMetadata = async () => {
return await _generateMetadata(
(t) => t("reset_password"),
(t) => t("change_your_password")
);
};

export default WithLayout({
getLayout: null,
Page: ForgotPassword,
getData: withAppDirSsr(getServerSideProps),
})<"P">;
42 changes: 6 additions & 36 deletions apps/web/pages/auth/forgot-password/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import type { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
"use client";

import Link from "next/link";
import type { CSSProperties } from "react";
import { useForm } from "react-hook-form";

import { getLocale } from "@calcom/features/auth/lib/getLocale";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { Button, PasswordField, Form } from "@calcom/ui";

import PageWrapper from "@components/PageWrapper";
import AuthContainer from "@components/ui/AuthContainer";

import { getServerSideProps } from "@server/lib/forgot-password/[id]/getServerSideProps";

type Props = {
requestId: string;
isRequestExpired: boolean;
csrfToken: string;
csrfToken: string | undefined;
};

export default function Page({ requestId, isRequestExpired, csrfToken }: Props) {
Expand Down Expand Up @@ -143,33 +142,4 @@ export default function Page({ requestId, isRequestExpired, csrfToken }: Props)
}

Page.PageWrapper = PageWrapper;
export async function getServerSideProps(context: GetServerSidePropsContext) {
const id = context.params?.id as string;

let resetPasswordRequest = await prisma.resetPasswordRequest.findFirst({
where: {
id,
expires: {
gt: new Date(),
},
},
select: {
email: true,
},
});
try {
resetPasswordRequest &&
(await prisma.user.findUniqueOrThrow({ where: { email: resetPasswordRequest.email } }));
} catch (e) {
resetPasswordRequest = null;
}
const locale = await getLocale(context.req);
return {
props: {
isRequestExpired: !resetPasswordRequest,
requestId: id,
csrfToken: await getCsrfToken({ req: context.req }),
...(await serverSideTranslations(locale, ["common"])),
},
};
}
export { getServerSideProps };
35 changes: 9 additions & 26 deletions apps/web/pages/auth/forgot-password/index.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
"use client";

// eslint-disable-next-line no-restricted-imports
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, SyntheticEvent } from "react";
import React from "react";

import { getLocale } from "@calcom/features/auth/lib/getLocale";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, EmailField } from "@calcom/ui";

import { type inferSSRProps } from "@lib/types/inferSSRProps";

import PageWrapper from "@components/PageWrapper";
import AuthContainer from "@components/ui/AuthContainer";

export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
import { getServerSideProps } from "@server/lib/forgot-password/getServerSideProps";

export default function ForgotPassword(props: inferSSRProps<typeof getServerSideProps>) {
const csrfToken = "csrfToken" in props ? (props.csrfToken as string) : undefined;
const { t } = useLocale();
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<{ message: string } | null>(null);
Expand Down Expand Up @@ -145,23 +147,4 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {

ForgotPassword.PageWrapper = PageWrapper;

export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, res } = context;

const session = await getServerSession({ req });

// @TODO res will not be available in future pages (app dir)
if (session) {
res.writeHead(302, { Location: "/" });
res.end();
return { props: {} };
}
const locale = await getLocale(context.req);

return {
props: {
csrfToken: await getCsrfToken(context),
...(await serverSideTranslations(locale, ["common"])),
},
};
};
export { getServerSideProps };
165 changes: 84 additions & 81 deletions apps/web/playwright/auth/forgot-password.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,89 +5,92 @@ import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
import prisma from "@calcom/prisma";

import { test } from "../lib/fixtures";
import { testBothFutureAndLegacyRoutes } from "../lib/future-legacy-routes";

test.afterEach(({ users }) => users.deleteAll());

test("Can reset forgotten password", async ({ page, users }) => {
const user = await users.create();

// Got to reset password flow
await page.goto("/auth/forgot-password");

await page.fill('input[name="email"]', `${user.username}@example.com`);
await page.press('input[name="email"]', "Enter");

// wait for confirm page.
await page.waitForSelector("text=Reset link sent");

// As a workaround, we query the db for the last created password request
// there should be one, otherwise we throw
const { id } = await prisma.resetPasswordRequest.findFirstOrThrow({
where: {
email: user.email,
},
select: {
id: true,
},
orderBy: {
createdAt: "desc",
},
testBothFutureAndLegacyRoutes.describe("Forgot password", async () => {
test("Can reset forgotten password", async ({ page, users }) => {
const user = await users.create();

// Got to reset password flow
await page.goto("/auth/forgot-password");

await page.fill('input[name="email"]', `${user.username}@example.com`);
await page.press('input[name="email"]', "Enter");

// wait for confirm page.
await page.waitForSelector("text=Reset link sent");

// As a workaround, we query the db for the last created password request
// there should be one, otherwise we throw
const { id } = await prisma.resetPasswordRequest.findFirstOrThrow({
where: {
email: user.email,
},
select: {
id: true,
},
orderBy: {
createdAt: "desc",
},
});

// Test when a user changes his email after starting the password reset flow
await prisma.user.update({
where: {
email: user.email,
},
data: {
email: `${user.username}[email protected]`,
},
});

await page.goto(`/auth/forgot-password/${id}`);

await page.waitForSelector("text=That request is expired.");

// Change the email back to continue testing.
await prisma.user.update({
where: {
email: `${user.username}[email protected]`,
},
data: {
email: user.email,
},
});

await page.goto(`/auth/forgot-password/${id}`);

const newPassword = `${user.username}-123CAL-${uuid().toString()}`; // To match the password policy

// Wait for page to fully load
await page.waitForSelector("text=Reset Password");

await page.fill('input[name="new_password"]', newPassword);
await page.click('button[type="submit"]');

await page.waitForSelector("text=Password updated");

await expect(page.locator(`text=Password updated`)).toBeVisible();
// now we check our DB to confirm the password was indeed updated.
// we're not logging in to the UI to speed up test performance.
const updatedUser = await prisma.user.findUniqueOrThrow({
where: {
email: user.email,
},
select: {
id: true,
password: true,
},
});

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expect(await verifyPassword(newPassword, updatedUser.password!)).toBeTruthy();

// finally, make sure the same URL cannot be used to reset the password again, as it should be expired.
await page.goto(`/auth/forgot-password/${id}`);

await expect(page.locator(`text=Whoops`)).toBeVisible();
});

// Test when a user changes his email after starting the password reset flow
await prisma.user.update({
where: {
email: user.email,
},
data: {
email: `${user.username}[email protected]`,
},
});

await page.goto(`/auth/forgot-password/${id}`);

await page.waitForSelector("text=That request is expired.");

// Change the email back to continue testing.
await prisma.user.update({
where: {
email: `${user.username}[email protected]`,
},
data: {
email: user.email,
},
});

await page.goto(`/auth/forgot-password/${id}`);

const newPassword = `${user.username}-123CAL-${uuid().toString()}`; // To match the password policy

// Wait for page to fully load
await page.waitForSelector("text=Reset Password");

await page.fill('input[name="new_password"]', newPassword);
await page.click('button[type="submit"]');

await page.waitForSelector("text=Password updated");

await expect(page.locator(`text=Password updated`)).toBeVisible();
// now we check our DB to confirm the password was indeed updated.
// we're not logging in to the UI to speed up test performance.
const updatedUser = await prisma.user.findUniqueOrThrow({
where: {
email: user.email,
},
select: {
id: true,
password: true,
},
});

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expect(await verifyPassword(newPassword, updatedUser.password!)).toBeTruthy();

// finally, make sure the same URL cannot be used to reset the password again, as it should be expired.
await page.goto(`/auth/forgot-password/${id}`);

await expect(page.locator(`text=Whoops`)).toBeVisible();
});
37 changes: 37 additions & 0 deletions apps/web/server/lib/forgot-password/[id]/getServerSideProps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";

import { getLocale } from "@calcom/features/auth/lib/getLocale";
import prisma from "@calcom/prisma";

export async function getServerSideProps(context: GetServerSidePropsContext) {
const id = context.params?.id as string;

let resetPasswordRequest = await prisma.resetPasswordRequest.findFirst({
where: {
id,
expires: {
gt: new Date(),
},
},
select: {
email: true,
},
});
try {
resetPasswordRequest &&
(await prisma.user.findUniqueOrThrow({ where: { email: resetPasswordRequest.email } }));
} catch (e) {
resetPasswordRequest = null;
}
const locale = await getLocale(context.req);
return {
props: {
isRequestExpired: !resetPasswordRequest,
requestId: id,
csrfToken: await getCsrfToken({ req: context.req }),
...(await serverSideTranslations(locale, ["common"])),
},
};
}
27 changes: 27 additions & 0 deletions apps/web/server/lib/forgot-password/getServerSideProps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";

import { getLocale } from "@calcom/features/auth/lib/getLocale";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";

export async function getServerSideProps(context: GetServerSidePropsContext) {
const { req, res } = context;

const session = await getServerSession({ req });

// @TODO res will not be available in future pages (app dir)
if (session) {
res.writeHead(302, { Location: "/" });
res.end();
return { props: {} };
}
const locale = await getLocale(context.req);

return {
props: {
csrfToken: await getCsrfToken(context),
...(await serverSideTranslations(locale, ["common"])),
},
};
}
Loading