From c11e76bac914d8791d3cf911fd508780d0866972 Mon Sep 17 00:00:00 2001 From: Sasha Date: Sat, 20 Jan 2024 00:14:14 +0200 Subject: [PATCH] chore: [app-router-migration 30] migrate the "login" page (#13195) Co-authored-by: Dmytro Hryshyn Co-authored-by: Benny Joo Co-authored-by: zomars --- apps/web/app/future/auth/login/page.tsx | 22 ++++ apps/web/pages/auth/login.tsx | 108 +----------------- apps/web/playwright/login.e2e.ts | 7 +- .../lib/auth/login/getServerSideProps.tsx | 102 +++++++++++++++++ 4 files changed, 134 insertions(+), 105 deletions(-) create mode 100644 apps/web/app/future/auth/login/page.tsx create mode 100644 apps/web/server/lib/auth/login/getServerSideProps.tsx diff --git a/apps/web/app/future/auth/login/page.tsx b/apps/web/app/future/auth/login/page.tsx new file mode 100644 index 0000000000..41565c1e6b --- /dev/null +++ b/apps/web/app/future/auth/login/page.tsx @@ -0,0 +1,22 @@ +import Login from "@pages/auth/login"; +import { withAppDirSsr } from "app/WithAppDirSsr"; +import { _generateMetadata } from "app/_utils"; +import { WithLayout } from "app/layoutHOC"; +import type { InferGetServerSidePropsType } from "next"; + +import { APP_NAME } from "@calcom/lib/constants"; + +import { getServerSideProps } from "@server/lib/auth/login/getServerSideProps"; + +export const generateMetadata = async () => { + return await _generateMetadata( + (t) => `${t("login")} | ${APP_NAME}`, + (t) => t("login") + ); +}; + +export default WithLayout({ + getLayout: null, + Page: Login, + getData: withAppDirSsr>(getServerSideProps), +})<"P">; diff --git a/apps/web/pages/auth/login.tsx b/apps/web/pages/auth/login.tsx index 856e6a7153..b75304603e 100644 --- a/apps/web/pages/auth/login.tsx +++ b/apps/web/pages/auth/login.tsx @@ -1,8 +1,8 @@ +"use client"; + import { zodResolver } from "@hookform/resolvers/zod"; import classNames from "classnames"; -import { jwtVerify } from "jose"; -import type { GetServerSidePropsContext } from "next"; -import { getCsrfToken, signIn } from "next-auth/react"; +import { signIn } from "next-auth/react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState } from "react"; @@ -12,21 +12,17 @@ import { z } from "zod"; import { SAMLLogin } from "@calcom/features/auth/SAMLLogin"; import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; -import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml"; import { WEBAPP_URL, WEBSITE_URL, HOSTED_CAL_FEATURES } from "@calcom/lib/constants"; import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; -import prisma from "@calcom/prisma"; import { trpc } from "@calcom/trpc/react"; import { Alert, Button, EmailField, PasswordField } from "@calcom/ui"; import { ArrowLeft, Lock } from "@calcom/ui/components/icon"; import type { inferSSRProps } from "@lib/types/inferSSRProps"; import type { WithNonceProps } from "@lib/withNonce"; -import withNonce from "@lib/withNonce"; import AddToHomescreen from "@components/AddToHomescreen"; import PageWrapper from "@components/PageWrapper"; @@ -34,8 +30,7 @@ import BackupCode from "@components/auth/BackupCode"; import TwoFactor from "@components/auth/TwoFactor"; import AuthContainer from "@components/ui/AuthContainer"; -import { IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants"; -import { ssrInit } from "@server/lib/ssr"; +import { getServerSideProps } from "@server/lib/auth/login/getServerSideProps"; interface LoginValues { email: string; @@ -52,7 +47,7 @@ export default function Login({ samlProductID, totpEmail, }: // eslint-disable-next-line @typescript-eslint/ban-types -inferSSRProps & WithNonceProps<{}>) { +inferSSRProps & WithNonceProps<{}>) { const searchParams = useCompatSearchParams(); const { t } = useLocale(); const router = useRouter(); @@ -269,97 +264,6 @@ inferSSRProps & WithNonceProps<{}>) { ); } -// TODO: Once we understand how to retrieve prop types automatically from getServerSideProps, remove this temporary variable -const _getServerSideProps = async function getServerSideProps(context: GetServerSidePropsContext) { - const { req, res, query } = context; - - const session = await getServerSession({ req, res }); - const ssr = await ssrInit(context); - - const verifyJwt = (jwt: string) => { - const secret = new TextEncoder().encode(process.env.CALENDSO_ENCRYPTION_KEY); - - return jwtVerify(jwt, secret, { - issuer: WEBSITE_URL, - audience: `${WEBSITE_URL}/auth/login`, - algorithms: ["HS256"], - }); - }; - - let totpEmail = null; - if (context.query.totp) { - try { - const decryptedJwt = await verifyJwt(context.query.totp as string); - if (decryptedJwt.payload) { - totpEmail = decryptedJwt.payload.email as string; - } else { - return { - redirect: { - destination: "/auth/error?error=JWT%20Invalid%20Payload", - permanent: false, - }, - }; - } - } catch (e) { - return { - redirect: { - destination: "/auth/error?error=Invalid%20JWT%3A%20Please%20try%20again", - permanent: false, - }, - }; - } - } - - if (session) { - const { callbackUrl } = query; - - if (callbackUrl) { - try { - const destination = getSafeRedirectUrl(callbackUrl as string); - if (destination) { - return { - redirect: { - destination, - permanent: false, - }, - }; - } - } catch (e) { - console.warn(e); - } - } - - return { - redirect: { - destination: "/", - permanent: false, - }, - }; - } - - const userCount = await prisma.user.count(); - if (userCount === 0) { - // Proceed to new onboarding to create first admin user - return { - redirect: { - destination: "/auth/setup", - permanent: false, - }, - }; - } - return { - props: { - csrfToken: await getCsrfToken(context), - trpcState: ssr.dehydrate(), - isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED, - isSAMLLoginEnabled, - samlTenantID, - samlProductID, - totpEmail, - }, - }; -}; +export { getServerSideProps }; Login.PageWrapper = PageWrapper; - -export const getServerSideProps = withNonce(_getServerSideProps); diff --git a/apps/web/playwright/login.e2e.ts b/apps/web/playwright/login.e2e.ts index 94569128f8..95eddda9e3 100644 --- a/apps/web/playwright/login.e2e.ts +++ b/apps/web/playwright/login.e2e.ts @@ -2,13 +2,14 @@ import { expect } from "@playwright/test"; import { login } from "./fixtures/users"; import { test } from "./lib/fixtures"; +import { testBothFutureAndLegacyRoutes } from "./lib/future-legacy-routes"; import { localize } from "./lib/testUtils"; test.describe.configure({ mode: "parallel" }); // a test to logout requires both a succesfull login as logout, to prevent // a doubling of tests failing on logout & logout, we can group them. -test.describe("user can login & logout succesfully", async () => { +testBothFutureAndLegacyRoutes.describe("user can login & logout succesfully", async () => { test.afterAll(async ({ users }) => { await users.deleteAll(); }); @@ -43,7 +44,7 @@ test.describe("user can login & logout succesfully", async () => { }); }); -test.describe("Login and logout tests", () => { +testBothFutureAndLegacyRoutes.describe("Login and logout tests", () => { test.afterAll(async ({ users }) => { await users.deleteAll(); }); @@ -56,7 +57,7 @@ test.describe("Login and logout tests", () => { await expect(page.locator(`[data-testid=login-form]`)).toBeVisible(); }); - test.describe("Login flow validations", async () => { + testBothFutureAndLegacyRoutes.describe("Login flow validations", async () => { test("Should warn when user does not exist", async ({ page }) => { const alertMessage = (await localize("en"))("incorrect_email_password"); diff --git a/apps/web/server/lib/auth/login/getServerSideProps.tsx b/apps/web/server/lib/auth/login/getServerSideProps.tsx new file mode 100644 index 0000000000..64144dec37 --- /dev/null +++ b/apps/web/server/lib/auth/login/getServerSideProps.tsx @@ -0,0 +1,102 @@ +import { jwtVerify } from "jose"; +import type { GetServerSidePropsContext } from "next"; +import { getCsrfToken } from "next-auth/react"; + +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml"; +import { WEBSITE_URL } from "@calcom/lib/constants"; +import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; +import prisma from "@calcom/prisma"; + +import { IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants"; +import { ssrInit } from "@server/lib/ssr"; + +export async function getServerSideProps(context: GetServerSidePropsContext) { + const { req, res, query } = context; + + const session = await getServerSession({ req, res }); + const ssr = await ssrInit(context); + + const verifyJwt = (jwt: string) => { + const secret = new TextEncoder().encode(process.env.CALENDSO_ENCRYPTION_KEY); + + return jwtVerify(jwt, secret, { + issuer: WEBSITE_URL, + audience: `${WEBSITE_URL}/auth/login`, + algorithms: ["HS256"], + }); + }; + + let totpEmail = null; + if (context.query.totp) { + try { + const decryptedJwt = await verifyJwt(context.query.totp as string); + if (decryptedJwt.payload) { + totpEmail = decryptedJwt.payload.email as string; + } else { + return { + redirect: { + destination: "/auth/error?error=JWT%20Invalid%20Payload", + permanent: false, + }, + }; + } + } catch (e) { + return { + redirect: { + destination: "/auth/error?error=Invalid%20JWT%3A%20Please%20try%20again", + permanent: false, + }, + }; + } + } + + if (session) { + const { callbackUrl } = query; + + if (callbackUrl) { + try { + const destination = getSafeRedirectUrl(callbackUrl as string); + if (destination) { + return { + redirect: { + destination, + permanent: false, + }, + }; + } + } catch (e) { + console.warn(e); + } + } + + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + + const userCount = await prisma.user.count(); + if (userCount === 0) { + // Proceed to new onboarding to create first admin user + return { + redirect: { + destination: "/auth/setup", + permanent: false, + }, + }; + } + return { + props: { + csrfToken: await getCsrfToken(context), + trpcState: ssr.dehydrate(), + isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED, + isSAMLLoginEnabled, + samlTenantID, + samlProductID, + totpEmail, + }, + }; +}