Improved authentication screens (Login/Logout/Forgot Password) (#1627)
This commit is contained in:
parent
5aaf702e2b
commit
e06edadda5
6 changed files with 218 additions and 229 deletions
|
@ -100,18 +100,15 @@ export const PasswordField = forwardRef<HTMLInputElement, InputFieldProps>(funct
|
|||
return <InputField type="password" placeholder="•••••••••••••" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
export const EmailInput = forwardRef<HTMLInputElement, JSX.IntrinsicElements["input"]>(function EmailInput(
|
||||
props,
|
||||
ref
|
||||
) {
|
||||
export const EmailInput = forwardRef<HTMLInputElement, InputFieldProps>(function EmailInput(props, ref) {
|
||||
return (
|
||||
<input
|
||||
<Input
|
||||
ref={ref}
|
||||
type="email"
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
autoCorrect="off"
|
||||
inputMode="email"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
40
components/ui/AuthContainer.tsx
Normal file
40
components/ui/AuthContainer.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import React from "react";
|
||||
|
||||
import Loader from "@components/Loader";
|
||||
import { HeadSeo } from "@components/seo/head-seo";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
footerText?: React.ReactNode | string;
|
||||
showLogo?: boolean;
|
||||
heading?: string;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export default function AuthContainer(props: React.PropsWithChildren<Props>) {
|
||||
return (
|
||||
<div className="flex flex-col justify-center min-h-screen py-12 bg-neutral-50 sm:px-6 lg:px-8">
|
||||
<HeadSeo title={props.title} description={props.description} />
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
{props.showLogo && (
|
||||
<img className="h-6 mx-auto" src="/calendso-logo-white-word.svg" alt="Cal.com Logo" />
|
||||
)}
|
||||
{props.heading && (
|
||||
<h2 className="mt-6 text-3xl font-bold text-center font-cal text-neutral-900">{props.heading}</h2>
|
||||
)}
|
||||
</div>
|
||||
{props.loading && (
|
||||
<div className="absolute z-50 flex items-center w-full h-screen bg-gray-50">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="px-4 py-8 mx-2 bg-white border rounded-sm sm:px-10 border-neutral-200">
|
||||
{props.children}
|
||||
</div>
|
||||
<div className="mt-4 text-sm text-center text-neutral-600">{props.footerText}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,13 +1,14 @@
|
|||
import debounce from "lodash/debounce";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { getCsrfToken } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import React, { SyntheticEvent } from "react";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import { EmailField } from "@components/form/fields";
|
||||
import { HeadSeo } from "@components/seo/head-seo";
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
import Button from "@components/ui/Button";
|
||||
|
||||
export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
|
||||
|
@ -72,64 +73,56 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
|
|||
const Success = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="mt-6 text-3xl font-extrabold text-center text-gray-900">{t("done")}</h2>
|
||||
<p>{t("check_email_reset_password")}</p>
|
||||
{error && <p className="text-red-600">{error.message}</p>}
|
||||
<p className="text-center">{t("check_email_reset_password")}</p>
|
||||
{error && <p className="text-center text-red-600">{error.message}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center min-h-screen py-12 bg-gray-50 sm:px-6 lg:px-8">
|
||||
<HeadSeo title={t("forgot_password")} description={t("request_password_reset")} />
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="px-4 pt-3 pb-8 mx-2 space-y-6 bg-white rounded-lg shadow sm:px-10">
|
||||
{success && <Success />}
|
||||
{!success && (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<h2 className="mt-6 text-3xl font-extrabold text-center text-gray-900 font-cal">
|
||||
{t("forgot_password")}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">{t("reset_instructions")}</p>
|
||||
{error && <p className="text-red-600">{error.message}</p>}
|
||||
</div>
|
||||
<form className="space-y-6" onSubmit={handleSubmit} action="#">
|
||||
<input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden />
|
||||
|
||||
<EmailField
|
||||
onChange={handleChange}
|
||||
id="email"
|
||||
name="email"
|
||||
label={t("email_address")}
|
||||
placeholder="john.doe@example.com"
|
||||
required
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
className="justify-center w-full"
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
aria-label={t("request_password_reset")}
|
||||
loading={loading}>
|
||||
{t("request_password_reset")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
href="/auth/login"
|
||||
color="minimal"
|
||||
role="button"
|
||||
aria-label={t("login_instead")}
|
||||
className="justify-center w-full">
|
||||
{t("login_instead")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AuthContainer
|
||||
title={t("forgot_password")}
|
||||
description={t("request_password_reset")}
|
||||
heading={t("forgot_password")}
|
||||
footerText={
|
||||
<>
|
||||
{t("already_have_an_account")}{" "}
|
||||
<Link href="/auth/login">
|
||||
<a className="font-medium text-neutral-900">{t("login_instead")}</a>
|
||||
</Link>
|
||||
</>
|
||||
}>
|
||||
{success && <Success />}
|
||||
{!success && (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<p className="mb-4 text-sm text-gray-500">{t("reset_instructions")}</p>
|
||||
{error && <p className="text-red-600">{error.message}</p>}
|
||||
</div>
|
||||
<form className="space-y-6" onSubmit={handleSubmit} action="#">
|
||||
<input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden />
|
||||
<EmailField
|
||||
onChange={handleChange}
|
||||
id="email"
|
||||
name="email"
|
||||
label={t("email_address")}
|
||||
placeholder="john.doe@example.com"
|
||||
required
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
className="justify-center w-full"
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
aria-label={t("request_password_reset")}
|
||||
loading={loading}>
|
||||
{t("request_password_reset")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</AuthContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,9 +12,9 @@ import { trpc } from "@lib/trpc";
|
|||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import AddToHomescreen from "@components/AddToHomescreen";
|
||||
import Loader from "@components/Loader";
|
||||
import { EmailInput } from "@components/form/fields";
|
||||
import { HeadSeo } from "@components/seo/head-seo";
|
||||
import { EmailField, PasswordField, TextField } from "@components/form/fields";
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
import Button from "@components/ui/Button";
|
||||
|
||||
import { IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
@ -97,150 +97,123 @@ export default function Login({
|
|||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center min-h-screen py-12 bg-neutral-50 sm:px-6 lg:px-8">
|
||||
<HeadSeo title={t("login")} description={t("login")} />
|
||||
|
||||
{isSubmitting && (
|
||||
<div className="absolute z-50 flex items-center w-full h-screen bg-gray-50">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<img className="h-6 mx-auto" src="/calendso-logo-white-word.svg" alt="Cal.com Logo" />
|
||||
<h2 className="mt-6 text-3xl font-bold text-center font-cal text-neutral-900">
|
||||
{t("sign_in_account")}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="px-4 py-8 mx-2 bg-white border rounded-sm sm:px-10 border-neutral-200">
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<input name="csrfToken" type="hidden" defaultValue={csrfToken || undefined} hidden />
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-neutral-700">
|
||||
{t("email_address")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<EmailInput
|
||||
id="email"
|
||||
name="email"
|
||||
required
|
||||
value={email}
|
||||
onInput={(e) => setEmail(e.currentTarget.value)}
|
||||
className="block w-full px-3 py-2 placeholder-gray-400 border rounded-sm shadow-sm appearance-none border-neutral-300 focus:outline-none focus:ring-neutral-900 focus:border-neutral-900 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<AuthContainer
|
||||
title={t("login")}
|
||||
description={t("login")}
|
||||
loading={isSubmitting}
|
||||
showLogo
|
||||
heading={t("sign_in_account")}
|
||||
footerText={
|
||||
<>
|
||||
{t("dont_have_an_account")} {/* replace this with your account creation flow */}
|
||||
<a href={`${WEBSITE_URL}/signup`} className="font-medium text-neutral-900">
|
||||
{t("create_an_account")}
|
||||
</a>
|
||||
</>
|
||||
}>
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<input name="csrfToken" type="hidden" defaultValue={csrfToken || undefined} hidden />
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-neutral-700">
|
||||
{t("email_address")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<EmailField
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="john.doe@example.com"
|
||||
required
|
||||
value={email}
|
||||
onInput={(e) => setEmail(e.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex">
|
||||
<div className="w-1/2">
|
||||
<label htmlFor="password" className="block text-sm font-medium text-neutral-700">
|
||||
{t("password")}
|
||||
</label>
|
||||
</div>
|
||||
<div className="w-1/2 text-right">
|
||||
<Link href="/auth/forgot-password">
|
||||
<a tabIndex={-1} className="text-sm font-medium text-primary-600">
|
||||
{t("forgot")}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
value={password}
|
||||
onInput={(e) => setPassword(e.currentTarget.value)}
|
||||
className="block w-full px-3 py-2 placeholder-gray-400 border rounded-sm shadow-sm appearance-none border-neutral-300 focus:outline-none focus:ring-neutral-900 focus:border-neutral-900 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="absolute right-0 -top-[2px]">
|
||||
<Link href="/auth/forgot-password">
|
||||
<a tabIndex={-1} className="text-sm font-medium text-primary-600">
|
||||
{t("forgot")}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<PasswordField
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
value={password}
|
||||
onInput={(e) => setPassword(e.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{secondFactorRequired && (
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-neutral-700">
|
||||
{t("2fa_code")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
id="totpCode"
|
||||
name="totpCode"
|
||||
type="text"
|
||||
maxLength={6}
|
||||
minLength={6}
|
||||
inputMode="numeric"
|
||||
value={code}
|
||||
onInput={(e) => setCode(e.currentTarget.value)}
|
||||
className="block w-full px-3 py-2 placeholder-gray-400 border rounded-sm shadow-sm appearance-none border-neutral-300 focus:outline-none focus:ring-neutral-900 focus:border-neutral-900 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="flex justify-center w-full px-4 py-2 text-sm font-medium text-white border border-transparent rounded-sm shadow-sm bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
|
||||
{t("sign_in")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
|
||||
</form>
|
||||
{isGoogleLoginEnabled && (
|
||||
<div style={{ marginTop: "12px" }}>
|
||||
<button
|
||||
data-testid={"google"}
|
||||
onClick={async () => await signIn("google")}
|
||||
className="flex justify-center w-full px-4 py-2 text-sm font-medium text-black border border-transparent rounded-sm shadow-sm bg-secondary-50 hover:bg-secondary-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
|
||||
{t("signin_with_google")}
|
||||
</button>
|
||||
</div>
|
||||
{secondFactorRequired && (
|
||||
<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)}
|
||||
/>
|
||||
)}
|
||||
{isSAMLLoginEnabled && (
|
||||
<div style={{ marginTop: "12px" }}>
|
||||
<button
|
||||
data-testid={"saml"}
|
||||
onClick={async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!hostedCal) {
|
||||
await signIn("saml", {}, { tenant: samlTenantID, product: samlProductID });
|
||||
} else {
|
||||
if (email.length === 0) {
|
||||
setErrorMessage(t("saml_email_required"));
|
||||
return;
|
||||
}
|
||||
<div className="flex space-y-2">
|
||||
<Button className="flex justify-center w-full" type="submit" disabled={isSubmitting}>
|
||||
{t("sign_in")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
// hosted solution, fetch tenant and product from the backend
|
||||
mutation.mutate({
|
||||
email,
|
||||
});
|
||||
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
|
||||
</form>
|
||||
{isGoogleLoginEnabled && (
|
||||
<div style={{ marginTop: "12px" }}>
|
||||
<Button
|
||||
color="secondary"
|
||||
className="flex justify-center w-full"
|
||||
data-testid={"google"}
|
||||
onClick={async () => await signIn("google")}>
|
||||
{" "}
|
||||
{t("signin_with_google")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{isSAMLLoginEnabled && (
|
||||
<div style={{ marginTop: "12px" }}>
|
||||
<Button
|
||||
color="secondary"
|
||||
data-testid={"saml"}
|
||||
className="flex justify-center w-full"
|
||||
onClick={async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!hostedCal) {
|
||||
await signIn("saml", {}, { tenant: samlTenantID, product: samlProductID });
|
||||
} else {
|
||||
if (email.length === 0) {
|
||||
setErrorMessage(t("saml_email_required"));
|
||||
return;
|
||||
}
|
||||
}}
|
||||
className="flex justify-center w-full px-4 py-2 text-sm font-medium text-black border border-transparent rounded-sm shadow-sm bg-secondary-50 hover:bg-secondary-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
|
||||
{t("signin_with_saml")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 text-sm text-center text-neutral-600">
|
||||
{t("dont_have_an_account")} {/* replace this with your account creation flow */}
|
||||
<a href={`${WEBSITE_URL}/signup`} className="font-medium text-neutral-900">
|
||||
{t("create_an_account")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// hosted solution, fetch tenant and product from the backend
|
||||
mutation.mutate({
|
||||
email,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
{t("signin_with_saml")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</AuthContainer>
|
||||
|
||||
<AddToHomescreen />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,8 @@ import { useEffect } from "react";
|
|||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
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";
|
||||
|
||||
|
@ -23,40 +24,24 @@ export default function Logout(props: Props) {
|
|||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 overflow-y-auto"
|
||||
aria-labelledby="modal-title"
|
||||
role="dialog"
|
||||
aria-modal="true">
|
||||
<HeadSeo title={t("logged_out")} description={t("logged_out")} />
|
||||
<div className="flex items-end justify-center min-h-screen px-4 pt-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 px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
|
||||
<div>
|
||||
<div className="flex items-center justify-center w-12 h-12 mx-auto bg-green-100 rounded-full">
|
||||
<CheckIcon className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||
{t("youve_been_logged_out")}
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">{t("hope_to_see_you_soon")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-6">
|
||||
<Link href="/auth/login">
|
||||
<a className="inline-flex justify-center w-full px-4 py-2 text-base font-medium border border-transparent rounded-md shadow-sm bg-brand text-brandcontrast focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black sm:text-sm">
|
||||
{t("go_back_login")}
|
||||
</a>
|
||||
</Link>
|
||||
<AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")}>
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-center w-12 h-12 mx-auto bg-green-100 rounded-full">
|
||||
<CheckIcon className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||
{t("youve_been_logged_out")}
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">{t("hope_to_see_you_soon")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Link href="/auth/login">
|
||||
<Button className="flex justify-center w-full"> {t("go_back_login")}</Button>
|
||||
</Link>
|
||||
</AuthContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -158,6 +158,7 @@
|
|||
"30min_meeting": "30 Min Meeting",
|
||||
"secret_meeting": "Secret Meeting",
|
||||
"login_instead": "Login instead",
|
||||
"already_have_an_account": "Already have an account?",
|
||||
"create_account": "Create Account",
|
||||
"confirm_password": "Confirm password",
|
||||
"create_your_account": "Create your account",
|
||||
|
|
Loading…
Reference in a new issue