chore: Auth module revamp (#4335)

Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Dhruwang Jariwala
2024-11-26 13:58:13 +05:30
committed by GitHub
parent 7598a16b75
commit f80d1b32b7
170 changed files with 2484 additions and 2582 deletions
@@ -1,46 +0,0 @@
"use client";
import { FormControl, FormField, FormItem } from "@/modules/ui/components/form";
import { OTPInput } from "@/modules/ui/components/otp-input";
import { useTranslations } from "next-intl";
import React from "react";
import { UseFormReturn } from "react-hook-form";
interface TwoFactorProps {
form: UseFormReturn<
{
email: string;
password: string;
totpCode?: string | undefined;
backupCode?: string | undefined;
},
any,
undefined
>;
}
export const TwoFactor = ({ form }: TwoFactorProps) => {
const t = useTranslations();
return (
<>
<div className="mb-2 transition-all duration-500 ease-in-out">
<label htmlFor="totp" className="sr-only">
{t("auth.login.enter_your_two_factor_authentication_code")}
</label>
<FormField
control={form.control}
name="totpCode"
render={({ field }) => (
<FormItem className="w-full">
<FormControl>
<OTPInput value={field.value ?? ""} onChange={field.onChange} valueLength={6} />
</FormControl>
</FormItem>
)}
/>
</div>
</>
);
};
@@ -1,53 +0,0 @@
"use client";
import { FormField, FormItem } from "@/modules/ui/components/form";
import { FormControl } from "@/modules/ui/components/form";
import { Input } from "@/modules/ui/components/input";
import { useTranslations } from "next-intl";
import React from "react";
import { UseFormReturn } from "react-hook-form";
interface TwoFactorBackupProps {
form: UseFormReturn<
{
email: string;
password: string;
totpCode?: string | undefined;
backupCode?: string | undefined;
},
any,
undefined
>;
}
export const TwoFactorBackup = ({ form }: TwoFactorBackupProps) => {
const t = useTranslations();
return (
<>
<div className="mb-2 transition-all duration-500 ease-in-out">
<label htmlFor="totpBackup" className="sr-only">
{t("auth.login.backup_code")}
</label>
<FormField
control={form.control}
name="backupCode"
render={({ field }) => (
<FormItem className="w-full">
<FormControl>
<Input
id="totpBackup"
required
placeholder="XXXXX-XXXXX"
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
value={field.value}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
</FormItem>
)}
/>
</div>
</>
);
};
@@ -1,379 +0,0 @@
"use client";
import { TwoFactor } from "@/modules/auth/components/SigninForm/components/TwoFactor";
import { TwoFactorBackup } from "@/modules/auth/components/SigninForm/components/TwoFactorBackup";
import { createEmailTokenAction } from "@/modules/auth/components/SignupOptions/actions";
import { AzureButton } from "@/modules/auth/components/SignupOptions/components/AzureButton";
import { GithubButton } from "@/modules/auth/components/SignupOptions/components/GithubButton";
import { GoogleButton } from "@/modules/auth/components/SignupOptions/components/GoogleButton";
import { OpenIdButton } from "@/modules/auth/components/SignupOptions/components/OpenIdButton";
import { Button } from "@/modules/ui/components/button";
import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form";
import { PasswordInput } from "@/modules/ui/components/password-input";
import { zodResolver } from "@hookform/resolvers/zod";
import { XCircleIcon } from "lucide-react";
import { signIn } from "next-auth/react";
import { useTranslations } from "next-intl";
import Link from "next/dist/client/link";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useRef, useState } from "react";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { z } from "zod";
import { cn } from "@formbricks/lib/cn";
import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage";
interface TSigninFormState {
email: string;
password: string;
totpCode: string;
backupCode: string;
}
interface SignInFormProps {
emailAuthEnabled: boolean;
publicSignUpEnabled: boolean;
passwordResetEnabled: boolean;
googleOAuthEnabled: boolean;
githubOAuthEnabled: boolean;
azureOAuthEnabled: boolean;
oidcOAuthEnabled: boolean;
oidcDisplayName?: string;
isMultiOrgEnabled: boolean;
}
export const SigninForm = ({
emailAuthEnabled,
publicSignUpEnabled,
passwordResetEnabled,
googleOAuthEnabled,
githubOAuthEnabled,
azureOAuthEnabled,
oidcOAuthEnabled,
oidcDisplayName,
isMultiOrgEnabled,
}: SignInFormProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const emailRef = useRef<HTMLInputElement>(null);
const formMethods = useForm<TSigninFormState>();
const callbackUrl = searchParams?.get("callbackUrl");
const ZSignInInput = z.object({
email: z.string().email(),
password: z.string().min(8),
totpCode: z.string().optional(),
backupCode: z.string().optional(),
});
type TSignInInput = z.infer<typeof ZSignInInput>;
const form = useForm<TSignInInput>({
defaultValues: {
email: "",
password: "",
totpCode: "",
backupCode: "",
},
resolver: zodResolver(ZSignInInput),
});
const t = useTranslations();
const onSubmit: SubmitHandler<TSigninFormState> = async (data) => {
setLoggingIn(true);
if (typeof window !== "undefined") {
localStorage.setItem(FORMBRICKS_LOGGED_IN_WITH_LS, "Email");
}
try {
const signInResponse = await signIn("credentials", {
callbackUrl: callbackUrl ?? "/",
email: data.email.toLowerCase(),
password: data.password,
...(totpLogin && { totpCode: data.totpCode }),
...(totpBackup && { backupCode: data.backupCode }),
redirect: false,
});
if (signInResponse?.error === "second factor required") {
setTotpLogin(true);
setLoggingIn(false);
return;
}
if (signInResponse?.error === "Email Verification is Pending") {
const emailTokenActionResponse = await createEmailTokenAction({ email: data.email });
if (emailTokenActionResponse?.serverError) {
setSignInError(emailTokenActionResponse.serverError);
return;
}
router.push(`/auth/verification-requested?token=${emailTokenActionResponse?.data}`);
return;
}
if (signInResponse?.error) {
setLoggingIn(false);
setSignInError(signInResponse.error);
return;
}
if (!signInResponse?.error) {
router.push(searchParams?.get("callbackUrl") || "/");
}
} catch (error) {
const errorMessage = error.toString();
const errorFeedback = errorMessage.includes("Invalid URL")
? t("auth.login.too_many_requests_please_try_again_after_some_time")
: error.message;
setSignInError(errorFeedback);
} finally {
setLoggingIn(false);
}
};
const [loggingIn, setLoggingIn] = useState(false);
const [showLogin, setShowLogin] = useState(false);
const [isPasswordFocused, setIsPasswordFocused] = useState(false);
const [totpLogin, setTotpLogin] = useState(false);
const [totpBackup, setTotpBackup] = useState(false);
const [signInError, setSignInError] = useState("");
const formRef = useRef<HTMLFormElement>(null);
const error = searchParams?.get("error");
const inviteToken = callbackUrl ? new URL(callbackUrl).searchParams.get("token") : null;
const [lastLoggedInWith, setLastLoginWith] = useState("");
useEffect(() => {
if (typeof window !== "undefined") {
setLastLoginWith(localStorage.getItem(FORMBRICKS_LOGGED_IN_WITH_LS) || "");
}
}, []);
useEffect(() => {
if (error) {
setSignInError(error);
}
}, [error]);
const formLabel = useMemo(() => {
if (totpBackup) {
return t("auth.login.enter_your_backup_code");
}
if (totpLogin) {
return t("auth.login.enter_your_two_factor_authentication_code");
}
return t("auth.login.login_to_your_account");
}, [totpBackup, totpLogin]);
const TwoFactorComponent = useMemo(() => {
if (totpBackup) {
return <TwoFactorBackup form={form} />;
}
if (totpLogin) {
return <TwoFactor form={form} />;
}
return null;
}, [totpBackup, totpLogin]);
return (
<FormProvider {...formMethods}>
<div className="text-center">
<h1 className="mb-4 text-slate-700">{formLabel}</h1>
<div className="space-y-2">
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
{TwoFactorComponent}
{showLogin && (
<div className={cn(totpLogin && "hidden", "space-y-2")}>
<FormField
control={form.control}
name="email"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full">
<FormControl>
<div>
<input
id="email"
type="email"
autoComplete="email"
required
value={field.value}
onChange={(email) => field.onChange(email)}
placeholder="work@email.com"
defaultValue={searchParams?.get("email") || ""}
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full">
<FormControl>
<div>
<PasswordInput
id="password"
autoComplete="current-password"
placeholder="*******"
aria-placeholder="password"
onFocus={() => setIsPasswordFocused(true)}
required
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
value={field.value}
onChange={(password) => field.onChange(password)}
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
</FormControl>
</FormItem>
)}
/>
{passwordResetEnabled && isPasswordFocused && (
<div className="ml-1 text-right transition-all duration-500 ease-in-out">
<Link
href="/auth/forgot-password"
className="hover:text-brand-dark text-xs text-slate-500">
{t("auth.login.forgot_your_password")}
</Link>
</div>
)}
</div>
)}
{emailAuthEnabled && (
<Button
size="base"
onClick={() => {
if (!showLogin) {
setShowLogin(true);
// Add a slight delay before focusing the input field to ensure it's visible
setTimeout(() => emailRef.current?.focus(), 100);
} else if (formRef.current) {
formRef.current.requestSubmit();
}
}}
className="relative w-full justify-center"
loading={loggingIn}>
{totpLogin ? t("common.submit") : t("auth.login.login_with_email")}
{lastLoggedInWith && lastLoggedInWith === "Email" ? (
<span className="absolute right-3 text-xs opacity-50">{t("auth.last_used")}</span>
) : null}
</Button>
)}
</form>
{googleOAuthEnabled && !totpLogin && (
<>
<GoogleButton
inviteUrl={callbackUrl}
lastUsed={lastLoggedInWith === "Google"}
text={t("auth.continue_with_google")}
/>
</>
)}
{githubOAuthEnabled && !totpLogin && (
<>
<GithubButton
inviteUrl={callbackUrl}
lastUsed={lastLoggedInWith === "Github"}
text={t("auth.continue_with_github")}
/>
</>
)}
{azureOAuthEnabled && !totpLogin && (
<>
<AzureButton
inviteUrl={callbackUrl}
lastUsed={lastLoggedInWith === "Azure"}
text={t("auth.continue_with_azure")}
/>
</>
)}
{oidcOAuthEnabled && !totpLogin && (
<>
<OpenIdButton
inviteUrl={callbackUrl}
text={t("auth.continue_with_oidc", { oidcDisplayName })}
lastUsed={lastLoggedInWith === "OpenID"}
/>
</>
)}
</div>
{publicSignUpEnabled && !totpLogin && isMultiOrgEnabled && (
<div className="mt-9 text-center text-xs">
<span className="leading-5 text-slate-500">{t("auth.login.new_to_formbricks")}</span>
<br />
<Link
href={inviteToken ? `/auth/signup?inviteToken=${inviteToken}` : "/auth/signup"}
className="font-semibold text-slate-600 underline hover:text-slate-700">
{t("auth.login.create_an_account")}
</Link>
</div>
)}
</div>
{totpLogin && !totpBackup && (
<div className="mt-9 text-center text-xs">
<span className="leading-5 text-slate-500">{t("auth.login.lost_access")}</span>
<br />
<div className="flex flex-col">
<button
type="button"
className="font-semibold text-slate-600 underline hover:text-slate-700"
onClick={() => {
setTotpBackup(true);
}}>
{t("auth.login.use_a_backup_code")}
</button>
<button
type="button"
className="mt-4 font-semibold text-slate-600 underline hover:text-slate-700"
onClick={() => {
setTotpLogin(false);
}}>
{t("common.go_back")}
</button>
</div>
</div>
)}
{totpBackup && (
<div className="mt-9 text-center text-xs">
<button
type="button"
className="font-semibold text-slate-600 underline hover:text-slate-700"
onClick={() => {
setTotpBackup(false);
}}>
{t("common.go_back")}
</button>
</div>
)}
{signInError && (
<div className="absolute top-10 rounded-md bg-red-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
{t("auth.login.an_error_occurred_when_logging_you_in")}
</h3>
<div className="mt-2 text-sm text-red-700">
<p className="space-y-1 whitespace-pre-wrap">{signInError}</p>
</div>
</div>
</div>
</div>
)}
</FormProvider>
);
};
@@ -1,21 +0,0 @@
"use server";
import { actionClient } from "@/lib/utils/action-client";
import { z } from "zod";
import { createEmailToken } from "@formbricks/lib/jwt";
import { getUserByEmail } from "@formbricks/lib/user/service";
const ZCreateEmailTokenAction = z.object({
email: z.string().min(5).max(255).email({ message: "Invalid email" }),
});
export const createEmailTokenAction = actionClient
.schema(ZCreateEmailTokenAction)
.action(async ({ parsedInput }) => {
const user = await getUserByEmail(parsedInput.email);
if (!user) {
throw new Error("Invalid request");
}
return createEmailToken(parsedInput.email);
});
@@ -1,50 +0,0 @@
import { Button } from "@/modules/ui/components/button";
import { MicrosoftIcon } from "@/modules/ui/components/icons";
import { signIn } from "next-auth/react";
import { useTranslations } from "next-intl";
import { useCallback, useEffect } from "react";
import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage";
export const AzureButton = ({
text = "Continue with Azure",
inviteUrl,
directRedirect = false,
lastUsed,
}: {
text?: string;
inviteUrl?: string | null;
directRedirect?: boolean;
lastUsed?: boolean;
}) => {
const t = useTranslations();
const handleLogin = useCallback(async () => {
if (typeof window !== "undefined") {
localStorage.setItem(FORMBRICKS_LOGGED_IN_WITH_LS, "Azure");
}
await signIn("azure-ad", {
redirect: true,
callbackUrl: inviteUrl ? inviteUrl : "/",
});
}, [inviteUrl]);
useEffect(() => {
if (directRedirect) {
handleLogin();
}
}, [directRedirect, handleLogin]);
return (
<Button
size="base"
type="button"
EndIcon={MicrosoftIcon}
startIconClassName="ml-2"
onClick={handleLogin}
variant="secondary"
className="relative w-full justify-center">
{text}
{lastUsed && <span className="absolute right-3 text-xs opacity-50">{t("auth.last_used")}</span>}
</Button>
);
};
@@ -1,42 +0,0 @@
"use client";
import { Button } from "@/modules/ui/components/button";
import { GithubIcon } from "@/modules/ui/components/icons";
import { signIn } from "next-auth/react";
import { useTranslations } from "next-intl";
import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage";
export const GithubButton = ({
text = "Continue with Github",
inviteUrl,
lastUsed,
}: {
text?: string;
inviteUrl?: string | null;
lastUsed?: boolean;
}) => {
const t = useTranslations();
const handleLogin = async () => {
if (typeof window !== "undefined") {
localStorage.setItem(FORMBRICKS_LOGGED_IN_WITH_LS, "Github");
}
await signIn("github", {
redirect: true,
callbackUrl: inviteUrl ? inviteUrl : "/", // redirect after login to /
});
};
return (
<Button
size="base"
type="button"
EndIcon={GithubIcon}
startIconClassName="ml-2"
onClick={handleLogin}
variant="secondary"
className="relative w-full justify-center">
{text}
{lastUsed && <span className="absolute right-3 text-xs opacity-50">{t("auth.last_used")}</span>}
</Button>
);
};
@@ -1,42 +0,0 @@
"use client";
import { Button } from "@/modules/ui/components/button";
import { GoogleIcon } from "@/modules/ui/components/icons";
import { signIn } from "next-auth/react";
import { useTranslations } from "next-intl";
import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage";
export const GoogleButton = ({
text = "Continue with Google",
inviteUrl,
lastUsed,
}: {
text?: string;
inviteUrl?: string | null;
lastUsed?: boolean;
}) => {
const t = useTranslations();
const handleLogin = async () => {
if (typeof window !== "undefined") {
localStorage.setItem(FORMBRICKS_LOGGED_IN_WITH_LS, "Google");
}
await signIn("google", {
redirect: true,
callbackUrl: inviteUrl ? inviteUrl : "/", // redirect after login to /
});
};
return (
<Button
size="base"
type="button"
EndIcon={GoogleIcon}
startIconClassName="ml-3"
onClick={handleLogin}
variant="secondary"
className="relative w-full justify-center">
{text}
{lastUsed && <span className="absolute right-3 text-xs opacity-50">{t("auth.last_used")}</span>}
</Button>
);
};
@@ -1,75 +0,0 @@
import { CheckIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
interface Validation {
label: string;
state: boolean;
}
const PASSWORD_REGEX = {
UPPER_AND_LOWER: /^(?=.*[A-Z])(?=.*[a-z])/,
NUMBER: /\d/,
};
const DEFAULT_VALIDATIONS = [
{ label: "auth.signup.password_validation_uppercase_and_lowercase", state: false },
{ label: "auth.signup.password_validation_minimum_8_and_maximum_128_characters", state: false },
{ label: "auth.signup.password_validation_contain_at_least_1_number", state: false },
];
export const IsPasswordValid = ({
password,
setIsValid,
}: {
password: string | null;
setIsValid: (isValid: boolean) => void;
}) => {
const t = useTranslations();
const [validations, setValidations] = useState<Validation[]>(DEFAULT_VALIDATIONS);
useEffect(() => {
let newValidations = [...DEFAULT_VALIDATIONS];
const checkValidation = (prevValidations: Validation[], index: number, state: boolean) => {
const updatedValidations = [...prevValidations];
updatedValidations[index].state = state;
return updatedValidations;
};
if (password !== null) {
newValidations = checkValidation(newValidations, 0, PASSWORD_REGEX.UPPER_AND_LOWER.test(password));
newValidations = checkValidation(newValidations, 1, password.length >= 8 && password.length <= 128);
newValidations = checkValidation(newValidations, 2, PASSWORD_REGEX.NUMBER.test(password));
}
setIsValid(newValidations.every((validation) => validation.state === true));
setValidations(newValidations);
}, [password, setIsValid]);
const renderIcon = (state: boolean) => {
if (state === false) {
return (
<span className="flex h-5 w-5 items-center justify-center">
<i className="inline-block h-2 w-2 rounded-full bg-slate-700"></i>
</span>
);
} else {
return <CheckIcon className="h-5 w-5" />;
}
};
return (
<div className="my-2 text-left text-slate-700 sm:text-sm">
<ul>
{validations.map((validation, index) => (
<li key={index}>
<div className="flex items-center">
{renderIcon(validation.state)}
{t(validation.label)}
</div>
</li>
))}
</ul>
</div>
);
};
@@ -1,47 +0,0 @@
import { Button } from "@/modules/ui/components/button";
import { signIn } from "next-auth/react";
import { useTranslations } from "next-intl";
import { useCallback, useEffect } from "react";
import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage";
export const OpenIdButton = ({
text = "Continue with OpenId Connect",
inviteUrl,
directRedirect = false,
lastUsed,
}: {
text?: string;
inviteUrl?: string | null;
directRedirect?: boolean;
lastUsed?: boolean;
}) => {
const t = useTranslations();
const handleLogin = useCallback(async () => {
if (typeof window !== "undefined") {
localStorage.setItem(FORMBRICKS_LOGGED_IN_WITH_LS, "OpenID");
}
await signIn("openid", {
redirect: true,
callbackUrl: inviteUrl ? inviteUrl : "/",
});
}, [inviteUrl]);
useEffect(() => {
if (directRedirect) {
handleLogin();
}
}, [directRedirect, handleLogin]);
return (
<Button
size="base"
type="button"
startIconClassName="ml-2"
onClick={handleLogin}
variant="secondary"
className="relative w-full justify-center">
{text}
{lastUsed && <span className="absolute right-3 text-xs opacity-50">{t("auth.last_used")}</span>}
</Button>
);
};
@@ -1,233 +0,0 @@
"use client";
import { createEmailTokenAction } from "@/modules/auth/components/SignupOptions/actions";
import { AzureButton } from "@/modules/auth/components/SignupOptions/components/AzureButton";
import { GithubButton } from "@/modules/auth/components/SignupOptions/components/GithubButton";
import { GoogleButton } from "@/modules/auth/components/SignupOptions/components/GoogleButton";
import { IsPasswordValid } from "@/modules/auth/components/SignupOptions/components/IsPasswordValid";
import { OpenIdButton } from "@/modules/auth/components/SignupOptions/components/OpenIdButton";
import { Button } from "@/modules/ui/components/button";
import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form";
import { Input } from "@/modules/ui/components/input";
import { PasswordInput } from "@/modules/ui/components/password-input";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useRef, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { z } from "zod";
import { createUser } from "@formbricks/lib/utils/users";
import { ZUserName } from "@formbricks/types/user";
interface SignupOptionsProps {
emailAuthEnabled: boolean;
emailFromSearchParams: string;
setError?: (error: string) => void;
emailVerificationDisabled: boolean;
googleOAuthEnabled: boolean;
githubOAuthEnabled: boolean;
azureOAuthEnabled: boolean;
oidcOAuthEnabled: boolean;
inviteToken: string | null;
callbackUrl: string;
oidcDisplayName?: string;
userLocale: string;
}
export const SignupOptions = ({
emailAuthEnabled,
emailFromSearchParams,
setError,
emailVerificationDisabled,
googleOAuthEnabled,
githubOAuthEnabled,
azureOAuthEnabled,
oidcOAuthEnabled,
inviteToken,
callbackUrl,
oidcDisplayName,
userLocale,
}: SignupOptionsProps) => {
const [showLogin, setShowLogin] = useState(false);
const [isValid, setIsValid] = useState(false);
const [signingUp, setSigningUp] = useState(false);
const t = useTranslations();
const ZSignupInput = z.object({
name: ZUserName,
email: z.string().email(),
password: z
.string()
.min(8)
.regex(/^(?=.*[A-Z])(?=.*\d).*$/),
});
type TSignupInput = z.infer<typeof ZSignupInput>;
const form = useForm<TSignupInput>({
defaultValues: {
name: "",
email: emailFromSearchParams || "",
password: "",
},
resolver: zodResolver(ZSignupInput),
});
const router = useRouter();
const nameRef = useRef<HTMLInputElement>(null);
const handleSubmit = async (data: TSignupInput) => {
if (!isValid) {
return;
}
setSigningUp(true);
try {
await createUser(data.name, data.email, data.password, userLocale, inviteToken || "");
const emailTokenActionResponse = await createEmailTokenAction({ email: data.email });
if (emailTokenActionResponse?.serverError) {
toast.error(emailTokenActionResponse.serverError);
return;
}
const token = emailTokenActionResponse?.data;
const url = emailVerificationDisabled
? `/auth/signup-without-verification-success`
: `/auth/verification-requested?token=${token}`;
router.push(url);
} catch (e: any) {
if (setError) {
setError(e.message);
}
setSigningUp(false);
}
};
return (
<div className="space-y-2">
{emailAuthEnabled && (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)}>
{showLogin && (
<div>
<div className="space-y-2">
<FormField
control={form.control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full">
<FormControl>
<div>
<Input
value={field.value}
name="name"
autoFocus
onChange={(name) => field.onChange(name)}
placeholder="Full name"
className="bg-white"
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full">
<FormControl>
<div>
<Input
value={field.value}
name="email"
onChange={(email) => field.onChange(email)}
defaultValue={emailFromSearchParams}
placeholder="work@email.com"
className="bg-white"
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full">
<FormControl>
<div>
<PasswordInput
id="password"
name="password"
value={field.value}
onChange={(password) => field.onChange(password)}
autoComplete="current-password"
placeholder="*******"
aria-placeholder="password"
required
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md shadow-sm sm:text-sm"
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
</FormControl>
</FormItem>
)}
/>
</div>
<IsPasswordValid password={form.watch("password")} setIsValid={setIsValid} />
</div>
)}
{showLogin && (
<Button
type="submit"
className="h-10 w-full justify-center"
loading={signingUp}
disabled={!form.formState.isValid}>
{t("auth.continue_with_email")}
</Button>
)}
{!showLogin && (
<Button
type="button"
onClick={() => {
setShowLogin(true);
// Add a slight delay before focusing the input field to ensure it's visible
setTimeout(() => nameRef.current?.focus(), 100);
}}
className="h-10 w-full justify-center">
{t("auth.continue_with_email")}
</Button>
)}
</form>
</FormProvider>
)}
{googleOAuthEnabled && (
<>
<GoogleButton inviteUrl={callbackUrl} text={t("auth.continue_with_google")} />
</>
)}
{githubOAuthEnabled && (
<>
<GithubButton inviteUrl={callbackUrl} text={t("auth.continue_with_github")} />
</>
)}
{azureOAuthEnabled && (
<>
<AzureButton inviteUrl={callbackUrl} text={t("auth.continue_with_azure")} />
</>
)}
{oidcOAuthEnabled && (
<>
<OpenIdButton inviteUrl={callbackUrl} text={t("auth.continue_with_oidc", { oidcDisplayName })} />
</>
)}
</div>
);
};
@@ -0,0 +1,11 @@
import { Button } from "@/modules/ui/components/button";
import { getTranslations } from "next-intl/server";
export const BackToLoginButton = async () => {
const t = await getTranslations();
return (
<Button size="base" variant="secondary" href="/auth/login" className="w-full justify-center">
{t("auth.signup.log_in")}
</Button>
);
};
@@ -0,0 +1,18 @@
import { Logo } from "@/modules/ui/components/logo";
interface FormWrapperProps {
children: React.ReactNode;
}
export const FormWrapper = ({ children }: FormWrapperProps) => {
return (
<div className="mx-auto flex flex-1 flex-col justify-center px-4 py-12 sm:px-6 lg:flex-none lg:px-20 xl:px-24">
<div className="mx-auto w-full max-w-sm rounded-xl bg-white p-8 shadow-xl lg:w-96">
<div className="mb-8 text-center">
<Logo className="mx-auto w-3/4" />
</div>
{children}
</div>
</div>
);
};
@@ -0,0 +1,51 @@
import CalComLogo from "@/images/cal-logo-light.svg";
import Peer from "@/images/peer.webp";
import { CheckCircle2Icon } from "lucide-react";
import { getTranslations } from "next-intl/server";
import Image from "next/image";
export const Testimonial = async () => {
const t = await getTranslations();
return (
<div className="flex flex-col items-center justify-center bg-gradient-to-tr from-slate-100 to-slate-300">
<div className="3xl:w-2/3 mb-10 space-y-8 px-12 xl:px-20">
<div>
<h2 className="text-3xl font-bold text-slate-800">{t("auth.testimonial_title")}</h2>
</div>
{/* <p className="text-slate-600">
Make customer-centric decisions based on data.
<br /> Keep 100% data ownership.
</p> */}
<div className="space-y-2">
<div className="flex space-x-2">
<CheckCircle2Icon className="text-brand-dark h-6 w-6" />
<p className="inline text-lg text-slate-800">{t("auth.testimonial_all_features_included")}</p>
</div>
<div className="flex space-x-2">
<CheckCircle2Icon className="text-brand-dark h-6 w-6" />
<p className="inline text-lg text-slate-800">{t("auth.testimonial_free_and_open_source")}</p>
</div>
<div className="flex space-x-2">
<CheckCircle2Icon className="text-brand-dark h-6 w-6" />
<p className="inline text-lg text-slate-800">{t("auth.testimonial_no_credit_card_required")}</p>
</div>
</div>
<div className="rounded-xl border border-slate-200 bg-gradient-to-tr from-slate-100 to-slate-200 p-8">
<p className="italic text-slate-700">{t("auth.testimonial_1")}</p>
<div className="mt-4 flex items-center space-x-6">
<Image
src={Peer}
alt="Cal.com Co-Founder Peer Richelsen"
className="h-28 w-28 rounded-full border border-slate-200 shadow-sm"
/>
<div>
<p className="mb-1.5 text-sm text-slate-500">Peer Richelsen, Co-Founder Cal.com</p>
<Image src={CalComLogo} alt="Cal.com Logo" />
</div>
</div>
</div>
</div>
</div>
);
};