diff --git a/apps/web/components/setup/AdminUser.tsx b/apps/web/components/setup/AdminUser.tsx index 9d50b1f87a..4dce0f9934 100644 --- a/apps/web/components/setup/AdminUser.tsx +++ b/apps/web/components/setup/AdminUser.tsx @@ -57,12 +57,10 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on }), }); - const formMethods = useForm<{ - username: string; - email_address: string; - full_name: string; - password: string; - }>({ + type formSchemaType = z.infer; + + const formMethods = useForm({ + mode: "onChange", resolver: zodResolver(formSchema), }); @@ -70,7 +68,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on props.onError(); }; - const onSubmit = formMethods.handleSubmit(async (data: z.infer) => { + const onSubmit = formMethods.handleSubmit(async (data) => { props.onSubmit(); const response = await fetch("/api/auth/setup", { method: "POST", @@ -130,11 +128,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on className={classNames("my-0", longWebsiteUrl && "rounded-t-none")} onBlur={onBlur} name="username" - onChange={async (e) => { - onChange(e.target.value); - formMethods.setValue("username", e.target.value); - await formMethods.trigger("username"); - }} + onChange={(e) => onChange(e.target.value)} /> )} @@ -148,11 +142,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on { - onChange(e.target.value); - formMethods.setValue("full_name", e.target.value); - await formMethods.trigger("full_name"); - }} + onChange={(e) => onChange(e.target.value)} color={formMethods.formState.errors.full_name ? "warn" : ""} type="text" name="full_name" @@ -172,11 +162,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on { - onChange(e.target.value); - formMethods.setValue("email_address", e.target.value); - await formMethods.trigger("email_address"); - }} + onChange={(e) => onChange(e.target.value)} className="my-0" name="email_address" /> @@ -191,11 +177,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on { - onChange(e.target.value); - formMethods.setValue("password", e.target.value); - await formMethods.trigger("password"); - }} + onChange={(e) => onChange(e.target.value)} hintErrors={["caplow", "admin_min", "num"]} name="password" className="my-0" diff --git a/apps/web/pages/api/auth/signup.ts b/apps/web/pages/api/auth/signup.ts index f53421fb9c..f44c231fb3 100644 --- a/apps/web/pages/api/auth/signup.ts +++ b/apps/web/pages/api/auth/signup.ts @@ -1,5 +1,4 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import { z } from "zod"; import dayjs from "@calcom/dayjs"; import { checkPremiumUsername } from "@calcom/ee/common/lib/checkPremiumUsername"; @@ -11,18 +10,9 @@ import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager"; import { validateUsernameInTeam, validateUsername } from "@calcom/lib/validateUsername"; import prisma from "@calcom/prisma"; import { IdentityProvider } from "@calcom/prisma/enums"; +import { signupSchema } from "@calcom/prisma/zod-utils"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; -const signupSchema = z.object({ - username: z.string().refine((value) => !value.includes("+"), { - message: "String should not contain a plus symbol (+).", - }), - email: z.string().email(), - password: z.string().min(7), - language: z.string().optional(), - token: z.string().optional(), -}); - export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== "POST") { return res.status(405).end(); diff --git a/apps/web/pages/signup.tsx b/apps/web/pages/signup.tsx index ea0c0a0974..c41025bc99 100644 --- a/apps/web/pages/signup.tsx +++ b/apps/web/pages/signup.tsx @@ -17,6 +17,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import slugify from "@calcom/lib/slugify"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; +import { signupSchema as apiSignupSchema } from "@calcom/prisma/zod-utils"; import type { inferSSRProps } from "@calcom/types/inferSSRProps"; import { Alert, Button, EmailField, HeadSeo, PasswordField, TextField } from "@calcom/ui"; @@ -25,14 +26,7 @@ import PageWrapper from "@components/PageWrapper"; import { IS_GOOGLE_LOGIN_ENABLED } from "../server/lib/constants"; import { ssrInit } from "../server/lib/ssr"; -const signupSchema = z.object({ - username: z.string().refine((value) => !value.includes("+"), { - message: "String should not contain a plus symbol (+).", - }), - email: z.string().email(), - password: z.string().min(7), - language: z.string().optional(), - token: z.string().optional(), +const signupSchema = apiSignupSchema.extend({ apiError: z.string().optional(), // Needed to display API errors doesnt get passed to the API }); @@ -46,6 +40,7 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup const { t, i18n } = useLocale(); const flags = useFlagMap(); const methods = useForm({ + mode: "onChange", resolver: zodResolver(signupSchema), defaultValues: prepopulateFormValues, }); diff --git a/packages/features/auth/lib/isPasswordValid.ts b/packages/features/auth/lib/isPasswordValid.ts index 95f431a5f7..1e56d84ba1 100644 --- a/packages/features/auth/lib/isPasswordValid.ts +++ b/packages/features/auth/lib/isPasswordValid.ts @@ -10,15 +10,11 @@ export function isPasswordValid(password: string, breakdown?: boolean, strict?: num = false, // At least one number min = false, // Eight characters, or fifteen in strict mode. admin_min = false; - if (password.length > 7 && (!strict || password.length > 14)) min = true; + if (password.length >= 7 && (!strict || password.length > 14)) min = true; if (strict && password.length > 14) admin_min = true; - for (let i = 0; i < password.length; i++) { - if (!isNaN(parseInt(password[i]))) num = true; - else { - if (password[i] === password[i].toUpperCase()) cap = true; - if (password[i] === password[i].toLowerCase()) low = true; - } - } + if (password.match(/\d/)) num = true; + if (password.match(/[a-z]/)) low = true; + if (password.match(/[A-Z]/)) cap = true; if (!breakdown) return cap && low && num && min && (strict ? admin_min : true); diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index 1f36ebdd86..d8d67a5525 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -14,6 +14,7 @@ import type { import { appDataSchemas } from "@calcom/app-store/apps.schemas.generated"; import dayjs from "@calcom/dayjs"; +import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid"; import type { FieldType as FormBuilderFieldType } from "@calcom/features/form-builder/schema"; import { fieldsSchema as formBuilderFieldsSchema } from "@calcom/features/form-builder/schema"; import { isSupportedTimeZone } from "@calcom/lib/date-fns"; @@ -602,6 +603,28 @@ export const emailSchemaRefinement = (value: string) => { return emailRegex.test(value); }; +export const signupSchema = z.object({ + username: z.string().refine((value) => !value.includes("+"), { + message: "String should not contain a plus symbol (+).", + }), + email: z.string().email(), + password: z.string().superRefine((data, ctx) => { + const isStrict = false; + const result = isPasswordValid(data, true, isStrict); + Object.keys(result).map((key: string) => { + if (!result[key as keyof typeof result]) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [key], + message: key, + }); + } + }); + }), + language: z.string().optional(), + token: z.string().optional(), +}); + export const ZVerifyCodeInputSchema = z.object({ email: z.string().email(), code: z.string(), @@ -610,3 +633,4 @@ export const ZVerifyCodeInputSchema = z.object({ export type ZVerifyCodeInputSchema = z.infer; export const coerceToDate = z.coerce.date(); + diff --git a/packages/ui/components/form/inputs/HintOrErrors.tsx b/packages/ui/components/form/inputs/HintOrErrors.tsx index be2b5a8e86..a2115f7c56 100644 --- a/packages/ui/components/form/inputs/HintOrErrors.tsx +++ b/packages/ui/components/form/inputs/HintOrErrors.tsx @@ -3,17 +3,21 @@ import { useFormContext } from "react-hook-form"; import { Check, Circle, Info, X } from "../../icon"; -export function HintsOrErrors(props: { +type hintsOrErrorsProps = { hintErrors?: string[]; fieldName: string; t: (key: string) => string; -}) { +}; + +export function HintsOrErrors({ + hintErrors, + fieldName, + t, +}: hintsOrErrorsProps) { const methods = useFormContext() as ReturnType | null; /* If there's no methods it means we're using these components outside a React Hook Form context */ if (!methods) return null; const { formState } = methods; - const { hintErrors, fieldName, t } = props; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const fieldErrors: FieldErrors | undefined = formState.errors[fieldName];