Improved authentication screens (Login/Logout/Forgot Password) (#1627)

This commit is contained in:
Jamie Pine 2022-01-27 02:16:20 -08:00 committed by GitHub
parent 5aaf702e2b
commit e06edadda5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 218 additions and 229 deletions

View file

@ -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}
/>
);

View 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>
);
}

View file

@ -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>
);
}

View file

@ -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>
</>
);
}

View file

@ -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">
&#8203;
</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>
);
}

View file

@ -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",