From b1b8a3ecb9814c84caab953bb6ae3c06f2a696af Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Wed, 12 Nov 2025 09:58:11 -0800 Subject: [PATCH 1/2] can end in non alphanumeric --- taco/internal/domain/organization.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/taco/internal/domain/organization.go b/taco/internal/domain/organization.go index 82f064e1d..92fd93ea8 100644 --- a/taco/internal/domain/organization.go +++ b/taco/internal/domain/organization.go @@ -14,8 +14,8 @@ var ( ErrInvalidOrgID = errors.New("invalid organization ID format") ) -// OrgIDPattern defines valid organization ID format: alphanumeric, hyphens, underscores, spaces, colons, periods -var OrgIDPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_\- :.]*[a-zA-Z0-9]$`) +// OrgIDPattern defines valid organization ID format: must start with alphanumeric, can contain alphanumeric, hyphens, underscores, spaces, colons, periods +var OrgIDPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_\- :.]*$`) // ============================================ // Domain Models @@ -96,7 +96,7 @@ func ValidateOrgID(orgID string) error { return fmt.Errorf("%w: must be at most 255 characters", ErrInvalidOrgID) } if !OrgIDPattern.MatchString(orgID) { - return fmt.Errorf("%w: must contain only letters, numbers, hyphens, underscores, spaces, colons, and periods", ErrInvalidOrgID) + return fmt.Errorf("%w: must start with a letter or number, and can only contain letters, numbers, hyphens, underscores, spaces, colons, and periods", ErrInvalidOrgID) } return nil } From 8041a2cfe17af61ac2752389e497e6891d7005c0 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Wed, 12 Nov 2025 12:17:16 -0800 Subject: [PATCH 2/2] reflect change in the ui as well --- .../CreateOrganisationButtonWOS.tsx | 100 ++++++++++++++---- 1 file changed, 81 insertions(+), 19 deletions(-) diff --git a/ui/src/components/CreateOrganisationButtonWOS.tsx b/ui/src/components/CreateOrganisationButtonWOS.tsx index 7c5fb4dbc..a0d5166b0 100644 --- a/ui/src/components/CreateOrganisationButtonWOS.tsx +++ b/ui/src/components/CreateOrganisationButtonWOS.tsx @@ -6,36 +6,91 @@ import { Label } from "@/components/ui/label"; import { useState } from "react"; import { useToast } from "@/hooks/use-toast"; +// Organization name validation to match backend rules +const ORG_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_\- :.]*$/; +const MIN_LENGTH = 3; +const MAX_LENGTH = 255; + +function validateOrgName(name: string): string | null { + const trimmed = name.trim(); + + if (trimmed.length < MIN_LENGTH) { + return `Organization name must be at least ${MIN_LENGTH} characters`; + } + + if (trimmed.length > MAX_LENGTH) { + return `Organization name must be at most ${MAX_LENGTH} characters`; + } + + if (!ORG_NAME_PATTERN.test(trimmed)) { + return "Organization name must start with a letter or number, and can only contain letters, numbers, hyphens, underscores, spaces, colons, and periods"; + } + + return null; +} export default function CreateOrganizationBtn({ userId, email }: { userId: string, email: string }) { const [name, setName] = useState(""); + const [validationError, setValidationError] = useState(null); const [open, setOpen] = useState(false); const { toast } = useToast(); + + const handleNameChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setName(value); + + // Clear validation error when user starts typing again + if (validationError) { + setValidationError(null); + } + }; + + const handleOpenChange = (isOpen: boolean) => { + setOpen(isOpen); + // Reset form when dialog is closed + if (!isOpen) { + setName(""); + setValidationError(null); + } + }; + async function handleSubmit(e: React.FormEvent) { e.preventDefault(); - try { - const organization = await createOrganization({ data: { name: name, userId: userId, email: email } }); - toast({ - title: "Organization created", - description: "The page will now reload to refresh organisations list. To use this new organization, select it from the list.", - duration: 5000, - variant: "default" - }); - setOpen(false); - window.setTimeout(() => { - window.location.reload(); - }, 5000); - } catch (error) { - console.error("Failed to create organization:", error); - } + // Validate before submitting + const error = validateOrgName(name); + if (error) { + setValidationError(error); + return; + } + try { + const organization = await createOrganization({ data: { name: name.trim(), userId: userId, email: email } }); + toast({ + title: "Organization created", + description: "The page will now reload to refresh organisations list. To use this new organization, select it from the list.", + duration: 5000, + variant: "default" + }); + handleOpenChange(false); // Close dialog and reset form + window.setTimeout(() => { + window.location.reload(); + }, 5000); + } catch (error) { + console.error("Failed to create organization:", error); + toast({ + title: "Failed to create organization", + description: error instanceof Error ? error.message : "An error occurred while creating the organization", + duration: 5000, + variant: "destructive" + }); + } } return ( - + - + @@ -45,15 +100,22 @@ export default function CreateOrganizationBtn({ userId, email }: { userId: strin
-
+
setName(e.target.value)} + onChange={handleNameChange} placeholder="Enter organization name" + className={validationError ? "border-destructive" : ""} required /> + {validationError && ( +

{validationError}

+ )} +

+ Must be 3-255 characters, start with a letter or number +