"use client"; import { Dispatch, SetStateAction, useActionState, useEffect, useRef, useState, } from "react"; import { z } from "zod"; import { createSamlConfig, updateSamlConfig } from "@/actions/integrations"; import { AddIcon } from "@/components/icons"; import { useToast } from "@/components/ui"; import { CustomButton, CustomServerInput } from "@/components/ui/custom"; import { CustomLink } from "@/components/ui/custom/custom-link"; import { SnippetChip } from "@/components/ui/entities"; import { FormButtons } from "@/components/ui/form"; import { apiBaseUrl } from "@/lib"; const validateXMLContent = ( xmlContent: string, ): { isValid: boolean; error?: string } => { try { // Basic checks if (!xmlContent || !xmlContent.trim()) { return { isValid: false, error: "XML content is empty.", }; } const trimmedContent = xmlContent.trim(); // Check if it starts and ends with XML tags if (!trimmedContent.startsWith("<") || !trimmedContent.endsWith(">")) { return { isValid: false, error: "Content does not appear to be valid XML format.", }; } // Use DOMParser to validate XML structure const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlContent, "text/xml"); // Check for parser errors const parserError = xmlDoc.querySelector("parsererror"); if (parserError) { const errorText = parserError.textContent || "Unknown XML parsing error"; return { isValid: false, error: `XML parsing error: ${errorText.substring(0, 100)}...`, }; } // Check if the document has a root element if (!xmlDoc.documentElement) { return { isValid: false, error: "XML does not have a valid root element.", }; } // Optional: Check for basic SAML metadata structure const rootElement = xmlDoc.documentElement; const rootTagName = rootElement.tagName.toLowerCase(); // Check if it looks like SAML metadata (common root elements) const samlRootElements = [ "entitydescriptor", "entitiesDescriptor", "metadata", "md:entitydescriptor", "md:entitiesdescriptor", ]; const isSamlMetadata = samlRootElements.some((element) => rootTagName.includes(element.toLowerCase()), ); if (!isSamlMetadata) { // Check for common SAML namespace attributes const xmlString = xmlContent.toLowerCase(); const hasSamlNamespace = xmlString.includes("saml") || xmlString.includes("urn:oasis:names:tc:saml") || xmlString.includes("metadata"); if (!hasSamlNamespace) { return { isValid: false, error: "The XML file does not appear to be SAML metadata. Please ensure you're uploading the correct SAML metadata file from your Identity Provider.", }; } } return { isValid: true }; } catch (error) { return { isValid: false, error: error instanceof Error ? error.message : "Failed to validate XML content.", }; } }; export const SamlConfigForm = ({ setIsOpen, samlConfig, }: { setIsOpen: Dispatch>; samlConfig?: any; }) => { const [state, formAction, isPending] = useActionState( samlConfig?.id ? updateSamlConfig : createSamlConfig, null, ); const [emailDomain, setEmailDomain] = useState( samlConfig?.attributes?.email_domain || "", ); const [uploadedFile, setUploadedFile] = useState(null); const [clientErrors, setClientErrors] = useState<{ email_domain?: string | null; metadata_xml?: string | null; }>({}); const formRef = useRef(null); const { toast } = useToast(); // Client-side validation function const validateFields = (email: string, hasFile: boolean) => { // Validar cada campo por separado para poder limpiarlos individualmente const emailValidation = z .string() .trim() .min(1, { message: "Email domain is required" }) .safeParse(email); const metadataValidation = z .string() .trim() .min(1, { message: "Metadata XML is required" }) .safeParse(hasFile ? "dummy_xml_content" : ""); const newErrors = { email_domain: emailValidation.success ? null : emailValidation.error.issues[0]?.message, metadata_xml: metadataValidation.success ? null : metadataValidation.error.issues[0]?.message, }; setClientErrors(newErrors); }; useEffect(() => { if (state?.success) { toast({ title: "Configuration saved successfully", description: state.success, }); setIsOpen(false); } else if (state?.errors?.general) { toast({ variant: "destructive", title: "Oops! Something went wrong", description: state.errors.general, }); } }, [state, toast, setIsOpen]); const handleFileUpload = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) { setUploadedFile(null); validateFields(emailDomain, false); return; } // Check file extension const isXmlFile = file.name.toLowerCase().endsWith(".xml") || file.type === "text/xml" || file.type === "application/xml"; if (!isXmlFile) { toast({ variant: "destructive", title: "Invalid file type", description: "Please select a valid XML file (.xml extension).", }); // Clear the file input event.target.value = ""; setUploadedFile(null); validateFields(emailDomain, false); return; } const reader = new FileReader(); reader.onload = (e) => { const content = e.target?.result as string; // Comprehensive XML validation const xmlValidationResult = validateXMLContent(content); if (!xmlValidationResult.isValid) { toast({ variant: "destructive", title: "Invalid XML content", description: xmlValidationResult.error, }); // Clear the file input event.target.value = ""; setUploadedFile(null); validateFields(emailDomain, false); return; } // Set the XML content in a hidden input const xmlInput = document.getElementById( "metadata_xml", ) as HTMLInputElement; if (xmlInput) { xmlInput.value = content; } setUploadedFile(file); validateFields(emailDomain, true); toast({ title: "File uploaded successfully", description: "XML metadata file has been loaded.", }); }; reader.onerror = () => { toast({ variant: "destructive", title: "File read error", description: "Failed to read the selected file.", }); // Clear the file input event.target.value = ""; setUploadedFile(null); validateFields(emailDomain, false); }; reader.readAsText(file); }; const acsUrl = emailDomain ? `${apiBaseUrl}/accounts/saml/${emailDomain}/acs/` : `${apiBaseUrl}/accounts/saml/your-domain.com/acs/`; return (
Need help configuring SAML SSO?{" "} Read the docs
) => { const newValue = e.target.value; setEmailDomain(newValue); }} />

Identity Provider Configuration

ACS URL:
Audience:
Name ID Format: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
Supported Assertion Attributes:
  • • firstName
  • • lastName
  • • userType
  • • organization

Note: The userType attribute will be used to assign the user's role. If the role does not exist, one will be created with minimal permissions. You can assign permissions to roles on the{" "} Roles {" "} page.

Metadata XML File * { const fileInput = document.getElementById( "metadata_xml_file", ) as HTMLInputElement; if (fileInput) { fileInput.click(); } }} startContent={} className={`rounded-medium text-default-500 h-10 justify-start border-2 ${ ( clientErrors.metadata_xml === null ? undefined : clientErrors.metadata_xml || state?.errors?.metadata_xml ) ? "border-red-500" : uploadedFile ? "border-green-500 bg-green-50 dark:bg-green-900/20" : "border-default-200" }`} > {uploadedFile ? ( {uploadedFile.name} ) : ( "Choose File" )}

Upload your Identity Provider's SAML metadata XML file

{(() => { const finalError = clientErrors.metadata_xml === null ? undefined : clientErrors.metadata_xml || state?.errors?.metadata_xml; return finalError; })()}
); };