feat: saml e2e improvements (#8158)

This commit is contained in:
Alejandro Bailo
2025-07-02 11:57:56 +02:00
committed by GitHub
parent bf58728d29
commit 5798321dc6
5 changed files with 127 additions and 42 deletions
+34 -12
View File
@@ -1,20 +1,9 @@
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { apiBaseUrl, getAuthHeaders, parseStringify } from "@/lib/helper";
const samlConfigFormSchema = z.object({
email_domain: z
.string()
.trim()
.min(1, { message: "Email domain is required" }),
metadata_xml: z
.string()
.trim()
.min(1, { message: "Metadata XML is required" }),
});
import { samlConfigFormSchema } from "@/types/formSchemas";
export const createSamlConfig = async (_prevState: any, formData: FormData) => {
const headers = await getAuthHeaders({ contentType: true });
@@ -156,6 +145,39 @@ export const getSamlConfig = async () => {
}
};
export const deleteSamlConfig = async (id: string) => {
const headers = await getAuthHeaders({ contentType: true });
try {
const url = new URL(`${apiBaseUrl}/saml-config/${id}`);
const response = await fetch(url.toString(), {
method: "DELETE",
headers,
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.errors?.[0]?.detail ||
`Failed to delete SAML config: ${response.statusText}`,
);
}
revalidatePath("/integrations");
return { success: "SAML configuration deleted successfully!" };
} catch (error) {
console.error("Error deleting SAML config:", error);
return {
errors: {
general:
error instanceof Error
? error.message
: "Error deleting SAML configuration. Please try again.",
},
};
}
};
export const initiateSamlAuth = async (email: string) => {
try {
const response = await fetch(`${apiBaseUrl}/auth/saml/initiate/`, {
+1 -1
View File
@@ -85,7 +85,7 @@ const SSRDataUser = async () => {
</div>
</div>
<div className="w-full pr-0 lg:w-2/3 xl:w-1/2 xl:pr-3">
<SamlIntegrationCard id={samlConfig.data[0]?.id} />
<SamlIntegrationCard samlConfig={samlConfig?.data?.[0]} />
</div>
</div>
);
@@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
import { useFormState } from "react-dom";
@@ -13,16 +14,18 @@ import { apiBaseUrl } from "@/lib";
export const SamlConfigForm = ({
setIsOpen,
id,
samlConfig,
}: {
setIsOpen: Dispatch<SetStateAction<boolean>>;
id: string;
samlConfig?: any;
}) => {
const [state, formAction, isPending] = useFormState(
id ? updateSamlConfig : createSamlConfig,
samlConfig?.id ? updateSamlConfig : createSamlConfig,
null,
);
const [emailDomain, setEmailDomain] = useState("");
const [emailDomain, setEmailDomain] = useState(
samlConfig?.attributes?.email_domain || "",
);
const [uploadedFile, setUploadedFile] = useState<{
name: string;
uploaded: boolean;
@@ -126,7 +129,7 @@ export const SamlConfigForm = ({
return (
<form ref={formRef} action={formAction} className="flex flex-col space-y-2">
<input type="hidden" name="id" value={id} />
<input type="hidden" name="id" value={samlConfig?.id || ""} />
<CustomServerInput
name="email_domain"
label="Email Domain"
@@ -170,17 +173,6 @@ export const SamlConfigForm = ({
/>
</div>
<div>
<span className="mb-2 block text-sm font-medium text-default-500">
Name ID Format:
</span>
<SnippetChip
value="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
ariaLabel="Copy Name ID Format to clipboard"
className="w-full"
/>
</div>
<div>
<span className="mb-2 block text-sm font-medium text-default-500">
Supported Assertion Attributes:
@@ -195,7 +187,11 @@ export const SamlConfigForm = ({
<strong>Note:</strong> The userType attribute will be used to
assign the user&apos;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.
roles on the{" "}
<Link href="/roles">
<span className="underline">Roles</span>
</Link>{" "}
page.
</p>
</div>
</div>
@@ -253,7 +249,10 @@ export const SamlConfigForm = ({
{state?.errors?.metadata_xml}
</span>
</div>
<FormButtons setIsOpen={setIsOpen} submitText={id ? "Update" : "Save"} />
<FormButtons
setIsOpen={setIsOpen}
submitText={samlConfig?.id ? "Update" : "Save"}
/>
</form>
);
};
@@ -2,15 +2,50 @@
import { Card, CardBody, CardHeader } from "@nextui-org/react";
import { Link } from "@nextui-org/react";
import { CheckIcon } from "lucide-react";
import { CheckIcon, Trash2Icon } from "lucide-react";
import { useState } from "react";
import { deleteSamlConfig } from "@/actions/integrations";
import { useToast } from "@/components/ui";
import { CustomAlertModal, CustomButton } from "@/components/ui/custom";
import { SamlConfigForm } from "./forms";
export const SamlIntegrationCard = ({ id }: { id: string }) => {
export const SamlIntegrationCard = ({ samlConfig }: { samlConfig?: any }) => {
const [isSamlModalOpen, setIsSamlModalOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const { toast } = useToast();
const id = samlConfig?.id;
const handleRemoveSaml = async () => {
if (!id) return;
setIsDeleting(true);
try {
const result = await deleteSamlConfig(id);
if (result.success) {
toast({
title: "SAML configuration removed",
description: result.success,
});
} else if (result.errors?.general) {
toast({
variant: "destructive",
title: "Error removing SAML configuration",
description: result.errors.general,
});
}
} catch (error) {
toast({
variant: "destructive",
title: "Error",
description: "Failed to remove SAML configuration. Please try again.",
});
} finally {
setIsDeleting(false);
}
};
return (
<>
@@ -19,7 +54,10 @@ export const SamlIntegrationCard = ({ id }: { id: string }) => {
onOpenChange={setIsSamlModalOpen}
title="Configure SAML SSO"
>
<SamlConfigForm setIsOpen={setIsSamlModalOpen} id={id} />
<SamlConfigForm
setIsOpen={setIsSamlModalOpen}
samlConfig={samlConfig}
/>
</CustomAlertModal>
<Card className="dark:bg-prowler-blue-400">
@@ -56,14 +94,29 @@ export const SamlIntegrationCard = ({ id }: { id: string }) => {
{id ? "Enabled" : "Disabled"}
</span>
</div>
<CustomButton
size="sm"
ariaLabel="Add SAML SSO"
color="action"
onPress={() => setIsSamlModalOpen(true)}
>
{id ? "Update" : "Enable"}
</CustomButton>
<div className="flex gap-2">
<CustomButton
size="sm"
ariaLabel="Configure SAML SSO"
color="action"
onPress={() => setIsSamlModalOpen(true)}
>
{id ? "Update" : "Enable"}
</CustomButton>
{id && (
<CustomButton
size="sm"
ariaLabel="Remove SAML SSO"
color="danger"
variant="bordered"
isLoading={isDeleting}
startContent={!isDeleting ? <Trash2Icon size={16} /> : null}
onPress={handleRemoveSaml}
>
Remove
</CustomButton>
)}
</div>
</div>
</CardBody>
</Card>
+11
View File
@@ -309,3 +309,14 @@ export const editUserFormSchema = () =>
userId: z.string(),
role: z.string().optional(),
});
export const samlConfigFormSchema = z.object({
email_domain: z
.string()
.trim()
.min(1, { message: "Email domain is required" }),
metadata_xml: z
.string()
.trim()
.min(1, { message: "Metadata XML is required" }),
});