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} />; return <InputField type="password" placeholder="•••••••••••••" ref={ref} {...props} />;
}); });
export const EmailInput = forwardRef<HTMLInputElement, JSX.IntrinsicElements["input"]>(function EmailInput( export const EmailInput = forwardRef<HTMLInputElement, InputFieldProps>(function EmailInput(props, ref) {
props,
ref
) {
return ( return (
<input <Input
ref={ref}
type="email" type="email"
autoCapitalize="none" autoCapitalize="none"
autoComplete="email" autoComplete="email"
autoCorrect="off" autoCorrect="off"
inputMode="email" inputMode="email"
ref={ref}
{...props} {...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 debounce from "lodash/debounce";
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react"; import { getCsrfToken } from "next-auth/react";
import Link from "next/link";
import React, { SyntheticEvent } from "react"; import React, { SyntheticEvent } from "react";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
import { EmailField } from "@components/form/fields"; 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"; import Button from "@components/ui/Button";
export default function ForgotPassword({ csrfToken }: { csrfToken: string }) { export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
@ -72,31 +73,34 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
const Success = () => { const Success = () => {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<h2 className="mt-6 text-3xl font-extrabold text-center text-gray-900">{t("done")}</h2> <p className="text-center">{t("check_email_reset_password")}</p>
<p>{t("check_email_reset_password")}</p> {error && <p className="text-center text-red-600">{error.message}</p>}
{error && <p className="text-red-600">{error.message}</p>}
</div> </div>
); );
}; };
return ( return (
<div className="flex flex-col justify-center min-h-screen py-12 bg-gray-50 sm:px-6 lg:px-8"> <AuthContainer
<HeadSeo title={t("forgot_password")} description={t("request_password_reset")} /> title={t("forgot_password")}
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md"> description={t("request_password_reset")}
<div className="px-4 pt-3 pb-8 mx-2 space-y-6 bg-white rounded-lg shadow sm:px-10"> 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 && <Success />}
{!success && ( {!success && (
<> <>
<div className="space-y-6"> <div className="space-y-6">
<h2 className="mt-6 text-3xl font-extrabold text-center text-gray-900 font-cal"> <p className="mb-4 text-sm text-gray-500">{t("reset_instructions")}</p>
{t("forgot_password")}
</h2>
<p className="text-sm text-gray-500">{t("reset_instructions")}</p>
{error && <p className="text-red-600">{error.message}</p>} {error && <p className="text-red-600">{error.message}</p>}
</div> </div>
<form className="space-y-6" onSubmit={handleSubmit} action="#"> <form className="space-y-6" onSubmit={handleSubmit} action="#">
<input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden /> <input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden />
<EmailField <EmailField
onChange={handleChange} onChange={handleChange}
id="email" id="email"
@ -114,22 +118,11 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
loading={loading}> loading={loading}>
{t("request_password_reset")} {t("request_password_reset")}
</Button> </Button>
<Button
href="/auth/login"
color="minimal"
role="button"
aria-label={t("login_instead")}
className="justify-center w-full">
{t("login_instead")}
</Button>
</div> </div>
</form> </form>
</> </>
)} )}
</div> </AuthContainer>
</div>
</div>
); );
} }

View file

@ -12,9 +12,9 @@ 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 Loader from "@components/Loader"; import { EmailField, PasswordField, TextField } from "@components/form/fields";
import { EmailInput } from "@components/form/fields"; import AuthContainer from "@components/ui/AuthContainer";
import { HeadSeo } from "@components/seo/head-seo"; 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";
@ -97,24 +97,21 @@ export default function Login({
}); });
return ( 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")} /> <AuthContainer
title={t("login")}
{isSubmitting && ( description={t("login")}
<div className="absolute z-50 flex items-center w-full h-screen bg-gray-50"> loading={isSubmitting}
<Loader /> showLogo
</div> heading={t("sign_in_account")}
)} footerText={
<>
<div className="sm:mx-auto sm:w-full sm:max-w-md"> {t("dont_have_an_account")} {/* replace this with your account creation flow */}
<img className="h-6 mx-auto" src="/calendso-logo-white-word.svg" alt="Cal.com Logo" /> <a href={`${WEBSITE_URL}/signup`} className="font-medium text-neutral-900">
<h2 className="mt-6 text-3xl font-bold text-center font-cal text-neutral-900"> {t("create_an_account")}
{t("sign_in_account")} </a>
</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}> <form className="space-y-6" onSubmit={handleSubmit}>
<input name="csrfToken" type="hidden" defaultValue={csrfToken || undefined} hidden /> <input name="csrfToken" type="hidden" defaultValue={csrfToken || undefined} hidden />
<div> <div>
@ -122,34 +119,26 @@ export default function Login({
{t("email_address")} {t("email_address")}
</label> </label>
<div className="mt-1"> <div className="mt-1">
<EmailInput <EmailField
id="email" id="email"
name="email" name="email"
placeholder="john.doe@example.com"
required required
value={email} value={email}
onInput={(e) => setEmail(e.currentTarget.value)} 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> </div>
</div> </div>
<div> <div className="relative">
<div className="flex"> <div className="absolute right-0 -top-[2px]">
<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"> <Link href="/auth/forgot-password">
<a tabIndex={-1} className="text-sm font-medium text-primary-600"> <a tabIndex={-1} className="text-sm font-medium text-primary-600">
{t("forgot")} {t("forgot")}
</a> </a>
</Link> </Link>
</div> </div>
</div> <PasswordField
<div className="mt-1">
<input
id="password" id="password"
name="password" name="password"
type="password" type="password"
@ -157,57 +146,49 @@ export default function Login({
required required
value={password} value={password}
onInput={(e) => setPassword(e.currentTarget.value)} 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>
</div>
{secondFactorRequired && ( {secondFactorRequired && (
<div> <TextField
<label htmlFor="email" className="block text-sm font-medium text-neutral-700"> className="mt-1"
{t("2fa_code")}
</label>
<div className="mt-1">
<input
id="totpCode" id="totpCode"
name="totpCode" name={t("2fa_code")}
type="text" type="text"
maxLength={6} maxLength={6}
minLength={6} minLength={6}
inputMode="numeric" inputMode="numeric"
value={code} value={code}
onInput={(e) => setCode(e.currentTarget.value)} 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"> <div className="flex space-y-2">
<button <Button className="flex justify-center w-full" type="submit" disabled={isSubmitting}>
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")} {t("sign_in")}
</button> </Button>
</div> </div>
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>} {errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
</form> </form>
{isGoogleLoginEnabled && ( {isGoogleLoginEnabled && (
<div style={{ marginTop: "12px" }}> <div style={{ marginTop: "12px" }}>
<button <Button
color="secondary"
className="flex justify-center w-full"
data-testid={"google"} data-testid={"google"}
onClick={async () => await signIn("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")} {t("signin_with_google")}
</button> </Button>
</div> </div>
)} )}
{isSAMLLoginEnabled && ( {isSAMLLoginEnabled && (
<div style={{ marginTop: "12px" }}> <div style={{ marginTop: "12px" }}>
<button <Button
color="secondary"
data-testid={"saml"} data-testid={"saml"}
className="flex justify-center w-full"
onClick={async (event) => { onClick={async (event) => {
event.preventDefault(); event.preventDefault();
@ -224,23 +205,15 @@ export default function Login({
email, email,
}); });
} }
}} }}>
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")} {t("signin_with_saml")}
</button> </Button>
</div> </div>
)} )}
</div> </AuthContainer>
<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>
<AddToHomescreen /> <AddToHomescreen />
</div> </>
); );
} }

View file

@ -7,7 +7,8 @@ import { useEffect } from "react";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
import { inferSSRProps } from "@lib/types/inferSSRProps"; 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"; import { ssrInit } from "@server/lib/ssr";
@ -23,18 +24,8 @@ export default function Logout(props: Props) {
const { t } = useLocale(); const { t } = useLocale();
return ( return (
<div <AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")}>
className="fixed inset-0 z-50 overflow-y-auto" <div className="mb-4">
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"> <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" /> <CheckIcon className="w-6 h-6 text-green-600" />
</div> </div>
@ -47,16 +38,10 @@ export default function Logout(props: Props) {
</div> </div>
</div> </div>
</div> </div>
<div className="mt-5 sm:mt-6">
<Link href="/auth/login"> <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"> <Button className="flex justify-center w-full"> {t("go_back_login")}</Button>
{t("go_back_login")}
</a>
</Link> </Link>
</div> </AuthContainer>
</div>
</div>
</div>
); );
} }

View file

@ -158,6 +158,7 @@
"30min_meeting": "30 Min Meeting", "30min_meeting": "30 Min Meeting",
"secret_meeting": "Secret Meeting", "secret_meeting": "Secret Meeting",
"login_instead": "Login instead", "login_instead": "Login instead",
"already_have_an_account": "Already have an account?",
"create_account": "Create Account", "create_account": "Create Account",
"confirm_password": "Confirm password", "confirm_password": "Confirm password",
"create_your_account": "Create your account", "create_your_account": "Create your account",