mirror of
https://github.com/makeplane/plane.git
synced 2026-02-24 04:00:14 +01:00
[VPAT-55] chore(security): implement input validation across authentication and workspace forms (#8528)
* chore(security): implement input validation across authentication and workspace forms
- Add OWASP-compliant autocomplete attributes to all auth input fields
- Create centralized validation utilities blocking injection-risk characters
- Apply validation to names, display names, workspace names, and slugs
- Block special characters: < > ' " % # { } [ ] * ^ !
- Secure sensitive input fields across admin, web, and space apps
* chore: add missing workspace name validation to settings and admin forms
* feat: enhance validation regex for international names and usernames
- Updated regex patterns to support Unicode characters for person names, display names, company names, and slugs.
- Improved validation functions to block injection-risk characters in names and slugs.
This commit is contained in:
@@ -14,6 +14,7 @@ import { Button, getButtonStyling } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { InstanceWorkspaceService } from "@plane/services";
|
||||
import type { IWorkspace } from "@plane/types";
|
||||
import { validateSlug, validateWorkspaceName } from "@plane/utils";
|
||||
// components
|
||||
import { CustomSelect, Input } from "@plane/ui";
|
||||
// hooks
|
||||
@@ -96,14 +97,7 @@ export function WorkspaceCreateForm() {
|
||||
control={control}
|
||||
name="name"
|
||||
rules={{
|
||||
required: "This is a required field.",
|
||||
validate: (value) =>
|
||||
/^[\w\s-]*$/.test(value) ||
|
||||
`Workspaces names can contain only (" "), ( - ), ( _ ) and alphanumeric characters.`,
|
||||
maxLength: {
|
||||
value: 80,
|
||||
message: "Limit your name to 80 characters.",
|
||||
},
|
||||
validate: (value) => validateWorkspaceName(value, true),
|
||||
}}
|
||||
render={({ field: { value, ref, onChange } }) => (
|
||||
<Input
|
||||
@@ -135,11 +129,7 @@ export function WorkspaceCreateForm() {
|
||||
control={control}
|
||||
name="slug"
|
||||
rules={{
|
||||
required: "The URL is a required field.",
|
||||
maxLength: {
|
||||
value: 48,
|
||||
message: "Limit your URL to 48 characters.",
|
||||
},
|
||||
validate: (value) => validateSlug(value),
|
||||
}}
|
||||
render={({ field: { onChange, value, ref } }) => (
|
||||
<Input
|
||||
|
||||
@@ -13,7 +13,7 @@ import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { AuthService } from "@plane/services";
|
||||
import { Checkbox, Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
|
||||
import { getPasswordStrength } from "@plane/utils";
|
||||
import { getPasswordStrength, validatePersonName, validateCompanyName } from "@plane/utils";
|
||||
// components
|
||||
import { AuthHeader } from "@/app/(all)/(home)/auth-header";
|
||||
import { Banner } from "../common/banner";
|
||||
@@ -173,9 +173,15 @@ export function InstanceSetupForm() {
|
||||
inputSize="md"
|
||||
placeholder="Wilber"
|
||||
value={formData.first_name}
|
||||
onChange={(e) => handleFormChange("first_name", e.target.value)}
|
||||
autoComplete="on"
|
||||
onChange={(e) => {
|
||||
const validation = validatePersonName(e.target.value);
|
||||
if (validation === true || e.target.value === "") {
|
||||
handleFormChange("first_name", e.target.value);
|
||||
}
|
||||
}}
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
maxLength={50}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full space-y-1">
|
||||
@@ -190,8 +196,14 @@ export function InstanceSetupForm() {
|
||||
inputSize="md"
|
||||
placeholder="Wright"
|
||||
value={formData.last_name}
|
||||
onChange={(e) => handleFormChange("last_name", e.target.value)}
|
||||
autoComplete="on"
|
||||
onChange={(e) => {
|
||||
const validation = validatePersonName(e.target.value);
|
||||
if (validation === true || e.target.value === "") {
|
||||
handleFormChange("last_name", e.target.value);
|
||||
}
|
||||
}}
|
||||
autoComplete="off"
|
||||
maxLength={50}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -229,7 +241,13 @@ export function InstanceSetupForm() {
|
||||
inputSize="md"
|
||||
placeholder="Company name"
|
||||
value={formData.company_name}
|
||||
onChange={(e) => handleFormChange("company_name", e.target.value)}
|
||||
onChange={(e) => {
|
||||
const validation = validateCompanyName(e.target.value, false);
|
||||
if (validation === true || e.target.value === "") {
|
||||
handleFormChange("company_name", e.target.value);
|
||||
}
|
||||
}}
|
||||
maxLength={80}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IUser, IWorkspace, TOnboardingSteps } from "@plane/types";
|
||||
// ui
|
||||
import { CustomSelect, Input, Spinner } from "@plane/ui";
|
||||
import { validateWorkspaceName, validateSlug } from "@plane/utils";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useUserProfile, useUserSettings } from "@/hooks/store/user";
|
||||
@@ -138,8 +139,7 @@ export const CreateWorkspace = observer(function CreateWorkspace(props: Props) {
|
||||
name="name"
|
||||
rules={{
|
||||
required: t("common.errors.required"),
|
||||
validate: (value) =>
|
||||
/^[\w\s-]*$/.test(value) || t("workspace_creation.errors.validation.name_alphanumeric"),
|
||||
validate: (value) => validateWorkspaceName(value, true),
|
||||
maxLength: {
|
||||
value: 80,
|
||||
message: t("workspace_creation.errors.validation.name_length"),
|
||||
@@ -200,7 +200,8 @@ export const CreateWorkspace = observer(function CreateWorkspace(props: Props) {
|
||||
type="text"
|
||||
value={value.toLocaleLowerCase().trim().replace(/ /g, "-")}
|
||||
onChange={(e) => {
|
||||
if (/^[a-zA-Z0-9_-]+$/.test(e.target.value)) setInvalidSlug(false);
|
||||
const validation = validateSlug(e.target.value);
|
||||
if (validation === true) setInvalidSlug(false);
|
||||
else setInvalidSlug(true);
|
||||
onChange(e.target.value.toLowerCase());
|
||||
}}
|
||||
|
||||
@@ -17,7 +17,7 @@ import type { IUser, TUserProfile, TOnboardingSteps } from "@plane/types";
|
||||
// ui
|
||||
import { Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
|
||||
// components
|
||||
import { cn, getFileURL, getPasswordStrength } from "@plane/utils";
|
||||
import { cn, getFileURL, getPasswordStrength, validatePersonName } from "@plane/utils";
|
||||
import { UserImageUploadModal } from "@/components/core/modals/user-image-upload-modal";
|
||||
// hooks
|
||||
import { useUser, useUserProfile } from "@/hooks/store/user";
|
||||
@@ -303,9 +303,10 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
|
||||
name="first_name"
|
||||
rules={{
|
||||
required: "First name is required",
|
||||
validate: validatePersonName,
|
||||
maxLength: {
|
||||
value: 24,
|
||||
message: "First name must be within 24 characters.",
|
||||
value: 50,
|
||||
message: "First name must be within 50 characters.",
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
@@ -340,9 +341,10 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
|
||||
name="last_name"
|
||||
rules={{
|
||||
required: "Last name is required",
|
||||
validate: validatePersonName,
|
||||
maxLength: {
|
||||
value: 24,
|
||||
message: "Last name must be within 24 characters.",
|
||||
value: 50,
|
||||
message: "Last name must be within 50 characters.",
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Button } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IUser } from "@plane/types";
|
||||
import { EOnboardingSteps } from "@plane/types";
|
||||
import { cn, getFileURL, getPasswordStrength } from "@plane/utils";
|
||||
import { cn, getFileURL, getPasswordStrength, validatePersonName } from "@plane/utils";
|
||||
// components
|
||||
import { UserImageUploadModal } from "@/components/core/modals/user-image-upload-modal";
|
||||
// hooks
|
||||
@@ -208,9 +208,10 @@ export const ProfileSetupStep = observer(function ProfileSetupStep({ handleStepC
|
||||
name="first_name"
|
||||
rules={{
|
||||
required: "Name is required",
|
||||
validate: validatePersonName,
|
||||
maxLength: {
|
||||
value: 24,
|
||||
message: "Name must be within 24 characters.",
|
||||
value: 50,
|
||||
message: "Name must be within 50 characters.",
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Button } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IUser, IWorkspace } from "@plane/types";
|
||||
import { Spinner } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
import { cn, validateWorkspaceName, validateSlug } from "@plane/utils";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store/use-instance";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
@@ -146,8 +146,7 @@ export const WorkspaceCreateStep = observer(function WorkspaceCreateStep({
|
||||
name="name"
|
||||
rules={{
|
||||
required: t("common.errors.required"),
|
||||
validate: (value) =>
|
||||
/^[\w\s-]*$/.test(value) || t("workspace_creation.errors.validation.name_alphanumeric"),
|
||||
validate: (value) => validateWorkspaceName(value, true),
|
||||
maxLength: {
|
||||
value: 80,
|
||||
message: t("workspace_creation.errors.validation.name_length"),
|
||||
@@ -220,7 +219,8 @@ export const WorkspaceCreateStep = observer(function WorkspaceCreateStep({
|
||||
type="text"
|
||||
value={value.toLocaleLowerCase().trim().replace(/ /g, "-")}
|
||||
onChange={(e) => {
|
||||
if (/^[a-zA-Z0-9_-]+$/.test(e.target.value)) setInvalidSlug(false);
|
||||
const validation = validateSlug(e.target.value);
|
||||
if (validation === true) setInvalidSlug(false);
|
||||
else setInvalidSlug(true);
|
||||
onChange(e.target.value.toLowerCase());
|
||||
}}
|
||||
|
||||
@@ -28,6 +28,8 @@ import { handleCoverImageChange } from "@/helpers/cover-image.helper";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store/use-instance";
|
||||
import { useUser, useUserProfile } from "@/hooks/store/user";
|
||||
// utils
|
||||
import { validatePersonName, validateDisplayName } from "@plane/utils";
|
||||
|
||||
type TUserProfileForm = {
|
||||
avatar_url: string;
|
||||
@@ -260,6 +262,7 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
|
||||
name="first_name"
|
||||
rules={{
|
||||
required: "Please enter first name",
|
||||
validate: validatePersonName,
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
@@ -272,7 +275,7 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
|
||||
hasError={Boolean(errors.first_name)}
|
||||
placeholder="Enter your first name"
|
||||
className={`w-full rounded-md ${errors.first_name ? "border-danger-strong" : ""}`}
|
||||
maxLength={24}
|
||||
maxLength={50}
|
||||
autoComplete="on"
|
||||
/>
|
||||
)}
|
||||
@@ -284,6 +287,9 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
|
||||
<Controller
|
||||
control={control}
|
||||
name="last_name"
|
||||
rules={{
|
||||
validate: validatePersonName,
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="last_name"
|
||||
@@ -295,11 +301,12 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
|
||||
hasError={Boolean(errors.last_name)}
|
||||
placeholder="Enter your last name"
|
||||
className="w-full rounded-md"
|
||||
maxLength={24}
|
||||
maxLength={50}
|
||||
autoComplete="on"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.last_name && <span className="text-11 text-danger-primary">{errors.last_name.message}</span>}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-13 font-medium text-secondary">
|
||||
@@ -311,14 +318,7 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
|
||||
name="display_name"
|
||||
rules={{
|
||||
required: "Display name is required.",
|
||||
validate: (value) => {
|
||||
if (value.trim().length < 1) return "Display name can't be empty.";
|
||||
if (value.split(" ").length > 1) return "Display name can't have two consecutive spaces.";
|
||||
if (value.replace(/\s/g, "").length < 1) return "Display name must be at least 1 character long.";
|
||||
if (value.replace(/\s/g, "").length > 20)
|
||||
return "Display name must be less than 20 characters long.";
|
||||
return true;
|
||||
},
|
||||
validate: validateDisplayName,
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
@@ -331,7 +331,7 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
|
||||
hasError={Boolean(errors?.display_name)}
|
||||
placeholder="Enter your display name"
|
||||
className={`w-full ${errors?.display_name ? "border-danger-strong" : ""}`}
|
||||
maxLength={24}
|
||||
maxLength={50}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IWorkspace } from "@plane/types";
|
||||
// ui
|
||||
import { CustomSelect, Input } from "@plane/ui";
|
||||
import { validateWorkspaceName, validateSlug } from "@plane/utils";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
@@ -126,8 +127,7 @@ export const CreateWorkspaceForm = observer(function CreateWorkspaceForm(props:
|
||||
name="name"
|
||||
rules={{
|
||||
required: t("common.errors.required"),
|
||||
validate: (value) =>
|
||||
/^[\w\s-]*$/.test(value) || t("workspace_creation.errors.validation.name_alphanumeric"),
|
||||
validate: (value) => validateWorkspaceName(value, true),
|
||||
maxLength: {
|
||||
value: 80,
|
||||
message: t("workspace_creation.errors.validation.name_length"),
|
||||
@@ -178,7 +178,8 @@ export const CreateWorkspaceForm = observer(function CreateWorkspaceForm(props:
|
||||
type="text"
|
||||
value={value.toLocaleLowerCase().trim().replace(/ /g, "-")}
|
||||
onChange={(e) => {
|
||||
if (/^[a-zA-Z0-9_-]+$/.test(e.target.value)) setInvalidSlug(false);
|
||||
const validation = validateSlug(e.target.value);
|
||||
if (validation === true) setInvalidSlug(false);
|
||||
else setInvalidSlug(true);
|
||||
onChange(e.target.value.toLowerCase());
|
||||
}}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { EditIcon } from "@plane/propel/icons";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IWorkspace } from "@plane/types";
|
||||
import { CustomSelect, Input } from "@plane/ui";
|
||||
import { cn, copyUrlToClipboard, getFileURL } from "@plane/utils";
|
||||
import { cn, copyUrlToClipboard, getFileURL, validateWorkspaceName } from "@plane/utils";
|
||||
// components
|
||||
import { WorkspaceImageUploadModal } from "@/components/core/modals/workspace-image-upload-modal";
|
||||
import { TimezoneSelect } from "@/components/global/timezone-select";
|
||||
@@ -195,11 +195,7 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() {
|
||||
control={control}
|
||||
name="name"
|
||||
rules={{
|
||||
required: t("workspace_settings.settings.general.errors.name.required"),
|
||||
maxLength: {
|
||||
value: 80,
|
||||
message: t("workspace_settings.settings.general.errors.name.max_length"),
|
||||
},
|
||||
validate: (value) => validateWorkspaceName(value, true),
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
@@ -216,6 +212,7 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.name && <p className="text-caption-sm-regular text-danger-primary">{errors.name.message}</p>}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h4 className="text-body-sm-medium text-tertiary">
|
||||
|
||||
@@ -36,6 +36,7 @@ export * from "./tab-indices";
|
||||
export * from "./theme";
|
||||
export { resolveGeneralTheme } from "./theme-legacy";
|
||||
export * from "./url";
|
||||
export * from "./validation";
|
||||
export * from "./work-item-filters";
|
||||
export * from "./work-item";
|
||||
export * from "./workspace";
|
||||
|
||||
216
packages/utils/src/validation.ts
Normal file
216
packages/utils/src/validation.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Input Validation Utilities
|
||||
* Following OWASP Input Validation best practices using allowlist approach
|
||||
*
|
||||
* Security: Blocks injection-risk characters: < > ' " % # { } [ ] * ^ !
|
||||
* These patterns are designed to prevent XSS, SQL injection, template injection,
|
||||
* and other security vulnerabilities while maintaining good UX
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// VALIDATION REGEX PATTERNS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Person Name Pattern (for first_name, last_name)
|
||||
* Allows: Unicode letters (\p{L}), spaces, hyphens, apostrophes
|
||||
* Use case: Accommodates international names like "José", "李明", "محمد", "Müller"
|
||||
* Blocks: Injection-risk characters and special symbols
|
||||
*/
|
||||
export const PERSON_NAME_REGEX = /^[\p{L}\s'-]+$/u;
|
||||
|
||||
/**
|
||||
* Display Name Pattern (for display_name, usernames)
|
||||
* Allows: Unicode letters (\p{L}), numbers (\p{N}), underscore, period, hyphen
|
||||
* Use case: International usernames like "josé_123", "李明.dev", "müller-2024"
|
||||
* Blocks: Spaces and injection-risk characters
|
||||
*/
|
||||
export const DISPLAY_NAME_REGEX = /^[\p{L}\p{N}_.-]+$/u;
|
||||
|
||||
/**
|
||||
* Company/Organization Name Pattern (for company_name, workspace names)
|
||||
* Allows: Unicode letters (\p{L}), numbers (\p{N}), spaces, underscores, hyphens
|
||||
* Use case: International business names like "Société Générale", "株式会社", "Müller GmbH"
|
||||
* Blocks: Special punctuation and injection-risk chars
|
||||
*/
|
||||
export const COMPANY_NAME_REGEX = /^[\p{L}\p{N}\s_-]+$/u;
|
||||
|
||||
/**
|
||||
* URL Slug Pattern (for workspace slugs, URL-safe identifiers)
|
||||
* Allows: Unicode letters (\p{L}), numbers (\p{N}), underscores, hyphens
|
||||
* Use case: International URL-safe identifiers like "josé-workspace", "李明-project"
|
||||
* Blocks: Spaces and special characters (URL encoding will handle Unicode in actual URLs)
|
||||
*/
|
||||
export const SLUG_REGEX = /^[\p{L}\p{N}_-]+$/u;
|
||||
|
||||
// =============================================================================
|
||||
// VALIDATION FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @description Validates person names (first name, last name)
|
||||
* @param {string} name - Name to validate
|
||||
* @returns {boolean | string} true if valid, error message if invalid
|
||||
* @example
|
||||
* validatePersonName("John") // returns true
|
||||
* validatePersonName("O'Brien") // returns true
|
||||
* validatePersonName("Jean-Paul") // returns true
|
||||
* validatePersonName("John<script>") // returns error message
|
||||
*/
|
||||
export const validatePersonName = (name: string): boolean | string => {
|
||||
if (!name || name.trim() === "") {
|
||||
return "Name is required";
|
||||
}
|
||||
|
||||
if (name.length > 50) {
|
||||
return "Name must be 50 characters or less";
|
||||
}
|
||||
|
||||
if (hasInjectionRiskChars(name)) {
|
||||
return "Names cannot contain special characters like < > ' \" { } [ ] * ^ ! # %";
|
||||
}
|
||||
|
||||
if (!PERSON_NAME_REGEX.test(name)) {
|
||||
return "Names can only contain letters, spaces, hyphens, and apostrophes";
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Validates display names and usernames
|
||||
* @param {string} displayName - Display name to validate
|
||||
* @returns {boolean | string} true if valid, error message if invalid
|
||||
* @example
|
||||
* validateDisplayName("john_doe") // returns true
|
||||
* validateDisplayName("john.doe-123") // returns true
|
||||
* validateDisplayName("john doe") // returns error message (spaces not allowed)
|
||||
* validateDisplayName("john<>doe") // returns error message
|
||||
*/
|
||||
export const validateDisplayName = (displayName: string): boolean | string => {
|
||||
if (!displayName || displayName.trim() === "") {
|
||||
return true; // Display name is optional in most cases
|
||||
}
|
||||
|
||||
if (displayName.length > 50) {
|
||||
return "Display name must be 50 characters or less";
|
||||
}
|
||||
|
||||
if (hasInjectionRiskChars(displayName)) {
|
||||
return "Display name cannot contain special characters like < > ' \" { } [ ] * ^ ! # %";
|
||||
}
|
||||
|
||||
if (!DISPLAY_NAME_REGEX.test(displayName)) {
|
||||
return "Display name can only contain letters, numbers, periods, hyphens, and underscores";
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Validates company and organization names
|
||||
* @param {string} companyName - Company name to validate
|
||||
* @param {boolean} required - Whether the field is required
|
||||
* @returns {boolean | string} true if valid, error message if invalid
|
||||
* @example
|
||||
* validateCompanyName("Acme Corp") // returns true
|
||||
* validateCompanyName("Acme_Corp-123") // returns true
|
||||
* validateCompanyName("Acme{Corp}") // returns error message
|
||||
*/
|
||||
export const validateCompanyName = (companyName: string, required: boolean = false): boolean | string => {
|
||||
if (!companyName || companyName.trim() === "") {
|
||||
return required ? "Company name is required" : true;
|
||||
}
|
||||
|
||||
if (companyName.length > 80) {
|
||||
return "Company name must be 80 characters or less";
|
||||
}
|
||||
|
||||
if (hasInjectionRiskChars(companyName)) {
|
||||
return "Company name cannot contain special characters like < > ' \" { } [ ] * ^ ! # %";
|
||||
}
|
||||
|
||||
if (!COMPANY_NAME_REGEX.test(companyName)) {
|
||||
return "Company name can only contain letters, numbers, spaces, hyphens, and underscores";
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Validates company and organization names
|
||||
* @param {string} workspaceName - Workspace name to validate
|
||||
* @param {boolean} required - Whether the field is required
|
||||
* @returns {boolean | string} true if valid, error message if invalid
|
||||
* @example
|
||||
* validateWorkspaceName("Acme Corp") // returns true
|
||||
* validateWorkspaceName("Acme_Corp-123") // returns true
|
||||
* validateWorkspaceName("Acme{Corp}") // returns error message
|
||||
*/
|
||||
export const validateWorkspaceName = (workspaceName: string, required: boolean = false): boolean | string => {
|
||||
if (!workspaceName || workspaceName.trim() === "") {
|
||||
return required ? "Workspace name is required" : true;
|
||||
}
|
||||
|
||||
if (workspaceName.length > 80) {
|
||||
return "Workspace name must be 80 characters or less";
|
||||
}
|
||||
|
||||
if (hasInjectionRiskChars(workspaceName)) {
|
||||
return "Workspace name cannot contain special characters like < > ' \" { } [ ] * ^ ! # %";
|
||||
}
|
||||
|
||||
if (!COMPANY_NAME_REGEX.test(workspaceName)) {
|
||||
return "Workspace name can only contain letters, numbers, spaces, hyphens, and underscores";
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Validates URL slugs and identifiers
|
||||
* @param {string} slug - Slug to validate
|
||||
* @returns {boolean | string} true if valid, error message if invalid
|
||||
* @example
|
||||
* validateSlug("my-workspace") // returns true
|
||||
* validateSlug("my_workspace_123") // returns true
|
||||
* validateSlug("my workspace") // returns error message (spaces not allowed)
|
||||
*/
|
||||
export const validateSlug = (slug: string): boolean | string => {
|
||||
if (!slug || slug.trim() === "") {
|
||||
return "Slug is required";
|
||||
}
|
||||
|
||||
if (slug.length > 48) {
|
||||
return "Slug must be 48 characters or less";
|
||||
}
|
||||
|
||||
if (hasInjectionRiskChars(slug)) {
|
||||
return "Slug cannot contain special characters like < > ' \" { } [ ] * ^ ! # %";
|
||||
}
|
||||
|
||||
if (!SLUG_REGEX.test(slug)) {
|
||||
return "Slug can only contain letters, numbers, hyphens, and underscores";
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Checks if a string contains any injection-risk characters
|
||||
* @param {string} input - String to check
|
||||
* @returns {boolean} true if injection-risk characters found
|
||||
* @example
|
||||
* hasInjectionRiskChars("Hello World") // returns false
|
||||
* hasInjectionRiskChars("Hello<script>") // returns true
|
||||
*/
|
||||
export const hasInjectionRiskChars = (input: string): boolean => {
|
||||
const injectionRiskPattern = /[<>'"{}[\]*^!#%]/;
|
||||
return injectionRiskPattern.test(input);
|
||||
};
|
||||
Reference in New Issue
Block a user