fix: security checks for user input fields for email (#3819)

This commit is contained in:
Dhruwang Jariwala
2024-11-12 15:31:21 +05:30
committed by GitHub
parent da6f54eede
commit e7edfe3ba1
12 changed files with 348 additions and 180 deletions

View File

@@ -19,7 +19,11 @@ interface InviteOrganizationMemberProps {
const ZInviteOrganizationMemberDetails = z.object({
email: z.string().email(),
inviteMessage: z.string().trim().min(1),
inviteMessage: z
.string()
.trim()
.min(1)
.refine((value) => !/https?:\/\/|<script/i.test(value), "Invite message cannot contain URLs or scripts"),
});
type TInviteOrganizationMemberDetails = z.infer<typeof ZInviteOrganizationMemberDetails>;

View File

@@ -1,10 +1,13 @@
"use client";
import { AddMemberRole } from "@/modules/ee/role-management/components/add-member-role";
import { zodResolver } from "@hookform/resolvers/zod";
import { OrganizationRole } from "@prisma/client";
import { useTranslations } from "next-intl";
import { useForm } from "react-hook-form";
import { TOrganizationRole } from "@formbricks/types/memberships";
import { z } from "zod";
import { TOrganizationRole, ZOrganizationRole } from "@formbricks/types/memberships";
import { ZUserName } from "@formbricks/types/user";
import { Alert, AlertDescription } from "@formbricks/ui/components/Alert";
import { Button } from "@formbricks/ui/components/Button";
import { Input } from "@formbricks/ui/components/Input";
@@ -18,6 +21,7 @@ interface IndividualInviteTabProps {
isFormbricksCloud: boolean;
environmentId: string;
}
export const IndividualInviteTab = ({
setOpen,
onSubmit,
@@ -25,6 +29,13 @@ export const IndividualInviteTab = ({
isFormbricksCloud,
environmentId,
}: IndividualInviteTabProps) => {
const ZFormSchema = z.object({
name: ZUserName,
email: z.string().email("Invalid email address"),
role: ZOrganizationRole,
});
type TFormData = z.infer<typeof ZFormSchema>;
const t = useTranslations();
const {
register,
@@ -33,12 +44,13 @@ export const IndividualInviteTab = ({
reset,
control,
watch,
formState: { isSubmitting },
} = useForm<{
name: string;
email: string;
role: TOrganizationRole;
}>();
formState: { isSubmitting, errors },
} = useForm<TFormData>({
resolver: zodResolver(ZFormSchema),
defaultValues: {
role: "owner",
},
});
const submitEventClass = async () => {
const data = getValues();
@@ -55,9 +67,10 @@ export const IndividualInviteTab = ({
<Label htmlFor="memberNameInput">{t("common.full_name")}</Label>
<Input
id="memberNameInput"
placeholder="e.g. Hans Wurst"
placeholder="Hans Wurst"
{...register("name", { required: true, validate: (value) => value.trim() !== "" })}
/>
{errors.name && <p className="mt-1 text-sm text-red-500">{errors.name.message}</p>}
</div>
<div>
<Label htmlFor="memberEmailInput">{t("common.email")}</Label>

View File

@@ -2,16 +2,19 @@
import { TwoFactor } from "@/app/(auth)/auth/login/components/TwoFactor";
import { TwoFactorBackup } from "@/app/(auth)/auth/login/components/TwoFactorBackup";
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 { Controller, FormProvider, SubmitHandler, useForm } from "react-hook-form";
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";
import { Button } from "@formbricks/ui/components/Button";
import { FormControl, FormError, FormField, FormItem } from "@formbricks/ui/components/Form";
import { PasswordInput } from "@formbricks/ui/components/PasswordInput";
import { AzureButton } from "@formbricks/ui/components/SignupOptions/components/AzureButton";
import { GithubButton } from "@formbricks/ui/components/SignupOptions/components/GithubButton";
@@ -53,6 +56,23 @@ export const SigninForm = ({
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);
@@ -137,11 +157,11 @@ export const SigninForm = ({
const TwoFactorComponent = useMemo(() => {
if (totpBackup) {
return <TwoFactorBackup />;
return <TwoFactorBackup form={form} />;
}
if (totpLogin) {
return <TwoFactor />;
return <TwoFactor form={form} />;
}
return null;
@@ -153,53 +173,58 @@ export const SigninForm = ({
<h1 className="mb-4 text-slate-700">{formLabel}</h1>
<div className="space-y-2">
<form onSubmit={formMethods.handleSubmit(onSubmit)} className="space-y-2">
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
{TwoFactorComponent}
{showLogin && (
<div className={cn(totpLogin && "hidden")}>
<div className="mb-2 transition-all duration-500 ease-in-out">
<label htmlFor="email" className="sr-only">
{t("common.email")}
</label>
<input
id="email"
type="email"
autoComplete="email"
required
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"
{...formMethods.register("email", {
required: true,
pattern: /\S+@\S+\.\S+/,
})}
/>
</div>
<div className="transition-all duration-500 ease-in-out">
<label htmlFor="password" className="sr-only">
{t("common.password")}
</label>
<Controller
name="password"
control={formMethods.control}
render={({ field }) => (
<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"
{...field}
/>
)}
rules={{
required: true,
}}
/>
</div>
<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

View File

@@ -2,11 +2,24 @@
import { useTranslations } from "next-intl";
import React from "react";
import { Controller, useFormContext } from "react-hook-form";
import { UseFormReturn } from "react-hook-form";
import { FormControl, FormField, FormItem } from "@formbricks/ui/components/Form";
import { OTPInput } from "@formbricks/ui/components/OTPInput";
export const TwoFactor = () => {
const { control } = useFormContext();
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 (
@@ -16,11 +29,15 @@ export const TwoFactor = () => {
{t("auth.login.enter_your_two_factor_authentication_code")}
</label>
<Controller
control={control}
<FormField
control={form.control}
name="totpCode"
render={({ field }) => (
<OTPInput value={field.value ?? ""} onChange={field.onChange} valueLength={6} />
<FormItem className="w-full">
<FormControl>
<OTPInput value={field.value ?? ""} onChange={field.onChange} valueLength={6} />
</FormControl>
</FormItem>
)}
/>
</div>

View File

@@ -2,11 +2,25 @@
import { useTranslations } from "next-intl";
import React from "react";
import { useFormContext } from "react-hook-form";
import { UseFormReturn } from "react-hook-form";
import { FormField, FormItem } from "@formbricks/ui/components/Form";
import { FormControl } from "@formbricks/ui/components/Form";
import { Input } from "@formbricks/ui/components/Input";
export const TwoFactorBackup = () => {
const { register } = useFormContext();
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 (
@@ -15,12 +29,23 @@ export const TwoFactorBackup = () => {
<label htmlFor="totpBackup" className="sr-only">
{t("auth.login.backup_code")}
</label>
<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"
{...register("backupCode")}
<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>
</>

View File

@@ -15,6 +15,7 @@ import {
DEFAULT_ORGANIZATION_ID,
DEFAULT_ORGANIZATION_ROLE,
EMAIL_VERIFICATION_DISABLED,
ENCRYPTION_KEY,
GITHUB_ID,
GITHUB_SECRET,
GOOGLE_CLIENT_ID,
@@ -25,6 +26,7 @@ import {
OIDC_ISSUER,
OIDC_SIGNING_ALGORITHM,
} from "./constants";
import { symmetricDecrypt, symmetricEncrypt } from "./crypto";
import { verifyToken } from "./jwt";
import { createMembership } from "./membership/service";
import { createOrganization, getOrganization } from "./organization/service";
@@ -52,6 +54,8 @@ export const authOptions: NextAuthOptions = {
type: "password",
placeholder: "Your password",
},
totpCode: { label: "Two-factor Code", type: "input", placeholder: "Code from authenticator app" },
backupCode: { label: "Backup Code", type: "input", placeholder: "Two-factor backup code" },
},
async authorize(credentials, _req) {
let user;
@@ -79,6 +83,54 @@ export const authOptions: NextAuthOptions = {
throw new Error("Invalid credentials");
}
if (user.twoFactorEnabled && credentials.backupCode) {
if (!ENCRYPTION_KEY) {
console.error("Missing encryption key; cannot proceed with backup code login.");
throw new Error("Internal Server Error");
}
if (!user.backupCodes) throw new Error("No backup codes found");
const backupCodes = JSON.parse(symmetricDecrypt(user.backupCodes, ENCRYPTION_KEY));
// check if user-supplied code matches one
const index = backupCodes.indexOf(credentials.backupCode.replaceAll("-", ""));
if (index === -1) throw new Error("Invalid backup code");
// delete verified backup code and re-encrypt remaining
backupCodes[index] = null;
await prisma.user.update({
where: {
id: user.id,
},
data: {
backupCodes: symmetricEncrypt(JSON.stringify(backupCodes), ENCRYPTION_KEY),
},
});
} else if (user.twoFactorEnabled) {
if (!credentials.totpCode) {
throw new Error("second factor required");
}
if (!user.twoFactorSecret) {
throw new Error("Internal Server Error");
}
if (!ENCRYPTION_KEY) {
throw new Error("Internal Server Error");
}
const secret = symmetricDecrypt(user.twoFactorSecret, ENCRYPTION_KEY);
if (secret.length !== 32) {
throw new Error("Internal Server Error");
}
const isValidToken = (await import("./totp")).totpAuthenticatorCheck(credentials.totpCode, secret);
if (!isValidToken) {
throw new Error("Invalid second factor code");
}
}
return {
id: user.id,
email: user.email,

View File

@@ -350,6 +350,7 @@
"status": "Status",
"step_by_step_manual": "Schritt-für-Schritt-Anleitung",
"styling": "Styling",
"submit": "Abschicken",
"summary": "Zusammenfassung",
"survey": "Umfrage",
"survey_completed": "Umfrage abgeschlossen.",
@@ -1133,6 +1134,7 @@
"disable_two_factor_authentication": "Zwei-Faktor-Authentifizierung deaktivieren",
"disable_two_factor_authentication_description": "Wenn Du die Zwei-Faktor-Authentifizierung deaktivieren musst, empfehlen wir, sie so schnell wie möglich wieder zu aktivieren.",
"each_backup_code_can_be_used_exactly_once_to_grant_access_without_your_authenticator": "Jeder Backup-Code kann genau einmal verwendet werden, um Zugang ohne deinen Authenticator zu gewähren.",
"enable_two_factor_authentication": "Zwei-Faktor-Authentifizierung aktivieren",
"enter_the_code_from_your_authenticator_app_below": "Gib den Code aus deiner Authentifizierungs-App unten ein.",
"file_size_must_be_less_than_10mb": "Dateigröße muss weniger als 10MB sein.",
"invalid_file_type": "Ungültiger Dateityp. Nur JPEG-, PNG- und WEBP-Dateien sind erlaubt.",

View File

@@ -350,6 +350,7 @@
"status": "Status",
"step_by_step_manual": "Step by step manual",
"styling": "Styling",
"submit": "Submit",
"summary": "Summary",
"survey": "Survey",
"survey_completed": "Survey completed.",
@@ -1133,6 +1134,7 @@
"disable_two_factor_authentication": "Disable two factor authentication",
"disable_two_factor_authentication_description": "If you need to disable 2FA, we recommend re-enabling it as soon as possible.",
"each_backup_code_can_be_used_exactly_once_to_grant_access_without_your_authenticator": "Each backup code can be used exactly once to grant access without your authenticator.",
"enable_two_factor_authentication": "Enable two factor authentication",
"enter_the_code_from_your_authenticator_app_below": "Enter the code from your authenticator app below.",
"file_size_must_be_less_than_10mb": "File size must be less than 10MB.",
"invalid_file_type": "Invalid file type. Only JPEG, PNG, and WEBP files are allowed.",

View File

@@ -350,6 +350,7 @@
"status": "status",
"step_by_step_manual": "Manual passo a passo",
"styling": "estilização",
"submit": "Enviar",
"summary": "Resumo",
"survey": "Pesquisa",
"survey_completed": "Pesquisa concluída.",
@@ -1133,6 +1134,7 @@
"disable_two_factor_authentication": "Desativar a autenticação de dois fatores",
"disable_two_factor_authentication_description": "Se você precisar desativar a 2FA, recomendamos reativá-la o mais rápido possível.",
"each_backup_code_can_be_used_exactly_once_to_grant_access_without_your_authenticator": "Cada código de backup pode ser usado exatamente uma vez para conceder acesso sem o seu autenticador.",
"enable_two_factor_authentication": "Ativar autenticação de dois fatores",
"enter_the_code_from_your_authenticator_app_below": "Digite o código do seu app autenticador abaixo.",
"file_size_must_be_less_than_10mb": "O tamanho do arquivo deve ser menor que 10MB.",
"invalid_file_type": "Tipo de arquivo inválido. Só são permitidos arquivos JPEG, PNG e WEBP.",

View File

@@ -1,5 +1,6 @@
import { z } from "zod";
import { ZOrganizationRole } from "./memberships";
import { ZUserName } from "./user";
export const ZInvite = z.object({
id: z.string(),
@@ -16,7 +17,7 @@ export type TInvite = z.infer<typeof ZInvite>;
export const ZInvitee = z.object({
email: z.string().email(),
name: z.string(),
name: ZUserName,
role: ZOrganizationRole,
});
export type TInvitee = z.infer<typeof ZInvitee>;

View File

@@ -22,14 +22,17 @@ export const ZUserNotificationSettings = z.object({
unsubscribedOrganizationIds: z.array(z.string()).optional(),
});
export const ZUserName = z
.string()
.trim()
.min(1, { message: "Name should be at least 1 character long" })
.regex(/^[a-zA-Z0-9\s]+$/, { message: "Name should only contain letters, numbers, and spaces" });
export type TUserNotificationSettings = z.infer<typeof ZUserNotificationSettings>;
export const ZUser = z.object({
id: z.string(),
name: z
.string({ message: "Name is required" })
.trim()
.min(1, { message: "Name should be at least 1 character long" }),
name: ZUserName,
email: z.string().email(),
emailVerified: z.date().nullable(),
imageUrl: z.string().url().nullable(),
@@ -46,7 +49,7 @@ export const ZUser = z.object({
export type TUser = z.infer<typeof ZUser>;
export const ZUserUpdateInput = z.object({
name: z.string().optional(),
name: ZUserName.optional(),
email: z.string().email().optional(),
emailVerified: z.date().nullish(),
role: ZRole.optional(),
@@ -59,10 +62,7 @@ export const ZUserUpdateInput = z.object({
export type TUserUpdateInput = z.infer<typeof ZUserUpdateInput>;
export const ZUserCreateInput = z.object({
name: z
.string({ message: "Name is required" })
.trim()
.min(1, { message: "Name should be at least 1 character long" }),
name: ZUserName,
email: z.string().email(),
emailVerified: z.date().optional(),
role: ZRole.optional(),

View File

@@ -1,10 +1,16 @@
"use client";
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 { z } from "zod";
import { createUser } from "@formbricks/lib/utils/users";
import { ZUserName } from "@formbricks/types/user";
import { Button } from "../Button";
import { FormControl, FormError, FormField, FormItem } from "../Form";
import { Input } from "../Input";
import { PasswordInput } from "../PasswordInput";
import { AzureButton } from "./components/AzureButton";
import { GithubButton } from "./components/GithubButton";
@@ -41,28 +47,35 @@ export const SignupOptions = ({
oidcDisplayName,
userLocale,
}: SignupOptionsProps) => {
const t = useTranslations();
const [password, setPassword] = useState<string | null>(null);
const [showLogin, setShowLogin] = useState(false);
const [isValid, setIsValid] = useState(false);
const [signingUp, setSigningUp] = useState(false);
const [isButtonEnabled, setButtonEnabled] = useState(true);
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 formRef = useRef<HTMLFormElement>(null);
const nameRef = useRef<HTMLInputElement>(null);
const checkFormValidity = () => {
// If all fields are filled, enable the button
if (formRef.current) {
setButtonEnabled(formRef.current.checkValidity());
}
};
const handleSubmit = async (e: any) => {
e.preventDefault();
const handleSubmit = async (data: TSignupInput) => {
if (!isValid) {
return;
}
@@ -70,16 +83,10 @@ export const SignupOptions = ({
setSigningUp(true);
try {
await createUser(
e.target.elements.name.value,
e.target.elements.email.value,
e.target.elements.password.value,
userLocale,
inviteToken || ""
);
await createUser(data.name, data.email, data.password, userLocale, inviteToken || "");
const url = emailVerificationDisabled
? `/auth/signup-without-verification-success`
: `/auth/verification-requested?email=${encodeURIComponent(e.target.elements.email.value)}`;
: `/auth/verification-requested?email=${encodeURIComponent(data.email)}`;
router.push(url);
} catch (e: any) {
@@ -93,87 +100,105 @@ export const SignupOptions = ({
return (
<div className="space-y-2">
{emailAuthEnabled && (
<form onSubmit={handleSubmit} ref={formRef} className="space-y-2" onChange={checkFormValidity}>
{showLogin && (
<div>
<div className="mb-2 transition-all duration-500 ease-in-out">
<label htmlFor="name" className="sr-only">
{t("common.full_name")}
</label>
<div className="mt-1">
<input
ref={nameRef}
id="name"
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)}>
{showLogin && (
<div>
<div className="space-y-2">
<FormField
control={form.control}
name="name"
type="text"
autoComplete="given-name"
placeholder={t("common.full_name")}
aria-placeholder={"Full name"}
required
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
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>
<div className="mb-2 transition-all duration-500 ease-in-out">
<label htmlFor="email" className="sr-only">
{t("common.email")}
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
placeholder="work@email.com"
defaultValue={emailFromSearchParams}
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
/>
</div>
<div className="transition-all duration-500 ease-in-out">
<label htmlFor="password" className="sr-only">
{t("common.password")}
</label>
<PasswordInput
id="password"
name="password"
value={password ? password : ""}
onChange={(e) => setPassword(e.target.value)}
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"
/>
</div>
<IsPasswordValid password={password} setIsValid={setIsValid} />
</div>
)}
{showLogin && (
<Button
size="base"
type="submit"
className="w-full justify-center"
loading={signingUp}
disabled={formRef.current ? !isButtonEnabled || !isValid : !isButtonEnabled}>
{t("auth.continue_with_email")}
</Button>
)}
)}
{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
size="base"
type="button"
onClick={() => {
setShowLogin(true);
setButtonEnabled(false);
// Add a slight delay before focusing the input field to ensure it's visible
setTimeout(() => nameRef.current?.focus(), 100);
}}
className="w-full justify-center">
{t("auth.continue_with_email")}
</Button>
)}
</form>
{!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 && (
<>