chore: [app-router-migration 30] migrate the "login" page (#13195)
Co-authored-by: Dmytro Hryshyn <dev.dmytroh@gmail.com> Co-authored-by: Benny Joo <sldisek783@gmail.com> Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
4 files changed
+134
-105
No files matched your search
@@ -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<InferGetServerSidePropsType<typeof getServerSideProps>>(getServerSideProps),
|
||||
})<"P">;
|
||||
@@ -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<typeof _getServerSideProps> & WithNonceProps<{}>) {
|
||||
inferSSRProps<typeof getServerSideProps> & WithNonceProps<{}>) {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
@@ -269,97 +264,6 @@ inferSSRProps<typeof _getServerSideProps> & 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);
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user