Better 2FA Interface (#1707)
* - added TwoFactor component - added react-digit-input package - added SAMLLogin component - upgraded auth/logic to react-hook-form - fixed EmailField to match other ___Field components to include Label - cleaned up login logic * upgraded error component Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
parent
ae5d5e1261
commit
d0a6d6a6e6
7 changed files with 258 additions and 199 deletions
64
components/auth/SAMLLogin.tsx
Normal file
64
components/auth/SAMLLogin.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { signIn } from "next-auth/react";
|
||||||
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
||||||
|
import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
|
import Button from "@components/ui/Button";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
email: string;
|
||||||
|
samlTenantID: string;
|
||||||
|
samlProductID: string;
|
||||||
|
hostedCal: boolean;
|
||||||
|
setErrorMessage: Dispatch<SetStateAction<string | null>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SAMLLogin(props: Props) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const methods = useFormContext();
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
|
const mutation = trpc.useMutation("viewer.samlTenantProduct", {
|
||||||
|
onSuccess: async (data) => {
|
||||||
|
await signIn("saml", {}, { tenant: data.tenant, product: data.product });
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
props.setErrorMessage(err.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-5">
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
data-testid={"saml"}
|
||||||
|
className="flex justify-center w-full"
|
||||||
|
onClick={async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// track Google logins. Without personal data/payload
|
||||||
|
telemetry.withJitsu((jitsu) =>
|
||||||
|
jitsu.track(telemetryEventTypes.googleLogin, collectPageParameters())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!props.hostedCal) {
|
||||||
|
await signIn("saml", {}, { tenant: props.samlTenantID, product: props.samlProductID });
|
||||||
|
} else {
|
||||||
|
if (props.email.length === 0) {
|
||||||
|
props.setErrorMessage(t("saml_email_required"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// hosted solution, fetch tenant and product from the backend
|
||||||
|
mutation.mutate({
|
||||||
|
email: methods.getValues("email"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{t("signin_with_saml")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
42
components/auth/TwoFactor.tsx
Normal file
42
components/auth/TwoFactor.tsx
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import useDigitInput from "react-digit-input";
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|
||||||
|
import { Input } from "@components/form/fields";
|
||||||
|
|
||||||
|
export default function TwoFactor() {
|
||||||
|
const [value, onChange] = useState("");
|
||||||
|
const { t } = useLocale();
|
||||||
|
const methods = useFormContext();
|
||||||
|
|
||||||
|
const digits = useDigitInput({
|
||||||
|
acceptedCharacters: /^[0-9]$/,
|
||||||
|
length: 6,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value) methods.setValue("totpCode", value);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const className = "h-12 w-12 !text-xl text-center";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-sm mx-auto !mt-0">
|
||||||
|
<p className="mb-4 text-sm text-gray-500">{t("2fa_enabled_instructions")}</p>
|
||||||
|
<input hidden type="hidden" value={value} {...methods.register("totpCode")} />
|
||||||
|
<div className="flex flex-row space-x-1">
|
||||||
|
<Input className={className} name="2fa1" inputMode="decimal" {...digits[0]} autoFocus />
|
||||||
|
<Input className={className} name="2fa2" inputMode="decimal" {...digits[1]} />
|
||||||
|
<Input className={className} name="2fa3" inputMode="decimal" {...digits[2]} />
|
||||||
|
<Input className={className} name="2fa4" inputMode="decimal" {...digits[3]} />
|
||||||
|
<Input className={className} name="2fa5" inputMode="decimal" {...digits[4]} />
|
||||||
|
<Input className={className} name="2fa6" inputMode="decimal" {...digits[5]} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -115,7 +115,17 @@ export const EmailInput = forwardRef<HTMLInputElement, InputFieldProps>(function
|
||||||
});
|
});
|
||||||
|
|
||||||
export const EmailField = forwardRef<HTMLInputElement, InputFieldProps>(function EmailField(props, ref) {
|
export const EmailField = forwardRef<HTMLInputElement, InputFieldProps>(function EmailField(props, ref) {
|
||||||
return <EmailInput ref={ref} {...props} />;
|
return (
|
||||||
|
<InputField
|
||||||
|
ref={ref}
|
||||||
|
type="email"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="email"
|
||||||
|
autoCorrect="off"
|
||||||
|
inputMode="email"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
type TextAreaProps = Omit<JSX.IntrinsicElements["textarea"], "name"> & { name: string };
|
type TextAreaProps = Omit<JSX.IntrinsicElements["textarea"], "name"> & { name: string };
|
||||||
|
|
|
@ -86,6 +86,7 @@
|
||||||
"qrcode": "^1.5.0",
|
"qrcode": "^1.5.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-date-picker": "^8.3.6",
|
"react-date-picker": "^8.3.6",
|
||||||
|
"react-digit-input": "^2.1.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-easy-crop": "^3.5.2",
|
"react-easy-crop": "^3.5.2",
|
||||||
"react-hook-form": "^7.20.4",
|
"react-hook-form": "^7.20.4",
|
||||||
|
|
|
@ -5,7 +5,8 @@ import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|
||||||
import { HeadSeo } from "@components/seo/head-seo";
|
import AuthContainer from "@components/ui/AuthContainer";
|
||||||
|
import Button from "@components/ui/Button";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
|
@ -15,17 +16,7 @@ export default function Error() {
|
||||||
const { error } = router.query;
|
const { error } = router.query;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<AuthContainer title="" description="">
|
||||||
className="fixed z-50 inset-0 overflow-y-auto"
|
|
||||||
aria-labelledby="modal-title"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true">
|
|
||||||
<HeadSeo title={t("error")} description={t("error")} />
|
|
||||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
||||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
|
||||||
​
|
|
||||||
</span>
|
|
||||||
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
|
|
||||||
<div>
|
<div>
|
||||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
|
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
|
||||||
<XIcon className="h-6 w-6 text-red-600" />
|
<XIcon className="h-6 w-6 text-red-600" />
|
||||||
|
@ -41,14 +32,10 @@ export default function Error() {
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 sm:mt-6">
|
<div className="mt-5 sm:mt-6">
|
||||||
<Link href="/auth/login">
|
<Link href="/auth/login">
|
||||||
<a className="inline-flex justify-center w-full rounded-sm border border-transparent shadow-sm px-4 py-2 bg-neutral-900 text-base font-medium text-white hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500 sm:text-sm">
|
<Button className="w-full flex justify-center">{t("go_back_login")}</Button>
|
||||||
{t("go_back_login")}
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AuthContainer>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,37 @@
|
||||||
|
import { ArrowLeftIcon } from "@heroicons/react/solid";
|
||||||
|
import classNames from "classnames";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { getCsrfToken, signIn } from "next-auth/react";
|
import { getCsrfToken, signIn } from "next-auth/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { ErrorCode, getSession } from "@lib/auth";
|
import { ErrorCode, getSession } from "@lib/auth";
|
||||||
import { WEBSITE_URL } from "@lib/config/constants";
|
import { WEBSITE_URL } from "@lib/config/constants";
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import { isSAMLLoginEnabled, hostedCal, samlTenantID, samlProductID } from "@lib/saml";
|
import { isSAMLLoginEnabled, hostedCal, samlTenantID, samlProductID } from "@lib/saml";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
||||||
import { trpc } from "@lib/trpc";
|
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import AddToHomescreen from "@components/AddToHomescreen";
|
import AddToHomescreen from "@components/AddToHomescreen";
|
||||||
import { EmailField, PasswordField, TextField } from "@components/form/fields";
|
import SAMLLogin from "@components/auth/SAMLLogin";
|
||||||
|
import TwoFactor from "@components/auth/TwoFactor";
|
||||||
|
import { EmailField, PasswordField, Form } from "@components/form/fields";
|
||||||
|
import { Alert } from "@components/ui/Alert";
|
||||||
import AuthContainer from "@components/ui/AuthContainer";
|
import AuthContainer from "@components/ui/AuthContainer";
|
||||||
import Button from "@components/ui/Button";
|
import Button from "@components/ui/Button";
|
||||||
|
|
||||||
import { IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
|
import { IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
|
interface LoginValues {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
totpCode: string;
|
||||||
|
csrfToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Login({
|
export default function Login({
|
||||||
csrfToken,
|
csrfToken,
|
||||||
isGoogleLoginEnabled,
|
isGoogleLoginEnabled,
|
||||||
|
@ -30,14 +42,13 @@ export default function Login({
|
||||||
}: inferSSRProps<typeof getServerSideProps>) {
|
}: inferSSRProps<typeof getServerSideProps>) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [email, setEmail] = useState("");
|
const form = useForm<LoginValues>();
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [code, setCode] = useState("");
|
const [twoFactorRequired, setTwoFactorRequired] = useState(false);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const [secondFactorRequired, setSecondFactorRequired] = useState(false);
|
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
const errorMessages: { [key: string]: string } = {
|
const errorMessages: { [key: string]: string } = {
|
||||||
[ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"),
|
// [ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"),
|
||||||
[ErrorCode.IncorrectPassword]: `${t("incorrect_password")} ${t("please_try_again")}`,
|
[ErrorCode.IncorrectPassword]: `${t("incorrect_password")} ${t("please_try_again")}`,
|
||||||
[ErrorCode.UserNotFound]: t("no_account_exists"),
|
[ErrorCode.UserNotFound]: t("no_account_exists"),
|
||||||
[ErrorCode.IncorrectTwoFactorCode]: `${t("incorrect_2fa_code")} ${t("please_try_again")}`,
|
[ErrorCode.IncorrectTwoFactorCode]: `${t("incorrect_2fa_code")} ${t("please_try_again")}`,
|
||||||
|
@ -49,90 +60,62 @@ export default function Login({
|
||||||
|
|
||||||
const callbackUrl = typeof router.query?.callbackUrl === "string" ? router.query.callbackUrl : "/";
|
const callbackUrl = typeof router.query?.callbackUrl === "string" ? router.query.callbackUrl : "/";
|
||||||
|
|
||||||
async function handleSubmit(e: React.SyntheticEvent) {
|
const LoginFooter = (
|
||||||
e.preventDefault();
|
<span>
|
||||||
|
{t("dont_have_an_account")}{" "}
|
||||||
|
<a href={`${WEBSITE_URL}/signup`} className="font-medium text-neutral-900">
|
||||||
|
{t("create_an_account")}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
if (isSubmitting) {
|
const TwoFactorFooter = (
|
||||||
return;
|
<Button
|
||||||
}
|
onClick={() => {
|
||||||
|
setTwoFactorRequired(false);
|
||||||
setIsSubmitting(true);
|
form.setValue("totpCode", "");
|
||||||
setErrorMessage(null);
|
}}
|
||||||
|
StartIcon={ArrowLeftIcon}
|
||||||
try {
|
color="minimal">
|
||||||
const response = await signIn<"credentials">("credentials", {
|
{t("go_back")}
|
||||||
redirect: false,
|
</Button>
|
||||||
email,
|
);
|
||||||
password,
|
|
||||||
totpCode: code,
|
|
||||||
callbackUrl,
|
|
||||||
});
|
|
||||||
if (!response) {
|
|
||||||
throw new Error("Received empty response from next auth");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.error) {
|
|
||||||
// we're logged in! let's do a hard refresh to the desired url
|
|
||||||
window.location.replace(callbackUrl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.error === ErrorCode.SecondFactorRequired) {
|
|
||||||
setSecondFactorRequired(true);
|
|
||||||
setErrorMessage(errorMessages[ErrorCode.SecondFactorRequired]);
|
|
||||||
} else {
|
|
||||||
setErrorMessage(errorMessages[response.error] || t("something_went_wrong"));
|
|
||||||
}
|
|
||||||
setIsSubmitting(false);
|
|
||||||
} catch (e) {
|
|
||||||
setErrorMessage(t("something_went_wrong"));
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mutation = trpc.useMutation("viewer.samlTenantProduct", {
|
|
||||||
onSuccess: (data) => {
|
|
||||||
signIn("saml", {}, { tenant: data.tenant, product: data.product });
|
|
||||||
},
|
|
||||||
onError: (err) => {
|
|
||||||
setErrorMessage(err.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AuthContainer
|
<AuthContainer
|
||||||
title={t("login")}
|
title={t("login")}
|
||||||
description={t("login")}
|
description={t("login")}
|
||||||
loading={isSubmitting}
|
loading={form.formState.isSubmitting}
|
||||||
showLogo
|
showLogo
|
||||||
heading={t("sign_in_account")}
|
heading={twoFactorRequired ? t("2fa_code") : t("sign_in_account")}
|
||||||
footerText={
|
footerText={twoFactorRequired ? TwoFactorFooter : LoginFooter}>
|
||||||
<>
|
<Form
|
||||||
{t("dont_have_an_account")} {/* replace this with your account creation flow */}
|
form={form}
|
||||||
<a href={`${WEBSITE_URL}/signup`} className="font-medium text-neutral-900">
|
className="space-y-6"
|
||||||
{t("create_an_account")}
|
handleSubmit={(values) => {
|
||||||
</a>
|
signIn<"credentials">("credentials", { ...values, callbackUrl, redirect: false })
|
||||||
</>
|
.then((res) => {
|
||||||
}>
|
if (!res) setErrorMessage(errorMessages[ErrorCode.InternalServerError]);
|
||||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
// we're logged in! let's do a hard refresh to the desired url
|
||||||
<input name="csrfToken" type="hidden" defaultValue={csrfToken || undefined} hidden />
|
else if (!res.error) window.location.replace(callbackUrl);
|
||||||
<div>
|
// reveal two factor input if required
|
||||||
<label htmlFor="email" className="block text-sm font-medium text-neutral-700">
|
else if (res.error === ErrorCode.SecondFactorRequired) setTwoFactorRequired(true);
|
||||||
{t("email_address")}
|
// fallback if error not found
|
||||||
</label>
|
else setErrorMessage(errorMessages[res.error] || t("something_went_wrong"));
|
||||||
<div className="mt-1">
|
})
|
||||||
|
.catch(() => setErrorMessage(errorMessages[ErrorCode.InternalServerError]));
|
||||||
|
}}>
|
||||||
|
<input defaultValue={csrfToken || undefined} type="hidden" hidden {...form.register("csrfToken")} />
|
||||||
|
|
||||||
|
<div className={classNames("space-y-6", { hidden: twoFactorRequired })}>
|
||||||
<EmailField
|
<EmailField
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
label={t("email_address")}
|
||||||
placeholder="john.doe@example.com"
|
placeholder="john.doe@example.com"
|
||||||
required
|
required
|
||||||
value={email}
|
{...form.register("email")}
|
||||||
onInput={(e) => setEmail(e.currentTarget.value)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute right-0 -top-[2px]">
|
<div className="absolute right-0 -top-[2px]">
|
||||||
<Link href="/auth/forgot-password">
|
<Link href="/auth/forgot-password">
|
||||||
|
@ -143,92 +126,59 @@ export default function Login({
|
||||||
</div>
|
</div>
|
||||||
<PasswordField
|
<PasswordField
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
|
||||||
type="password"
|
type="password"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
required
|
required
|
||||||
value={password}
|
{...form.register("password")}
|
||||||
onInput={(e) => setPassword(e.currentTarget.value)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{secondFactorRequired && (
|
{twoFactorRequired && <TwoFactor />}
|
||||||
<TextField
|
|
||||||
className="mt-1"
|
|
||||||
id="totpCode"
|
|
||||||
name={t("2fa_code")}
|
|
||||||
type="text"
|
|
||||||
maxLength={6}
|
|
||||||
minLength={6}
|
|
||||||
inputMode="numeric"
|
|
||||||
value={code}
|
|
||||||
onInput={(e) => setCode(e.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
{errorMessage && <Alert severity="error" title={errorMessage} />}
|
||||||
<div className="flex space-y-2">
|
<div className="flex space-y-2">
|
||||||
<Button className="flex justify-center w-full" type="submit" disabled={isSubmitting}>
|
<Button
|
||||||
{t("sign_in")}
|
className="flex justify-center w-full"
|
||||||
|
type="submit"
|
||||||
|
disabled={form.formState.isSubmitting}>
|
||||||
|
{twoFactorRequired ? t("submit") : t("sign_in")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</Form>
|
||||||
|
|
||||||
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
|
{!twoFactorRequired && (
|
||||||
</form>
|
<>
|
||||||
{isGoogleLoginEnabled && (
|
{isGoogleLoginEnabled && (
|
||||||
<div style={{ marginTop: "12px" }}>
|
<div className="mt-5">
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
className="flex justify-center w-full"
|
className="flex justify-center w-full"
|
||||||
data-testid={"google"}
|
data-testid={"google"}
|
||||||
onClick={async (event) => {
|
onClick={async (e) => {
|
||||||
event.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// track Google logins. Without personal data/payload
|
// track Google logins. Without personal data/payload
|
||||||
telemetry.withJitsu((jitsu) =>
|
telemetry.withJitsu((jitsu) =>
|
||||||
jitsu.track(telemetryEventTypes.googleLogin, collectPageParameters())
|
jitsu.track(telemetryEventTypes.googleLogin, collectPageParameters())
|
||||||
);
|
);
|
||||||
|
|
||||||
await signIn("google");
|
await signIn("google");
|
||||||
}}>
|
}}>
|
||||||
{" "}
|
|
||||||
{t("signin_with_google")}
|
{t("signin_with_google")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isSAMLLoginEnabled && (
|
{isSAMLLoginEnabled && (
|
||||||
<div style={{ marginTop: "12px" }}>
|
<SAMLLogin
|
||||||
<Button
|
email={form.getValues("email")}
|
||||||
color="secondary"
|
samlTenantID={samlTenantID}
|
||||||
data-testid={"saml"}
|
samlProductID={samlProductID}
|
||||||
className="flex justify-center w-full"
|
hostedCal={hostedCal}
|
||||||
onClick={async (event) => {
|
setErrorMessage={setErrorMessage}
|
||||||
event.preventDefault();
|
/>
|
||||||
|
)}
|
||||||
// track SAML logins. Without personal data/payload
|
</>
|
||||||
telemetry.withJitsu((jitsu) =>
|
|
||||||
jitsu.track(telemetryEventTypes.samlLogin, collectPageParameters())
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hostedCal) {
|
|
||||||
await signIn("saml", {}, { tenant: samlTenantID, product: samlProductID });
|
|
||||||
} else {
|
|
||||||
if (email.length === 0) {
|
|
||||||
setErrorMessage(t("saml_email_required"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// hosted solution, fetch tenant and product from the backend
|
|
||||||
mutation.mutate({
|
|
||||||
email,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
{t("signin_with_saml")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</AuthContainer>
|
</AuthContainer>
|
||||||
|
|
||||||
<AddToHomescreen />
|
<AddToHomescreen />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -9635,6 +9635,11 @@ react-date-picker@^8.3.6:
|
||||||
react-fit "^1.0.3"
|
react-fit "^1.0.3"
|
||||||
update-input-width "^1.2.2"
|
update-input-width "^1.2.2"
|
||||||
|
|
||||||
|
react-digit-input@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-digit-input/-/react-digit-input-2.1.0.tgz#8b0be6d3ea247fd361855483f21d0aafba341196"
|
||||||
|
integrity sha512-pGv0CtSmu3Mf4cD79LoYtJI7Wq4dpPiLiY1wvKsNaR+X2sJyk1ETiIxjq6G8i+XJqNXExM6vuytzDqblkkSaFw==
|
||||||
|
|
||||||
react-dom@^17.0.2:
|
react-dom@^17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
||||||
|
|
Loading…
Reference in a new issue