2023-05-23 10:09:32 +00:00
|
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
2022-02-04 20:30:36 +00:00
|
|
|
import classNames from "classnames";
|
2023-03-05 02:09:45 +00:00
|
|
|
import { jwtVerify } from "jose";
|
2023-02-16 22:39:57 +00:00
|
|
|
import type { GetServerSidePropsContext } from "next";
|
2022-01-07 20:23:37 +00:00
|
|
|
import { getCsrfToken, signIn } from "next-auth/react";
|
2021-09-22 19:52:38 +00:00
|
|
|
import Link from "next/link";
|
2023-08-02 09:35:48 +00:00
|
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
2023-04-05 18:14:46 +00:00
|
|
|
import type { CSSProperties } from "react";
|
2021-09-27 14:47:55 +00:00
|
|
|
import { useState } from "react";
|
2022-11-23 02:55:25 +00:00
|
|
|
import { FormProvider, useForm } from "react-hook-form";
|
2022-10-17 12:44:43 +00:00
|
|
|
import { FaGoogle } from "react-icons/fa";
|
2023-05-23 10:09:32 +00:00
|
|
|
import { z } from "zod";
|
2021-09-22 19:52:38 +00:00
|
|
|
|
2022-12-22 19:06:26 +00:00
|
|
|
import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
|
2023-03-10 23:45:24 +00:00
|
|
|
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
|
|
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
2022-10-19 15:50:25 +00:00
|
|
|
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
|
2023-01-26 22:51:03 +00:00
|
|
|
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
2022-04-27 14:28:36 +00:00
|
|
|
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
2022-07-27 23:28:21 +00:00
|
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
2022-07-28 19:58:26 +00:00
|
|
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
2022-08-22 19:34:28 +00:00
|
|
|
import prisma from "@calcom/prisma";
|
2023-01-23 23:08:01 +00:00
|
|
|
import { Alert, Button, EmailField, PasswordField } from "@calcom/ui";
|
2023-08-30 07:33:48 +00:00
|
|
|
import { ArrowLeft, Lock } from "@calcom/ui/components/icon";
|
2022-03-16 23:36:43 +00:00
|
|
|
|
2023-02-16 22:39:57 +00:00
|
|
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
|
|
|
import type { WithNonceProps } from "@lib/withNonce";
|
|
|
|
import withNonce from "@lib/withNonce";
|
2021-09-22 19:52:38 +00:00
|
|
|
|
2021-10-06 14:05:01 +00:00
|
|
|
import AddToHomescreen from "@components/AddToHomescreen";
|
2023-04-18 18:45:32 +00:00
|
|
|
import PageWrapper from "@components/PageWrapper";
|
2023-08-30 07:33:48 +00:00
|
|
|
import BackupCode from "@components/auth/BackupCode";
|
2022-02-04 20:30:36 +00:00
|
|
|
import TwoFactor from "@components/auth/TwoFactor";
|
2022-11-01 13:29:01 +00:00
|
|
|
import AuthContainer from "@components/ui/AuthContainer";
|
2021-09-21 09:29:20 +00:00
|
|
|
|
2022-01-13 20:05:23 +00:00
|
|
|
import { IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
|
2021-11-16 17:12:08 +00:00
|
|
|
import { ssrInit } from "@server/lib/ssr";
|
|
|
|
|
2022-02-04 20:30:36 +00:00
|
|
|
interface LoginValues {
|
|
|
|
email: string;
|
|
|
|
password: string;
|
|
|
|
totpCode: string;
|
2023-08-30 07:33:48 +00:00
|
|
|
backupCode: string;
|
2022-02-04 20:30:36 +00:00
|
|
|
csrfToken: string;
|
|
|
|
}
|
2022-01-13 20:05:23 +00:00
|
|
|
export default function Login({
|
|
|
|
csrfToken,
|
|
|
|
isGoogleLoginEnabled,
|
|
|
|
isSAMLLoginEnabled,
|
|
|
|
samlTenantID,
|
|
|
|
samlProductID,
|
2023-03-05 02:09:45 +00:00
|
|
|
totpEmail,
|
2023-02-06 22:50:08 +00:00
|
|
|
}: inferSSRProps<typeof _getServerSideProps> & WithNonceProps) {
|
2023-08-02 09:35:48 +00:00
|
|
|
const searchParams = useSearchParams();
|
2023-09-04 19:43:37 +00:00
|
|
|
const isTeamInvite = searchParams.get("teamInvite");
|
|
|
|
|
2021-10-14 14:24:21 +00:00
|
|
|
const { t } = useLocale();
|
2021-09-12 08:51:59 +00:00
|
|
|
const router = useRouter();
|
2023-05-26 10:02:02 +00:00
|
|
|
const formSchema = z
|
|
|
|
.object({
|
|
|
|
email: z
|
|
|
|
.string()
|
|
|
|
.min(1, `${t("error_required_field")}`)
|
|
|
|
.email(`${t("enter_valid_email")}`),
|
2023-07-21 08:32:03 +00:00
|
|
|
password: !!totpEmail ? z.literal("") : z.string().min(1, `${t("error_required_field")}`),
|
2023-05-26 10:02:02 +00:00
|
|
|
})
|
|
|
|
// Passthrough other fields like totpCode
|
|
|
|
.passthrough();
|
2023-05-23 10:09:32 +00:00
|
|
|
const methods = useForm<LoginValues>({ resolver: zodResolver(formSchema) });
|
2022-10-19 15:50:25 +00:00
|
|
|
const { register, formState } = methods;
|
2023-03-05 02:09:45 +00:00
|
|
|
const [twoFactorRequired, setTwoFactorRequired] = useState(!!totpEmail || false);
|
2023-08-30 07:33:48 +00:00
|
|
|
const [twoFactorLostAccess, setTwoFactorLostAccess] = useState(false);
|
2021-09-21 09:29:20 +00:00
|
|
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
2022-02-04 20:30:36 +00:00
|
|
|
|
2021-10-14 14:24:21 +00:00
|
|
|
const errorMessages: { [key: string]: string } = {
|
2022-10-17 12:44:43 +00:00
|
|
|
// [ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"),
|
2023-01-30 18:37:03 +00:00
|
|
|
// Don't leak information about whether an email is registered or not
|
2023-07-13 12:09:19 +00:00
|
|
|
[ErrorCode.IncorrectEmailPassword]: t("incorrect_email_password"),
|
2021-10-14 14:24:21 +00:00
|
|
|
[ErrorCode.IncorrectTwoFactorCode]: `${t("incorrect_2fa_code")} ${t("please_try_again")}`,
|
|
|
|
[ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`,
|
2022-01-13 20:05:23 +00:00
|
|
|
[ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
|
2021-10-14 14:24:21 +00:00
|
|
|
};
|
2021-09-21 09:29:20 +00:00
|
|
|
|
2022-02-02 18:33:27 +00:00
|
|
|
const telemetry = useTelemetry();
|
|
|
|
|
2023-08-02 09:35:48 +00:00
|
|
|
let callbackUrl = searchParams.get("callbackUrl") || "";
|
2022-03-09 14:47:02 +00:00
|
|
|
|
2022-03-18 17:56:56 +00:00
|
|
|
if (/"\//.test(callbackUrl)) callbackUrl = callbackUrl.substring(1);
|
2022-04-27 14:28:36 +00:00
|
|
|
|
|
|
|
// If not absolute URL, make it absolute
|
2022-03-09 14:47:02 +00:00
|
|
|
if (!/^https?:\/\//.test(callbackUrl)) {
|
2022-03-26 00:39:38 +00:00
|
|
|
callbackUrl = `${WEBAPP_URL}/${callbackUrl}`;
|
2022-03-09 14:47:02 +00:00
|
|
|
}
|
2021-09-12 08:51:59 +00:00
|
|
|
|
2022-06-30 07:01:07 +00:00
|
|
|
const safeCallbackUrl = getSafeRedirectUrl(callbackUrl);
|
|
|
|
|
|
|
|
callbackUrl = safeCallbackUrl || "";
|
2022-04-27 14:28:36 +00:00
|
|
|
|
2022-02-04 20:30:36 +00:00
|
|
|
const LoginFooter = (
|
2023-09-04 19:43:37 +00:00
|
|
|
<a
|
|
|
|
href={callbackUrl !== "" ? `${WEBSITE_URL}/signup?callbackUrl=${callbackUrl}` : `${WEBSITE_URL}/signup`}
|
|
|
|
className="text-brand-500 font-medium">
|
2022-10-17 12:44:43 +00:00
|
|
|
{t("dont_have_an_account")}
|
|
|
|
</a>
|
2022-02-04 20:30:36 +00:00
|
|
|
);
|
2021-09-21 09:29:20 +00:00
|
|
|
|
2022-02-04 20:30:36 +00:00
|
|
|
const TwoFactorFooter = (
|
2023-08-30 07:33:48 +00:00
|
|
|
<>
|
|
|
|
<Button
|
|
|
|
onClick={() => {
|
|
|
|
if (twoFactorLostAccess) {
|
|
|
|
setTwoFactorLostAccess(false);
|
|
|
|
methods.setValue("backupCode", "");
|
|
|
|
} else {
|
|
|
|
setTwoFactorRequired(false);
|
|
|
|
methods.setValue("totpCode", "");
|
|
|
|
}
|
|
|
|
setErrorMessage(null);
|
|
|
|
}}
|
|
|
|
StartIcon={ArrowLeft}
|
|
|
|
color="minimal">
|
|
|
|
{t("go_back")}
|
|
|
|
</Button>
|
|
|
|
{!twoFactorLostAccess ? (
|
|
|
|
<Button
|
|
|
|
onClick={() => {
|
|
|
|
setTwoFactorLostAccess(true);
|
|
|
|
setErrorMessage(null);
|
|
|
|
methods.setValue("totpCode", "");
|
|
|
|
}}
|
|
|
|
StartIcon={Lock}
|
|
|
|
color="minimal">
|
|
|
|
{t("lost_access")}
|
|
|
|
</Button>
|
|
|
|
) : null}
|
|
|
|
</>
|
2022-02-04 20:30:36 +00:00
|
|
|
);
|
2022-01-13 20:05:23 +00:00
|
|
|
|
2023-03-05 02:09:45 +00:00
|
|
|
const ExternalTotpFooter = (
|
|
|
|
<Button
|
|
|
|
onClick={() => {
|
|
|
|
window.location.replace("/");
|
|
|
|
}}
|
|
|
|
color="minimal">
|
|
|
|
{t("cancel")}
|
|
|
|
</Button>
|
|
|
|
);
|
|
|
|
|
2022-10-19 15:50:25 +00:00
|
|
|
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);
|
2023-08-30 07:33:48 +00:00
|
|
|
else if (res.error === ErrorCode.IncorrectBackupCode) setErrorMessage(t("incorrect_backup_code"));
|
|
|
|
else if (res.error === ErrorCode.MissingBackupCodes) setErrorMessage(t("missing_backup_codes"));
|
2022-10-19 15:50:25 +00:00
|
|
|
// fallback if error not found
|
|
|
|
else setErrorMessage(errorMessages[res.error] || t("something_went_wrong"));
|
|
|
|
};
|
|
|
|
|
2021-03-29 21:01:12 +00:00
|
|
|
return (
|
2023-04-05 18:14:46 +00:00
|
|
|
<div
|
|
|
|
style={
|
|
|
|
{
|
|
|
|
"--cal-brand": "#111827",
|
|
|
|
"--cal-brand-emphasis": "#101010",
|
|
|
|
"--cal-brand-text": "white",
|
2023-04-08 16:50:17 +00:00
|
|
|
"--cal-brand-subtle": "#9CA3AF",
|
2023-04-05 18:14:46 +00:00
|
|
|
} as CSSProperties
|
|
|
|
}>
|
2022-01-27 10:16:20 +00:00
|
|
|
<AuthContainer
|
|
|
|
title={t("login")}
|
|
|
|
description={t("login")}
|
|
|
|
showLogo
|
2022-10-17 12:44:43 +00:00
|
|
|
heading={twoFactorRequired ? t("2fa_code") : t("welcome_back")}
|
2023-01-14 00:42:13 +00:00
|
|
|
footerText={
|
|
|
|
twoFactorRequired
|
2023-03-05 02:09:45 +00:00
|
|
|
? !totpEmail
|
|
|
|
? TwoFactorFooter
|
|
|
|
: ExternalTotpFooter
|
2023-01-14 00:42:13 +00:00
|
|
|
: process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== "true"
|
|
|
|
? LoginFooter
|
|
|
|
: null
|
|
|
|
}>
|
2023-09-04 19:43:37 +00:00
|
|
|
{isTeamInvite && (
|
|
|
|
<Alert severity="info" message={t("signin_or_signup_to_accept_invite")} className="mb-4 mt-4" />
|
|
|
|
)}
|
2022-10-19 15:50:25 +00:00
|
|
|
<FormProvider {...methods}>
|
2023-05-23 10:09:32 +00:00
|
|
|
<form onSubmit={methods.handleSubmit(onSubmit)} noValidate data-testid="login-form">
|
2022-10-19 15:50:25 +00:00
|
|
|
<div>
|
|
|
|
<input defaultValue={csrfToken || undefined} type="hidden" hidden {...register("csrfToken")} />
|
|
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
|
|
<div className={classNames("space-y-6", { hidden: twoFactorRequired })}>
|
|
|
|
<EmailField
|
|
|
|
id="email"
|
|
|
|
label={t("email_address")}
|
2023-08-02 09:35:48 +00:00
|
|
|
defaultValue={totpEmail || (searchParams?.get("email") as string)}
|
2022-10-19 15:50:25 +00:00
|
|
|
placeholder="john.doe@example.com"
|
2022-10-17 12:44:43 +00:00
|
|
|
required
|
2022-10-19 15:50:25 +00:00
|
|
|
{...register("email")}
|
2022-10-17 12:44:43 +00:00
|
|
|
/>
|
2022-10-19 15:50:25 +00:00
|
|
|
<div className="relative">
|
|
|
|
<PasswordField
|
|
|
|
id="password"
|
2023-01-30 09:42:52 +00:00
|
|
|
autoComplete="off"
|
2023-03-05 02:09:45 +00:00
|
|
|
required={!totpEmail}
|
2022-10-19 15:50:25 +00:00
|
|
|
className="mb-0"
|
|
|
|
{...register("password")}
|
|
|
|
/>
|
2023-05-22 23:30:54 +00:00
|
|
|
<div className="absolute -top-[2px] ltr:right-0 rtl:left-0">
|
2023-04-20 22:06:35 +00:00
|
|
|
<Link
|
|
|
|
href="/auth/forgot-password"
|
|
|
|
tabIndex={-1}
|
|
|
|
className="text-default text-sm font-medium">
|
|
|
|
{t("forgot")}
|
|
|
|
</Link>
|
|
|
|
</div>
|
2022-10-19 15:50:25 +00:00
|
|
|
</div>
|
2022-10-17 12:44:43 +00:00
|
|
|
</div>
|
2022-01-27 10:16:20 +00:00
|
|
|
|
2023-08-30 07:33:48 +00:00
|
|
|
{twoFactorRequired ? !twoFactorLostAccess ? <TwoFactor center /> : <BackupCode center /> : null}
|
2022-10-19 15:50:25 +00:00
|
|
|
|
|
|
|
{errorMessage && <Alert severity="error" title={errorMessage} />}
|
|
|
|
<Button
|
|
|
|
type="submit"
|
|
|
|
color="primary"
|
|
|
|
disabled={formState.isSubmitting}
|
|
|
|
className="w-full justify-center">
|
|
|
|
{twoFactorRequired ? t("submit") : t("sign_in")}
|
|
|
|
</Button>
|
2022-10-17 12:44:43 +00:00
|
|
|
</div>
|
2022-10-19 15:50:25 +00:00
|
|
|
</form>
|
|
|
|
{!twoFactorRequired && (
|
|
|
|
<>
|
2023-04-05 18:14:46 +00:00
|
|
|
{(isGoogleLoginEnabled || isSAMLLoginEnabled) && <hr className="border-subtle my-8" />}
|
2022-10-19 15:50:25 +00:00
|
|
|
<div className="space-y-3">
|
|
|
|
{isGoogleLoginEnabled && (
|
|
|
|
<Button
|
|
|
|
color="secondary"
|
|
|
|
className="w-full justify-center"
|
|
|
|
data-testid="google"
|
|
|
|
StartIcon={FaGoogle}
|
|
|
|
onClick={async (e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
await signIn("google");
|
|
|
|
}}>
|
|
|
|
{t("signin_with_google")}
|
|
|
|
</Button>
|
|
|
|
)}
|
|
|
|
{isSAMLLoginEnabled && (
|
|
|
|
<SAMLLogin
|
|
|
|
samlTenantID={samlTenantID}
|
|
|
|
samlProductID={samlProductID}
|
|
|
|
setErrorMessage={setErrorMessage}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</FormProvider>
|
2022-01-27 10:16:20 +00:00
|
|
|
</AuthContainer>
|
2021-10-06 14:05:01 +00:00
|
|
|
<AddToHomescreen />
|
2023-04-05 18:14:46 +00:00
|
|
|
</div>
|
2021-06-24 15:59:11 +00:00
|
|
|
);
|
2021-03-29 21:01:12 +00:00
|
|
|
}
|
|
|
|
|
2023-02-06 22:50:08 +00:00
|
|
|
// TODO: Once we understand how to retrieve prop types automatically from getServerSideProps, remove this temporary variable
|
|
|
|
const _getServerSideProps = async function getServerSideProps(context: GetServerSidePropsContext) {
|
2023-03-10 23:45:24 +00:00
|
|
|
const { req, res } = context;
|
|
|
|
|
|
|
|
const session = await getServerSession({ req, res });
|
2021-11-16 17:12:08 +00:00
|
|
|
const ssr = await ssrInit(context);
|
2021-08-09 10:35:06 +00:00
|
|
|
|
2023-03-05 02:09:45 +00:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-09 10:35:06 +00:00
|
|
|
if (session) {
|
2021-11-16 17:12:08 +00:00
|
|
|
return {
|
|
|
|
redirect: {
|
|
|
|
destination: "/",
|
|
|
|
permanent: false,
|
|
|
|
},
|
|
|
|
};
|
2021-08-09 10:35:06 +00:00
|
|
|
}
|
|
|
|
|
2022-07-27 23:28:21 +00:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2021-03-29 21:01:12 +00:00
|
|
|
return {
|
2021-11-16 17:12:08 +00:00
|
|
|
props: {
|
|
|
|
csrfToken: await getCsrfToken(context),
|
|
|
|
trpcState: ssr.dehydrate(),
|
2022-01-13 20:05:23 +00:00
|
|
|
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
|
|
|
|
isSAMLLoginEnabled,
|
|
|
|
samlTenantID,
|
|
|
|
samlProductID,
|
2023-03-05 02:09:45 +00:00
|
|
|
totpEmail,
|
2021-11-16 17:12:08 +00:00
|
|
|
},
|
2021-06-24 15:59:11 +00:00
|
|
|
};
|
2023-02-06 22:50:08 +00:00
|
|
|
};
|
|
|
|
|
2023-04-05 18:14:46 +00:00
|
|
|
Login.isThemeSupported = false;
|
2023-04-18 18:45:32 +00:00
|
|
|
Login.PageWrapper = PageWrapper;
|
2023-04-05 18:14:46 +00:00
|
|
|
|
2023-02-06 22:50:08 +00:00
|
|
|
export const getServerSideProps = withNonce(_getServerSideProps);
|