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 Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import type { CSSProperties } from "react"; import { useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { FaGoogle } from "react-icons/fa"; 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 { 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"; 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"; interface LoginValues { email: string; password: string; totpCode: string; backupCode: string; csrfToken: string; } export default function Login({ csrfToken, isGoogleLoginEnabled, isSAMLLoginEnabled, samlTenantID, samlProductID, totpEmail, }: // eslint-disable-next-line @typescript-eslint/ban-types inferSSRProps & WithNonceProps<{}>) { const searchParams = useSearchParams(); const { t } = useLocale(); const router = useRouter(); const formSchema = z .object({ email: z .string() .min(1, `${t("error_required_field")}`) .email(`${t("enter_valid_email")}`), password: !!totpEmail ? z.literal("") : z.string().min(1, `${t("error_required_field")}`), }) // Passthrough other fields like totpCode .passthrough(); const methods = useForm({ resolver: zodResolver(formSchema) }); const { register, formState } = methods; const [twoFactorRequired, setTwoFactorRequired] = useState(!!totpEmail || false); const [twoFactorLostAccess, setTwoFactorLostAccess] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const errorMessages: { [key: string]: string } = { // [ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"), // Don't leak information about whether an email is registered or not [ErrorCode.IncorrectEmailPassword]: t("incorrect_email_password"), [ErrorCode.IncorrectTwoFactorCode]: `${t("incorrect_2fa_code")} ${t("please_try_again")}`, [ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`, [ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"), }; const telemetry = useTelemetry(); let callbackUrl = searchParams.get("callbackUrl") || ""; if (/"\//.test(callbackUrl)) callbackUrl = callbackUrl.substring(1); // If not absolute URL, make it absolute if (!/^https?:\/\//.test(callbackUrl)) { callbackUrl = `${WEBAPP_URL}/${callbackUrl}`; } const safeCallbackUrl = getSafeRedirectUrl(callbackUrl); callbackUrl = safeCallbackUrl || ""; const LoginFooter = ( {t("dont_have_an_account")} ); const TwoFactorFooter = ( <> {!twoFactorLostAccess ? ( ) : null} ); const ExternalTotpFooter = ( ); const onSubmit = async (values: LoginValues) => { setErrorMessage(null); telemetry.event(telemetryEventTypes.login, collectPageParameters()); const res = await signIn<"credentials">("credentials", { ...values, callbackUrl, redirect: false, }); if (!res) setErrorMessage(errorMessages[ErrorCode.InternalServerError]); // we're logged in! let's do a hard refresh to the desired url else if (!res.error) router.push(callbackUrl); else if (res.error === ErrorCode.SecondFactorRequired) setTwoFactorRequired(true); else if (res.error === ErrorCode.IncorrectBackupCode) setErrorMessage(t("incorrect_backup_code")); else if (res.error === ErrorCode.MissingBackupCodes) setErrorMessage(t("missing_backup_codes")); // fallback if error not found else setErrorMessage(errorMessages[res.error] || t("something_went_wrong")); }; const { data, isLoading } = trpc.viewer.public.ssoConnections.useQuery(undefined, { onError: (err) => { setErrorMessage(err.message); }, }); const displaySSOLogin = HOSTED_CAL_FEATURES ? true : isSAMLLoginEnabled && !isLoading && data?.connectionExists; return (
{t("forgot")}
{twoFactorRequired ? !twoFactorLostAccess ? : : null} {errorMessage && }
{!twoFactorRequired && ( <> {(isGoogleLoginEnabled || displaySSOLogin) &&
}
{isGoogleLoginEnabled && ( )} {displaySSOLogin && ( )}
)}
); } // 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 } = 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) { 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, }, }; }; Login.PageWrapper = PageWrapper; export const getServerSideProps = withNonce(_getServerSideProps);