diff --git a/apps/web/app/api/auth/reset-password/route.ts b/apps/web/app/api/auth/reset-password/route.ts index fc1a745bd1..57b761bf65 100644 --- a/apps/web/app/api/auth/reset-password/route.ts +++ b/apps/web/app/api/auth/reset-password/route.ts @@ -4,8 +4,8 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { z } from "zod"; -import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { validPassword } from "@calcom/features/auth/lib/validPassword"; +import { hashPassword } from "@calcom/lib/auth/hashPassword"; import prisma from "@calcom/prisma"; import { IdentityProvider } from "@calcom/prisma/enums"; diff --git a/apps/web/app/api/auth/setup/route.ts b/apps/web/app/api/auth/setup/route.ts index 590d02b2d1..693b9597a7 100644 --- a/apps/web/app/api/auth/setup/route.ts +++ b/apps/web/app/api/auth/setup/route.ts @@ -4,8 +4,8 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import z from "zod"; -import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; -import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid"; +import { hashPassword } from "@calcom/lib/auth/hashPassword"; +import { isPasswordValid } from "@calcom/lib/auth/isPasswordValid"; import { emailRegex } from "@calcom/lib/emailSchema"; import { HttpError } from "@calcom/lib/http-error"; import slugify from "@calcom/lib/slugify"; diff --git a/apps/web/components/setup/AdminUser.tsx b/apps/web/components/setup/AdminUser.tsx index d5d3219625..fe9624863f 100644 --- a/apps/web/components/setup/AdminUser.tsx +++ b/apps/web/components/setup/AdminUser.tsx @@ -5,7 +5,7 @@ import React from "react"; import { Controller, FormProvider, useForm } from "react-hook-form"; import { z } from "zod"; -import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid"; +import { isPasswordValid } from "@calcom/lib/auth/isPasswordValid"; import { WEBSITE_URL } from "@calcom/lib/constants"; import { emailRegex } from "@calcom/lib/emailSchema"; import { useLocale } from "@calcom/lib/hooks/useLocale"; diff --git a/apps/web/lib/apps/installation/[[...step]]/getServerSideProps.ts b/apps/web/lib/apps/installation/[[...step]]/getServerSideProps.ts index bedebb0697..f168705014 100644 --- a/apps/web/lib/apps/installation/[[...step]]/getServerSideProps.ts +++ b/apps/web/lib/apps/installation/[[...step]]/getServerSideProps.ts @@ -2,13 +2,13 @@ import type { GetServerSidePropsContext } from "next"; import { z } from "zod"; import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; +import type { LocationObject } from "@calcom/app-store/locations"; import { isConferencing as isConferencingApp } from "@calcom/app-store/utils"; import { getLocale } from "@calcom/features/auth/lib/getLocale"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps"; import { CAL_URL } from "@calcom/lib/constants"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; -import type { LocationObject } from "@calcom/app-store/locations"; import { UserRepository } from "@calcom/lib/server/repository/user"; import prisma from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; diff --git a/package.json b/package.json index 50620cb561..0d2e4acb20 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "devDependencies": { "@changesets/changelog-github": "^0.5.1", "@changesets/cli": "2.29.4", + "@faker-js/faker": "9.2.0", "@jetstreamapp/soql-parser-js": "^6.1.0", "@playwright/test": "^1.45.3", "@snaplet/copycat": "^4.1.0", diff --git a/packages/features/auth/lib/next-auth-options.ts b/packages/features/auth/lib/next-auth-options.ts index 8783c3ad30..53fd456464 100644 --- a/packages/features/auth/lib/next-auth-options.ts +++ b/packages/features/auth/lib/next-auth-options.ts @@ -16,6 +16,7 @@ import createUsersAndConnectToOrg from "@calcom/features/ee/dsync/lib/users/crea import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider"; import { getOrgFullOrigin, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains"; import { clientSecretVerifier, hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml"; +import { isPasswordValid } from "@calcom/lib/auth/isPasswordValid"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import { GOOGLE_CALENDAR_SCOPES, @@ -46,7 +47,6 @@ import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils"; import { getOrgUsernameFromEmail } from "../signup/utils/getOrgUsernameFromEmail"; import { ErrorCode } from "./ErrorCode"; import { dub } from "./dub"; -import { isPasswordValid } from "./isPasswordValid"; import CalComAdapter from "./next-auth-custom-adapter"; import { verifyPassword } from "./verifyPassword"; diff --git a/packages/features/auth/signup/handlers/calcomHandler.ts b/packages/features/auth/signup/handlers/calcomHandler.ts index f1bce058f5..adecb8e14e 100644 --- a/packages/features/auth/signup/handlers/calcomHandler.ts +++ b/packages/features/auth/signup/handlers/calcomHandler.ts @@ -2,12 +2,12 @@ import { cookies, headers } from "next/headers"; import { NextResponse } from "next/server"; import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils"; -import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail"; import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/createOrUpdateMemberships"; import { prefillAvatar } from "@calcom/features/auth/signup/utils/prefillAvatar"; import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billling-service"; import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller"; +import { hashPassword } from "@calcom/lib/auth/hashPassword"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { getLocaleFromRequest } from "@calcom/lib/getLocaleFromRequest"; import { HttpError } from "@calcom/lib/http-error"; diff --git a/packages/features/auth/signup/handlers/selfHostedHandler.ts b/packages/features/auth/signup/handlers/selfHostedHandler.ts index 5da0c05c53..6b2028b2e8 100644 --- a/packages/features/auth/signup/handlers/selfHostedHandler.ts +++ b/packages/features/auth/signup/handlers/selfHostedHandler.ts @@ -1,9 +1,9 @@ import { NextResponse } from "next/server"; import { checkPremiumUsername } from "@calcom/ee/common/lib/checkPremiumUsername"; -import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail"; import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/createOrUpdateMemberships"; +import { hashPassword } from "@calcom/lib/auth/hashPassword"; import { IS_PREMIUM_USERNAME_ENABLED } from "@calcom/lib/constants"; import logger from "@calcom/lib/logger"; import { isUsernameReservedDueToMigration } from "@calcom/lib/server/username"; diff --git a/packages/features/eventtypes/components/tabs/advanced/EventAdvancedTab.tsx b/packages/features/eventtypes/components/tabs/advanced/EventAdvancedTab.tsx index 5c60372edc..faf9a7868e 100644 --- a/packages/features/eventtypes/components/tabs/advanced/EventAdvancedTab.tsx +++ b/packages/features/eventtypes/components/tabs/advanced/EventAdvancedTab.tsx @@ -3,6 +3,7 @@ import type { Dispatch, SetStateAction } from "react"; import { Controller, useFormContext } from "react-hook-form"; import type { z } from "zod"; +import { getPaymentAppData } from "@calcom/app-store/_utils/payments/getPaymentAppData"; import { useAtomsContext } from "@calcom/atoms/hooks/useAtomsContext"; import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform"; import { @@ -31,8 +32,6 @@ import type { SettingsToggleClassNames, } from "@calcom/features/eventtypes/lib/types"; import { FormBuilder } from "@calcom/features/form-builder/FormBuilder"; -import type { fieldSchema } from "@calcom/features/form-builder/schema"; -import type { EditableSchema } from "@calcom/features/form-builder/schema"; import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector"; import { DEFAULT_LIGHT_BRAND_COLOR, @@ -42,11 +41,12 @@ import { } from "@calcom/lib/constants"; import { generateHashedLink } from "@calcom/lib/generateHashedLink"; import { checkWCAGContrastColor } from "@calcom/lib/getBrandColours"; -import { getPaymentAppData } from "@calcom/app-store/_utils/payments/getPaymentAppData"; import { extractHostTimezone } from "@calcom/lib/hashedLinksUtils"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { Prisma } from "@calcom/prisma/client"; import { SchedulingType } from "@calcom/prisma/enums"; +import type { EditableSchema } from "@calcom/prisma/zod-utils"; +import type { fieldSchema } from "@calcom/prisma/zod-utils"; import type { RouterOutputs } from "@calcom/trpc/react"; import classNames from "@calcom/ui/classNames"; import { Alert } from "@calcom/ui/components/alert"; diff --git a/packages/features/form-builder/Components.tsx b/packages/features/form-builder/Components.tsx index cb774f6f0a..4a1a5bfd6c 100644 --- a/packages/features/form-builder/Components.tsx +++ b/packages/features/form-builder/Components.tsx @@ -8,6 +8,7 @@ import type { import Widgets from "@calcom/app-store/routing-forms/components/react-awesome-query-builder/widgets"; import PhoneInput from "@calcom/features/components/phone-input"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { fieldSchema, variantsConfigSchema, FieldType } from "@calcom/prisma/zod-utils"; import { AddressInput } from "@calcom/ui/components/address"; import { InfoBadge } from "@calcom/ui/components/badge"; import { Button } from "@calcom/ui/components/button"; @@ -18,7 +19,6 @@ import { Tooltip } from "@calcom/ui/components/tooltip"; import { ComponentForField } from "./FormBuilderField"; import { propsTypes } from "./propsTypes"; -import type { fieldSchema, FieldType, variantsConfigSchema } from "./schema"; import { preprocessNameFieldDataWithVariant } from "./utils"; export const isValidValueProp: Record boolean> = { diff --git a/packages/features/form-builder/FormBuilder.tsx b/packages/features/form-builder/FormBuilder.tsx index 26409aca03..d05e6e330f 100644 --- a/packages/features/form-builder/FormBuilder.tsx +++ b/packages/features/form-builder/FormBuilder.tsx @@ -11,6 +11,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { md } from "@calcom/lib/markdownIt"; import { markdownToSafeHTMLClient } from "@calcom/lib/markdownToSafeHTMLClient"; import turndown from "@calcom/lib/turndownService"; +import { excludeOrRequireEmailSchema } from "@calcom/prisma/zod-utils"; import classNames from "@calcom/ui/classNames"; import { Badge } from "@calcom/ui/components/badge"; import { Button } from "@calcom/ui/components/button"; @@ -31,7 +32,7 @@ import { showToast } from "@calcom/ui/components/toast"; import { fieldTypesConfigMap } from "./fieldTypes"; import { fieldsThatSupportLabelAsSafeHtml } from "./fieldsThatSupportLabelAsSafeHtml"; -import { type fieldsSchema, excludeOrRequireEmailSchema } from "./schema"; +import type { fieldsSchema } from "./schema"; import { getFieldIdentifier } from "./utils/getFieldIdentifier"; import { getConfig as getVariantsConfig } from "./utils/variantsConfig"; diff --git a/packages/features/form-builder/fieldTypes.ts b/packages/features/form-builder/fieldTypes.ts index 196a86f090..2d1cde6849 100644 --- a/packages/features/form-builder/fieldTypes.ts +++ b/packages/features/form-builder/fieldTypes.ts @@ -1,7 +1,9 @@ import type z from "zod"; +import type { FieldType } from "@calcom/prisma/zod-utils"; + import { propsTypes } from "./propsTypes"; -import type { FieldType, fieldTypeConfigSchema } from "./schema"; +import type { fieldTypeConfigSchema } from "./schema"; const configMap: Record, "propsType">> = { // This won't be stored in DB. It allows UI to be configured from the codebase for all existing booking fields stored in DB as well diff --git a/packages/features/form-builder/fieldsThatSupportLabelAsSafeHtml.ts b/packages/features/form-builder/fieldsThatSupportLabelAsSafeHtml.ts index c4d432cdf5..88213a2067 100644 --- a/packages/features/form-builder/fieldsThatSupportLabelAsSafeHtml.ts +++ b/packages/features/form-builder/fieldsThatSupportLabelAsSafeHtml.ts @@ -1,4 +1,4 @@ -import type { FieldType } from "./schema"; +import type { FieldType } from "@calcom/prisma/zod-utils"; /** * Once a component supports `labelAsSafeHtml`, add it's field's type here diff --git a/packages/features/form-builder/schema.ts b/packages/features/form-builder/schema.ts index d67b84838a..fb4e881524 100644 --- a/packages/features/form-builder/schema.ts +++ b/packages/features/form-builder/schema.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { getValidRhfFieldName } from "@calcom/lib/getValidRhfFieldName"; +import { fieldSchema, fieldTypeEnum, variantsConfigSchema, type FieldType } from "@calcom/prisma/zod-utils"; import { fieldTypesConfigMap } from "./fieldTypes"; import { preprocessNameFieldDataWithVariant } from "./utils"; @@ -8,154 +8,6 @@ import { getConfig as getVariantsConfig } from "./utils/variantsConfig"; const nonEmptyString = () => z.string().refine((value: string) => value.trim().length > 0); -const fieldTypeEnum = z.enum([ - "name", - "text", - "textarea", - "number", - "email", - "phone", - "address", - "multiemail", - "select", - "multiselect", - "checkbox", - "radio", - "radioInput", - "boolean", - "url", -]); - -export type FieldType = z.infer; - -export const EditableSchema = z.enum([ - "system", // Can't be deleted, can't be hidden, name can't be edited, can't be marked optional - "system-but-optional", // Can't be deleted. Name can't be edited. But can be hidden or be marked optional - "system-but-hidden", // Can't be deleted, name can't be edited, will be shown - "user", // Fully editable - "user-readonly", // All fields are readOnly. -]); - -export const excludeOrRequireEmailSchema = z.string().superRefine((val, ctx) => { - const allDomains = val.split(",").map((dom) => dom.trim()); - - const regex = /^(?:@?[a-z0-9-]+(?:\.[a-z]{2,})?)?(?:@[a-z0-9-]+\.[a-z]{2,})?$/; - - /* - Valid patterns - [ example, example.anything, anyone@example.anything ] - Invalid patterns - Patterns involving capital letter [ Example, Example.anything, Anyone@example.anything ] -*/ - - const isValid = !allDomains.some((domain) => !regex.test(domain)); - - if (!isValid) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Enter valid domain or email", - }); - } -}); - -const baseFieldSchema = z.object({ - name: z.string().transform(getValidRhfFieldName), - type: fieldTypeEnum, - // TODO: We should make at least one of `defaultPlaceholder` and `placeholder` required. Do the same for label. - label: z.string().optional(), - labelAsSafeHtml: z.string().optional(), - - /** - * It is the default label that will be used when a new field is created. - * Note: It belongs in FieldsTypeConfig, so that changing defaultLabel in code can work for existing fields as well(for fields that are using the default label). - * Supports translation - */ - defaultLabel: z.string().optional(), - - placeholder: z.string().optional(), - /** - * It is the default placeholder that will be used when a new field is created. - * Note: Same as defaultLabel, it belongs in FieldsTypeConfig - * Supports translation - */ - defaultPlaceholder: z.string().optional(), - required: z.boolean().default(false).optional(), - /** - * It is the list of options that is valid for a certain type of fields. - * - */ - options: z - .array( - z.object({ - label: z.string(), - value: z.string(), - price: z.coerce.number().min(0).optional(), - }) - ) - .optional(), - /** - * This is an alternate way to specify options when the options are stored elsewhere. Form Builder expects options to be present at `dataStore[getOptionsAt]` - * This allows keeping a single source of truth in DB. - */ - getOptionsAt: z.string().optional(), - - /** - * For `radioInput` type of questions, it stores the input that is shown based on the user option selected. - * e.g. If user is given a list of locations and he selects "Phone", then he will be shown a phone input - */ - optionsInputs: z - .record( - z.object({ - // Support all types as needed - // Must be a subset of `fieldTypeEnum`.TODO: Enforce it in TypeScript - type: z.enum(["address", "phone", "text"]), - required: z.boolean().optional(), - placeholder: z.string().optional(), - }) - ) - .optional(), - - /** - * It is the minimum number of characters that can be entered in the field. - * It is used for types with `supportsLengthCheck= true`. - * @default 0 - * @requires supportsLengthCheck = true - */ - minLength: z.number().optional(), - - /** - * It is the maximum number of characters that can be entered in the field. - * It is used for types with `supportsLengthCheck= true`. - * @requires supportsLengthCheck = true - */ - maxLength: z.number().optional(), - - // Emails that needs to be excluded - excludeEmails: excludeOrRequireEmailSchema.optional(), - // Emails that need to be required - requireEmails: excludeOrRequireEmailSchema.optional(), - // Price associated with the field which works like addons which users can add to the booking - price: z.coerce.number().min(0).optional(), -}); - -export const variantsConfigSchema = z.object({ - variants: z.record( - z.object({ - /** - * Variant Fields schema for a variant of the main field. - * It doesn't support non text fields as of now - **/ - fields: baseFieldSchema - .omit({ - defaultLabel: true, - defaultPlaceholder: true, - options: true, - getOptionsAt: true, - optionsInputs: true, - }) - .array(), - }) - ), -}); - export type ALL_VIEWS = "ALL_VIEWS"; // It is the config that is specific to a type and doesn't make sense in all fields individually. Any field with the type will automatically inherit this config. @@ -238,47 +90,6 @@ export const fieldTypeConfigSchema = z return true; }); -/** - * Main field Schema - */ -export const fieldSchema = baseFieldSchema.merge( - z.object({ - variant: z.string().optional(), - variantsConfig: variantsConfigSchema.optional(), - - views: z - .object({ - label: z.string(), - id: z.string(), - description: z.string().optional(), - }) - .array() - .optional(), - - /** - * It is used to hide fields such as location when there are less than two options - */ - hideWhenJustOneOption: z.boolean().default(false).optional(), - - hidden: z.boolean().optional(), - editable: EditableSchema.default("user").optional(), - sources: z - .array( - z.object({ - // Unique ID for the `type`. If type is workflow, it's the workflow ID - id: z.string(), - type: z.union([z.literal("user"), z.literal("system"), z.string()]), - label: z.string(), - editUrl: z.string().optional(), - // Mark if a field is required by this source or not. This allows us to set `field.required` based on all the sources' fieldRequired value - fieldRequired: z.boolean().optional(), - }) - ) - .optional(), - disableOnPrefill: z.boolean().default(false).optional(), - }) -); - export const fieldsSchema = z.array(fieldSchema); export const fieldTypesSchemaMap: Partial< diff --git a/packages/features/form-builder/utils/variantsConfig.ts b/packages/features/form-builder/utils/variantsConfig.ts index 0a099a6932..db41a5a3e2 100644 --- a/packages/features/form-builder/utils/variantsConfig.ts +++ b/packages/features/form-builder/utils/variantsConfig.ts @@ -1,9 +1,9 @@ import type z from "zod"; import type { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { fieldSchema } from "@calcom/prisma/zod-utils"; import { fieldTypesConfigMap } from "../fieldTypes"; -import type { fieldSchema } from "../schema"; type ConfigVariants = NonNullable>["variants"]; type Field = z.infer; diff --git a/packages/features/auth/lib/hashPassword.ts b/packages/lib/auth/hashPassword.ts similarity index 100% rename from packages/features/auth/lib/hashPassword.ts rename to packages/lib/auth/hashPassword.ts diff --git a/packages/features/auth/lib/isPasswordValid.ts b/packages/lib/auth/isPasswordValid.ts similarity index 100% rename from packages/features/auth/lib/isPasswordValid.ts rename to packages/lib/auth/isPasswordValid.ts diff --git a/packages/lib/server/service/userCreationService.test.ts b/packages/lib/server/service/userCreationService.test.ts index 006af47701..9f32c9c3e8 100644 --- a/packages/lib/server/service/userCreationService.test.ts +++ b/packages/lib/server/service/userCreationService.test.ts @@ -2,8 +2,8 @@ import prismock from "../../../../tests/libs/__mocks__/prisma"; import { describe, test, expect, vi, beforeEach } from "vitest"; -import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller"; +import { hashPassword } from "@calcom/lib/auth/hashPassword"; import { CreationSource } from "@calcom/prisma/enums"; import { UserRepository } from "../repository/user"; @@ -20,7 +20,7 @@ vi.mock("@calcom/lib/server/i18n", () => { }; }); -vi.mock("@calcom/features/auth/lib/hashPassword", () => ({ +vi.mock("@calcom/lib/auth/hashPassword", () => ({ hashPassword: vi.fn().mockResolvedValue("hashed-password"), })); diff --git a/packages/lib/server/service/userCreationService.ts b/packages/lib/server/service/userCreationService.ts index 9d149951c0..20d6ecd071 100644 --- a/packages/lib/server/service/userCreationService.ts +++ b/packages/lib/server/service/userCreationService.ts @@ -1,5 +1,5 @@ -import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller"; +import { hashPassword } from "@calcom/lib/auth/hashPassword"; import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; import type { CreationSource, UserPermissionRole, IdentityProvider } from "@calcom/prisma/enums"; diff --git a/packages/prisma/package.json b/packages/prisma/package.json index 28b4116dbe..3cf2204189 100644 --- a/packages/prisma/package.json +++ b/packages/prisma/package.json @@ -18,11 +18,10 @@ "post-install": "yarn generate-schemas", "seed-app-store": "ts-node --transpile-only ../../scripts/seed-app-store.ts", "delete-app": "ts-node --transpile-only ./delete-app.ts", - "seed-insights": "ts-node --transpile-only ./seed-insights.ts", - "seed-pbac": "ts-node --transpile-only ./seed-pbac-organization.ts" + "seed-insights": "ts-node --transpile-only ../../scripts/seed-insights.ts", + "seed-pbac": "ts-node --transpile-only ../../scripts/seed-pbac-organization.ts" }, "devDependencies": { - "@faker-js/faker": "9.2.0", "npm-run-all": "^4.1.5" }, "dependencies": { @@ -46,4 +45,4 @@ "prisma": { "seed": "ts-node --transpile-only ../../scripts/seed.ts" } -} +} \ No newline at end of file diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index c0a2558ca9..afddcb2146 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -11,10 +11,9 @@ import type { ZodTypeAny, } from "zod"; -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 { isPasswordValid } from "@calcom/lib/auth/isPasswordValid"; import { emailSchema as emailRegexSchema, emailRegex } from "@calcom/lib/emailSchema"; +import { getValidRhfFieldName } from "@calcom/lib/getValidRhfFieldName"; import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema"; import { zodAttributesQueryValue } from "@calcom/lib/raqb/zod"; import { slugify } from "@calcom/lib/slugify"; @@ -135,10 +134,6 @@ export const eventTypeMetaDataSchemaWithoutApps = _eventTypeMetaDataSchemaWithou export type EventTypeMetadata = z.infer; -export const eventTypeBookingFields = formBuilderFieldsSchema; -export const BookingFieldTypeEnum = eventTypeBookingFields.element.shape.type.Enum; -export type BookingFieldType = FormBuilderFieldType; - // Validation of user added bookingFields' responses happen using `getBookingResponsesSchema` which requires `eventType`. // So it is a dynamic validation and thus entire validation can't exist here // Note that this validation runs to validate prefill params as well, so it should consider that partial values can be there. e.g. `name` might be empty string @@ -768,3 +763,193 @@ export const serviceAccountKeySchema = z export type TServiceAccountKeySchema = z.infer; export const rrSegmentQueryValueSchema = zodAttributesQueryValue.nullish(); + +// Routing Form Fields +export const fieldTypeEnum = z.enum([ + "name", + "text", + "textarea", + "number", + "email", + "phone", + "address", + "multiemail", + "select", + "multiselect", + "checkbox", + "radio", + "radioInput", + "boolean", + "url", +]); + +export type FieldType = z.infer; + +export const excludeOrRequireEmailSchema = z.string().superRefine((val, ctx) => { + const allDomains = val.split(",").map((dom) => dom.trim()); + + const regex = /^(?:@?[a-z0-9-]+(?:\.[a-z]{2,})?)?(?:@[a-z0-9-]+\.[a-z]{2,})?$/; + + /* + Valid patterns - [ example, example.anything, anyone@example.anything ] + Invalid patterns - Patterns involving capital letter [ Example, Example.anything, Anyone@example.anything ] +*/ + + const isValid = !allDomains.some((domain) => !regex.test(domain)); + + if (!isValid) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Enter valid domain or email", + }); + } +}); + +export const EditableSchema = z.enum([ + "system", // Can't be deleted, can't be hidden, name can't be edited, can't be marked optional + "system-but-optional", // Can't be deleted. Name can't be edited. But can be hidden or be marked optional + "system-but-hidden", // Can't be deleted, name can't be edited, will be shown + "user", // Fully editable + "user-readonly", // All fields are readOnly. +]); + +export const baseFieldSchema = z.object({ + name: z.string().transform(getValidRhfFieldName), + type: fieldTypeEnum, + // TODO: We should make at least one of `defaultPlaceholder` and `placeholder` required. Do the same for label. + label: z.string().optional(), + labelAsSafeHtml: z.string().optional(), + + /** + * It is the default label that will be used when a new field is created. + * Note: It belongs in FieldsTypeConfig, so that changing defaultLabel in code can work for existing fields as well(for fields that are using the default label). + * Supports translation + */ + defaultLabel: z.string().optional(), + + placeholder: z.string().optional(), + /** + * It is the default placeholder that will be used when a new field is created. + * Note: Same as defaultLabel, it belongs in FieldsTypeConfig + * Supports translation + */ + defaultPlaceholder: z.string().optional(), + required: z.boolean().default(false).optional(), + /** + * It is the list of options that is valid for a certain type of fields. + * + */ + options: z + .array( + z.object({ + label: z.string(), + value: z.string(), + price: z.coerce.number().min(0).optional(), + }) + ) + .optional(), + /** + * This is an alternate way to specify options when the options are stored elsewhere. Form Builder expects options to be present at `dataStore[getOptionsAt]` + * This allows keeping a single source of truth in DB. + */ + getOptionsAt: z.string().optional(), + + /** + * For `radioInput` type of questions, it stores the input that is shown based on the user option selected. + * e.g. If user is given a list of locations and he selects "Phone", then he will be shown a phone input + */ + optionsInputs: z + .record( + z.object({ + // Support all types as needed + // Must be a subset of `fieldTypeEnum`.TODO: Enforce it in TypeScript + type: z.enum(["address", "phone", "text"]), + required: z.boolean().optional(), + placeholder: z.string().optional(), + }) + ) + .optional(), + + /** + * It is the minimum number of characters that can be entered in the field. + * It is used for types with `supportsLengthCheck= true`. + * @default 0 + * @requires supportsLengthCheck = true + */ + minLength: z.number().optional(), + + /** + * It is the maximum number of characters that can be entered in the field. + * It is used for types with `supportsLengthCheck= true`. + * @requires supportsLengthCheck = true + */ + maxLength: z.number().optional(), + + // Emails that needs to be excluded + excludeEmails: excludeOrRequireEmailSchema.optional(), + // Emails that need to be required + requireEmails: excludeOrRequireEmailSchema.optional(), + // Price associated with the field which works like addons which users can add to the booking + price: z.coerce.number().min(0).optional(), +}); + +export const variantsConfigSchema = z.object({ + variants: z.record( + z.object({ + /** + * Variant Fields schema for a variant of the main field. + * It doesn't support non text fields as of now + **/ + fields: baseFieldSchema + .omit({ + defaultLabel: true, + defaultPlaceholder: true, + options: true, + getOptionsAt: true, + optionsInputs: true, + }) + .array(), + }) + ), +}); + +export const fieldSchema = baseFieldSchema.merge( + z.object({ + variant: z.string().optional(), + variantsConfig: variantsConfigSchema.optional(), + + views: z + .object({ + label: z.string(), + id: z.string(), + description: z.string().optional(), + }) + .array() + .optional(), + + /** + * It is used to hide fields such as location when there are less than two options + */ + hideWhenJustOneOption: z.boolean().default(false).optional(), + + hidden: z.boolean().optional(), + editable: EditableSchema.default("user").optional(), + sources: z + .array( + z.object({ + // Unique ID for the `type`. If type is workflow, it's the workflow ID + id: z.string(), + type: z.union([z.literal("user"), z.literal("system"), z.string()]), + label: z.string(), + editUrl: z.string().optional(), + // Mark if a field is required by this source or not. This allows us to set `field.required` based on all the sources' fieldRequired value + fieldRequired: z.boolean().optional(), + }) + ) + .optional(), + disableOnPrefill: z.boolean().default(false).optional(), + }) +); + +export const eventTypeBookingFields = z.array(fieldSchema); +export const BookingFieldTypeEnum = eventTypeBookingFields.element.shape.type.Enum; diff --git a/packages/trpc/server/routers/viewer/auth/changePassword.handler.ts b/packages/trpc/server/routers/viewer/auth/changePassword.handler.ts index 27437f3a3c..f0c1616774 100644 --- a/packages/trpc/server/routers/viewer/auth/changePassword.handler.ts +++ b/packages/trpc/server/routers/viewer/auth/changePassword.handler.ts @@ -1,6 +1,6 @@ -import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { validPassword } from "@calcom/features/auth/lib/validPassword"; import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword"; +import { hashPassword } from "@calcom/lib/auth/hashPassword"; import { prisma } from "@calcom/prisma"; import { IdentityProvider } from "@calcom/prisma/enums"; diff --git a/packages/trpc/server/routers/viewer/organizations/setPassword.handler.ts b/packages/trpc/server/routers/viewer/organizations/setPassword.handler.ts index 7b63ca4ba3..e2faafb94e 100644 --- a/packages/trpc/server/routers/viewer/organizations/setPassword.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/setPassword.handler.ts @@ -1,7 +1,7 @@ import { createHash } from "crypto"; -import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword"; +import { hashPassword } from "@calcom/lib/auth/hashPassword"; import { prisma } from "@calcom/prisma"; import { TRPCError } from "@trpc/server"; diff --git a/packages/prisma/seed-huge-event-types.ts b/scripts/seed-huge-event-types.ts similarity index 100% rename from packages/prisma/seed-huge-event-types.ts rename to scripts/seed-huge-event-types.ts diff --git a/packages/prisma/seed-insights.ts b/scripts/seed-insights.ts similarity index 98% rename from packages/prisma/seed-insights.ts rename to scripts/seed-insights.ts index d330b6455c..83a5237035 100644 --- a/packages/prisma/seed-insights.ts +++ b/scripts/seed-insights.ts @@ -2,11 +2,11 @@ import { faker } from "@faker-js/faker"; import { v4 as uuidv4 } from "uuid"; import dayjs from "@calcom/dayjs"; -import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; +import { hashPassword } from "@calcom/lib/auth/hashPassword"; +import type { Prisma } from "@calcom/prisma/client"; +import { PrismaClient } from "@calcom/prisma/client"; import { BookingStatus, AssignmentReasonEnum } from "@calcom/prisma/enums"; -import type { Prisma } from "./client"; -import { PrismaClient } from "./client"; import { seedAttributes, seedRoutingFormResponses, seedRoutingForms } from "./seed-utils"; function getRandomRatingFeedback() { diff --git a/packages/prisma/seed-pbac-organization.ts b/scripts/seed-pbac-organization.ts similarity index 99% rename from packages/prisma/seed-pbac-organization.ts rename to scripts/seed-pbac-organization.ts index 8cb68d958f..01a527b9ba 100644 --- a/packages/prisma/seed-pbac-organization.ts +++ b/scripts/seed-pbac-organization.ts @@ -1,11 +1,10 @@ import { uuid } from "short-uuid"; -import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; +import { hashPassword } from "@calcom/lib/auth/hashPassword"; import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability"; +import prisma from "@calcom/prisma"; import { MembershipRole, RoleType } from "@calcom/prisma/enums"; -import prisma from "."; - /** * Creates an organization with custom roles and PBAC (Permission-Based Access Control) enabled * This demonstrates how to set up fine-grained permissions for team members diff --git a/scripts/seed-performance-testing.ts b/scripts/seed-performance-testing.ts index a244275c24..408ce2aecf 100644 --- a/scripts/seed-performance-testing.ts +++ b/scripts/seed-performance-testing.ts @@ -11,7 +11,7 @@ import zoomMeta from "@calcom/app-store/zoomvideo/_metadata"; import dayjs from "@calcom/dayjs"; import { BookingStatus } from "@calcom/prisma/enums"; -import { createUserAndEventType } from "../packages/prisma/seed-utils"; +import { createUserAndEventType } from "./seed-utils"; async function _createManyDifferentUsersWithDifferentEventTypesAndBookings({ tillUser, @@ -322,4 +322,4 @@ async function createAUserWithManyBookings() { // startFrom: 10000, // }); -createAUserWithManyBookings(); \ No newline at end of file +createAUserWithManyBookings(); diff --git a/packages/prisma/seed-utils.ts b/scripts/seed-utils.ts similarity index 98% rename from packages/prisma/seed-utils.ts rename to scripts/seed-utils.ts index 6cbe9b2fe2..97150269a8 100644 --- a/packages/prisma/seed-utils.ts +++ b/scripts/seed-utils.ts @@ -4,13 +4,12 @@ import { uuid } from "short-uuid"; import type z from "zod"; import dayjs from "@calcom/dayjs"; -import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; +import { hashPassword } from "@calcom/lib/auth/hashPassword"; import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability"; +import prisma from "@calcom/prisma"; +import type { Prisma, UserPermissionRole } from "@calcom/prisma/client"; import { MembershipRole } from "@calcom/prisma/enums"; - -import prisma from "."; -import type { Prisma, UserPermissionRole } from "./client"; -import type { teamMetadataSchema } from "./zod-utils"; +import type { teamMetadataSchema } from "@calcom/prisma/zod-utils"; export async function createUserAndEventType({ user, diff --git a/scripts/seed.ts b/scripts/seed.ts index d60e1a19f1..cb52433c21 100644 --- a/scripts/seed.ts +++ b/scripts/seed.ts @@ -6,7 +6,7 @@ import googleMeetMeta from "@calcom/app-store/googlevideo/_metadata"; import zoomMeta from "@calcom/app-store/zoomvideo/_metadata"; import dayjs from "@calcom/dayjs"; import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains"; -import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; +import { hashPassword } from "@calcom/lib/auth/hashPassword"; import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability"; import prisma from "@calcom/prisma"; import type { Membership, Team, User, UserPermissionRole } from "@calcom/prisma/client"; @@ -14,10 +14,10 @@ import { Prisma } from "@calcom/prisma/client"; import { BookingStatus, MembershipRole, RedirectType, SchedulingType } from "@calcom/prisma/enums"; import type { Ensure } from "@calcom/types/utils"; -import mainHugeEventTypesSeed from "../packages/prisma/seed-huge-event-types"; -import { createUserAndEventType } from "../packages/prisma/seed-utils"; import type { teamMetadataSchema } from "../packages/prisma/zod-utils"; import mainAppStore from "./seed-app-store"; +import mainHugeEventTypesSeed from "./seed-huge-event-types"; +import { createUserAndEventType } from "./seed-utils"; type PlatformUser = { email: string; diff --git a/yarn.lock b/yarn.lock index 5fbb2144c8..0ba628150e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6032,9 +6032,9 @@ __metadata: linkType: hard "@faker-js/faker@npm:^7.3.0": - version: 7.4.0 - resolution: "@faker-js/faker@npm:7.4.0" - checksum: 1acebb84bfb142c08e6ba2942910d16bd92ea147fa585fa2fa9ce9983f7a8c7c016002beb7fc20ef6f7aef6c4ae9cb3fa680b275823402e34802b8489d2b980d + version: 7.6.0 + resolution: "@faker-js/faker@npm:7.6.0" + checksum: 942af6221774e8c98a0eb6bc75265e05fb81a941170377666c3439aab9495dd321d6beedc5406f07e6ad44262b3e43c20961f666d116ad150b78e7437dd1bb2b languageName: node linkType: hard @@ -21669,6 +21669,7 @@ __metadata: "@changesets/cli": 2.29.4 "@daily-co/daily-js": ^0.83.1 "@evyweb/ioctopus": ^1.2.0 + "@faker-js/faker": 9.2.0 "@jetstreamapp/soql-parser-js": ^6.1.0 "@next/third-parties": ^14.2.5 "@playwright/test": ^1.45.3