mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-16 10:17:33 +00:00
Edit path for SSO Settings
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { useMutation, UseMutationResult } from "@tanstack/react-query";
|
||||
import { updateSSOSettings } from "@/components/networking";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
export interface EditSSOSettingsParams {
|
||||
google_client_id?: string | null;
|
||||
google_client_secret?: string | null;
|
||||
microsoft_client_id?: string | null;
|
||||
microsoft_client_secret?: string | null;
|
||||
microsoft_tenant?: string | null;
|
||||
generic_client_id?: string | null;
|
||||
generic_client_secret?: string | null;
|
||||
generic_authorization_endpoint?: string | null;
|
||||
generic_token_endpoint?: string | null;
|
||||
generic_userinfo_endpoint?: string | null;
|
||||
proxy_base_url?: string | null;
|
||||
user_email?: string | null;
|
||||
sso_provider?: string | null;
|
||||
role_mappings?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface EditSSOSettingsResponse {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const useEditSSOSettings = (): UseMutationResult<EditSSOSettingsResponse, Error, EditSSOSettingsParams> => {
|
||||
const { accessToken } = useAuthorized();
|
||||
|
||||
return useMutation<EditSSOSettingsResponse, Error, EditSSOSettingsParams>({
|
||||
mutationFn: async (params: EditSSOSettingsParams) => {
|
||||
if (!accessToken) {
|
||||
throw new Error("Access token is required");
|
||||
}
|
||||
return await updateSSOSettings(accessToken, params);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -27,7 +27,14 @@ export interface SSOSettingsValues {
|
||||
proxy_base_url: string | null;
|
||||
user_email: string | null;
|
||||
ui_access_mode: string | null;
|
||||
role_mappings: string | null;
|
||||
role_mappings: {
|
||||
provider: string;
|
||||
group_claim: string;
|
||||
default_role: "internal_user" | "internal_user_viewer" | "proxy_admin" | "proxy_admin_viewer";
|
||||
roles: {
|
||||
[key: string]: string[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface SSOSettingsResponse {
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ describe("AddSSOSettingsModal", () => {
|
||||
const onCancel = vi.fn();
|
||||
const onSuccess = vi.fn();
|
||||
|
||||
render(<AddSSOSettingsModal isVisible={true} onCancel={onCancel} onSuccess={onSuccess} accessToken="test-token" />);
|
||||
render(<AddSSOSettingsModal isVisible={true} onCancel={onCancel} onSuccess={onSuccess} />);
|
||||
|
||||
expect(screen.getByText("SSO Provider")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
|
||||
+34
-211
@@ -1,145 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
import { updateSSOSettings } from "@/components/networking";
|
||||
import { parseErrorMessage } from "@/components/shared/errorUtils";
|
||||
import { TextInput } from "@tremor/react";
|
||||
import { Button as Button2, Form, Input, Modal, Select } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { Button, Form, Modal, Space } from "antd";
|
||||
import React from "react";
|
||||
import BaseSSOSettingsForm from "./BaseSSOSettingsForm";
|
||||
import { useEditSSOSettings } from "@/app/(dashboard)/hooks/sso/useEditSSOSettings";
|
||||
import { processSSOSettingsPayload } from "../utils";
|
||||
|
||||
interface AddSSOSettingsModalProps {
|
||||
isVisible: boolean;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
accessToken: string | null;
|
||||
}
|
||||
|
||||
const ssoProviderLogoMap: Record<string, string> = {
|
||||
google: "https://artificialanalysis.ai/img/logos/google_small.svg",
|
||||
microsoft: "https://upload.wikimedia.org/wikipedia/commons/a/a8/Microsoft_Azure_Logo.svg",
|
||||
okta: "https://www.okta.com/sites/default/files/Okta_Logo_BrightBlue_Medium.png",
|
||||
generic: "",
|
||||
};
|
||||
|
||||
// Define the SSO provider configuration type
|
||||
interface SSOProviderConfig {
|
||||
envVarMap: Record<string, string>;
|
||||
fields: Array<{
|
||||
label: string;
|
||||
name: string;
|
||||
placeholder?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
// Define configurations for each SSO provider
|
||||
const ssoProviderConfigs: Record<string, SSOProviderConfig> = {
|
||||
google: {
|
||||
envVarMap: {
|
||||
google_client_id: "GOOGLE_CLIENT_ID",
|
||||
google_client_secret: "GOOGLE_CLIENT_SECRET",
|
||||
},
|
||||
fields: [
|
||||
{ label: "Google Client ID", name: "google_client_id" },
|
||||
{ label: "Google Client Secret", name: "google_client_secret" },
|
||||
],
|
||||
},
|
||||
microsoft: {
|
||||
envVarMap: {
|
||||
microsoft_client_id: "MICROSOFT_CLIENT_ID",
|
||||
microsoft_client_secret: "MICROSOFT_CLIENT_SECRET",
|
||||
microsoft_tenant: "MICROSOFT_TENANT",
|
||||
},
|
||||
fields: [
|
||||
{ label: "Microsoft Client ID", name: "microsoft_client_id" },
|
||||
{ label: "Microsoft Client Secret", name: "microsoft_client_secret" },
|
||||
{ label: "Microsoft Tenant", name: "microsoft_tenant" },
|
||||
],
|
||||
},
|
||||
okta: {
|
||||
envVarMap: {
|
||||
generic_client_id: "GENERIC_CLIENT_ID",
|
||||
generic_client_secret: "GENERIC_CLIENT_SECRET",
|
||||
generic_authorization_endpoint: "GENERIC_AUTHORIZATION_ENDPOINT",
|
||||
generic_token_endpoint: "GENERIC_TOKEN_ENDPOINT",
|
||||
generic_userinfo_endpoint: "GENERIC_USERINFO_ENDPOINT",
|
||||
},
|
||||
fields: [
|
||||
{ label: "Generic Client ID", name: "generic_client_id" },
|
||||
{ label: "Generic Client Secret", name: "generic_client_secret" },
|
||||
{
|
||||
label: "Authorization Endpoint",
|
||||
name: "generic_authorization_endpoint",
|
||||
placeholder: "https://your-domain/authorize",
|
||||
},
|
||||
{ label: "Token Endpoint", name: "generic_token_endpoint", placeholder: "https://your-domain/token" },
|
||||
{
|
||||
label: "Userinfo Endpoint",
|
||||
name: "generic_userinfo_endpoint",
|
||||
placeholder: "https://your-domain/userinfo",
|
||||
},
|
||||
],
|
||||
},
|
||||
generic: {
|
||||
envVarMap: {
|
||||
generic_client_id: "GENERIC_CLIENT_ID",
|
||||
generic_client_secret: "GENERIC_CLIENT_SECRET",
|
||||
generic_authorization_endpoint: "GENERIC_AUTHORIZATION_ENDPOINT",
|
||||
generic_token_endpoint: "GENERIC_TOKEN_ENDPOINT",
|
||||
generic_userinfo_endpoint: "GENERIC_USERINFO_ENDPOINT",
|
||||
},
|
||||
fields: [
|
||||
{ label: "Generic Client ID", name: "generic_client_id" },
|
||||
{ label: "Generic Client Secret", name: "generic_client_secret" },
|
||||
{ label: "Authorization Endpoint", name: "generic_authorization_endpoint" },
|
||||
{ label: "Token Endpoint", name: "generic_token_endpoint" },
|
||||
{ label: "Userinfo Endpoint", name: "generic_userinfo_endpoint" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const AddSSOSettingsModal: React.FC<AddSSOSettingsModalProps> = ({ isVisible, onCancel, onSuccess, accessToken }) => {
|
||||
const AddSSOSettingsModal: React.FC<AddSSOSettingsModalProps> = ({ isVisible, onCancel, onSuccess }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const { mutateAsync, isPending } = useEditSSOSettings();
|
||||
|
||||
// Enhanced form submission handler
|
||||
const handleFormSubmit = async (formValues: Record<string, any>) => {
|
||||
if (!accessToken) {
|
||||
NotificationsManager.fromBackend("No access token available");
|
||||
return;
|
||||
}
|
||||
const payload = processSSOSettingsPayload(formValues);
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
// Save SSO settings using the new API
|
||||
await updateSSOSettings(accessToken, formValues);
|
||||
|
||||
NotificationsManager.success("SSO settings added successfully");
|
||||
|
||||
// Reset form and close modal
|
||||
form.resetFields();
|
||||
onSuccess();
|
||||
} catch (error: unknown) {
|
||||
NotificationsManager.fromBackend("Failed to save SSO settings: " + parseErrorMessage(error));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to render provider fields
|
||||
const renderProviderFields = (provider: string) => {
|
||||
const config = ssoProviderConfigs[provider];
|
||||
if (!config) return null;
|
||||
|
||||
return config.fields.map((field) => (
|
||||
<Form.Item
|
||||
key={field.name}
|
||||
label={field.label}
|
||||
name={field.name}
|
||||
rules={[{ required: true, message: `Please enter the ${field.label.toLowerCase()}` }]}
|
||||
>
|
||||
{field.name.includes("client") ? <Input.Password /> : <TextInput placeholder={field.placeholder} />}
|
||||
</Form.Item>
|
||||
));
|
||||
await mutateAsync(payload, {
|
||||
onSuccess: () => {
|
||||
NotificationsManager.success("SSO settings added successfully");
|
||||
onSuccess();
|
||||
},
|
||||
onError: (error) => {
|
||||
NotificationsManager.fromBackend("Failed to save SSO settings: " + parseErrorMessage(error));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
@@ -148,91 +39,23 @@ const AddSSOSettingsModal: React.FC<AddSSOSettingsModalProps> = ({ isVisible, on
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title="Add SSO" visible={isVisible} width={800} footer={null} onCancel={handleCancel}>
|
||||
<Form form={form} onFinish={handleFormSubmit} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} labelAlign="left">
|
||||
<Form.Item
|
||||
label="SSO Provider"
|
||||
name="sso_provider"
|
||||
rules={[{ required: true, message: "Please select an SSO provider" }]}
|
||||
>
|
||||
<Select>
|
||||
{Object.entries(ssoProviderLogoMap).map(([value, logo]) => (
|
||||
<Select.Option key={value} value={value}>
|
||||
<div style={{ display: "flex", alignItems: "center", padding: "4px 0" }}>
|
||||
{logo && (
|
||||
<img
|
||||
src={logo}
|
||||
alt={value}
|
||||
style={{ height: 24, width: 24, marginRight: 12, objectFit: "contain" }}
|
||||
/>
|
||||
)}
|
||||
<span>
|
||||
{value.toLowerCase() === "okta" ? "Okta / Auth0" : value.charAt(0).toUpperCase() + value.slice(1)}{" "}
|
||||
SSO
|
||||
</span>
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => prevValues.sso_provider !== currentValues.sso_provider}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const provider = getFieldValue("sso_provider");
|
||||
return provider ? renderProviderFields(provider) : null;
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Proxy Admin Email"
|
||||
name="user_email"
|
||||
rules={[{ required: true, message: "Please enter the email of the proxy admin" }]}
|
||||
>
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Proxy Base URL"
|
||||
name="proxy_base_url"
|
||||
normalize={(value) => value?.trim()}
|
||||
rules={[
|
||||
{ required: true, message: "Please enter the proxy base url" },
|
||||
{
|
||||
pattern: /^https?:\/\/.+/,
|
||||
message: "URL must start with http:// or https://",
|
||||
},
|
||||
{
|
||||
validator: (_, value) => {
|
||||
// Only check for trailing slash if the URL starts with http:// or https://
|
||||
if (value && /^https?:\/\/.+/.test(value) && value.endsWith("/")) {
|
||||
return Promise.reject("URL must not end with a trailing slash");
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TextInput placeholder="https://example.com" />
|
||||
</Form.Item>
|
||||
|
||||
<div
|
||||
style={{
|
||||
textAlign: "right",
|
||||
marginTop: "10px",
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
<Button2 onClick={handleCancel}>Cancel</Button2>
|
||||
<Button2 htmlType="submit" loading={isSubmitting}>
|
||||
Add SSO
|
||||
</Button2>
|
||||
</div>
|
||||
</Form>
|
||||
<Modal
|
||||
title="Add SSO"
|
||||
open={isVisible}
|
||||
width={800}
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={handleCancel} disabled={isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button loading={isPending} onClick={() => form.submit()}>
|
||||
{isPending ? "Adding..." : "Add SSO"}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
<BaseSSOSettingsForm form={form} onFormSubmit={handleFormSubmit} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
"use client";
|
||||
|
||||
import { TextInput } from "@tremor/react";
|
||||
import { Checkbox, Form, Input, Select } from "antd";
|
||||
import React from "react";
|
||||
import { ssoProviderLogoMap, ssoProviderDisplayNames } from "../constants";
|
||||
|
||||
export interface BaseSSOSettingsFormProps {
|
||||
form: any; // Replace with proper Form type if available
|
||||
onFormSubmit: (formValues: Record<string, any>) => Promise<void>;
|
||||
}
|
||||
|
||||
// Define the SSO provider configuration type
|
||||
export interface SSOProviderConfig {
|
||||
envVarMap: Record<string, string>;
|
||||
fields: Array<{
|
||||
label: string;
|
||||
name: string;
|
||||
placeholder?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
// Define configurations for each SSO provider
|
||||
export const ssoProviderConfigs: Record<string, SSOProviderConfig> = {
|
||||
google: {
|
||||
envVarMap: {
|
||||
google_client_id: "GOOGLE_CLIENT_ID",
|
||||
google_client_secret: "GOOGLE_CLIENT_SECRET",
|
||||
},
|
||||
fields: [
|
||||
{ label: "Google Client ID", name: "google_client_id" },
|
||||
{ label: "Google Client Secret", name: "google_client_secret" },
|
||||
],
|
||||
},
|
||||
microsoft: {
|
||||
envVarMap: {
|
||||
microsoft_client_id: "MICROSOFT_CLIENT_ID",
|
||||
microsoft_client_secret: "MICROSOFT_CLIENT_SECRET",
|
||||
microsoft_tenant: "MICROSOFT_TENANT",
|
||||
},
|
||||
fields: [
|
||||
{ label: "Microsoft Client ID", name: "microsoft_client_id" },
|
||||
{ label: "Microsoft Client Secret", name: "microsoft_client_secret" },
|
||||
{ label: "Microsoft Tenant", name: "microsoft_tenant" },
|
||||
],
|
||||
},
|
||||
okta: {
|
||||
envVarMap: {
|
||||
generic_client_id: "GENERIC_CLIENT_ID",
|
||||
generic_client_secret: "GENERIC_CLIENT_SECRET",
|
||||
generic_authorization_endpoint: "GENERIC_AUTHORIZATION_ENDPOINT",
|
||||
generic_token_endpoint: "GENERIC_TOKEN_ENDPOINT",
|
||||
generic_userinfo_endpoint: "GENERIC_USERINFO_ENDPOINT",
|
||||
},
|
||||
fields: [
|
||||
{ label: "Generic Client ID", name: "generic_client_id" },
|
||||
{ label: "Generic Client Secret", name: "generic_client_secret" },
|
||||
{
|
||||
label: "Authorization Endpoint",
|
||||
name: "generic_authorization_endpoint",
|
||||
placeholder: "https://your-domain/authorize",
|
||||
},
|
||||
{ label: "Token Endpoint", name: "generic_token_endpoint", placeholder: "https://your-domain/token" },
|
||||
{
|
||||
label: "Userinfo Endpoint",
|
||||
name: "generic_userinfo_endpoint",
|
||||
placeholder: "https://your-domain/userinfo",
|
||||
},
|
||||
],
|
||||
},
|
||||
generic: {
|
||||
envVarMap: {
|
||||
generic_client_id: "GENERIC_CLIENT_ID",
|
||||
generic_client_secret: "GENERIC_CLIENT_SECRET",
|
||||
generic_authorization_endpoint: "GENERIC_AUTHORIZATION_ENDPOINT",
|
||||
generic_token_endpoint: "GENERIC_TOKEN_ENDPOINT",
|
||||
generic_userinfo_endpoint: "GENERIC_USERINFO_ENDPOINT",
|
||||
},
|
||||
fields: [
|
||||
{ label: "Generic Client ID", name: "generic_client_id" },
|
||||
{ label: "Generic Client Secret", name: "generic_client_secret" },
|
||||
{ label: "Authorization Endpoint", name: "generic_authorization_endpoint" },
|
||||
{ label: "Token Endpoint", name: "generic_token_endpoint" },
|
||||
{ label: "Userinfo Endpoint", name: "generic_userinfo_endpoint" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Helper function to render provider fields
|
||||
export const renderProviderFields = (provider: string) => {
|
||||
const config = ssoProviderConfigs[provider];
|
||||
if (!config) return null;
|
||||
|
||||
return config.fields.map((field) => (
|
||||
<Form.Item
|
||||
key={field.name}
|
||||
label={field.label}
|
||||
name={field.name}
|
||||
rules={[{ required: true, message: `Please enter the ${field.label.toLowerCase()}` }]}
|
||||
>
|
||||
{field.name.includes("client") ? <Input.Password /> : <TextInput placeholder={field.placeholder} />}
|
||||
</Form.Item>
|
||||
));
|
||||
};
|
||||
|
||||
const BaseSSOSettingsForm: React.FC<BaseSSOSettingsFormProps> = ({ form, onFormSubmit }) => {
|
||||
return (
|
||||
<div>
|
||||
<Form form={form} onFinish={onFormSubmit} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} labelAlign="left">
|
||||
<Form.Item
|
||||
label="SSO Provider"
|
||||
name="sso_provider"
|
||||
rules={[{ required: true, message: "Please select an SSO provider" }]}
|
||||
>
|
||||
<Select>
|
||||
{Object.entries(ssoProviderLogoMap).map(([value, logo]) => (
|
||||
<Select.Option key={value} value={value}>
|
||||
<div style={{ display: "flex", alignItems: "center", padding: "4px 0" }}>
|
||||
{logo && (
|
||||
<img
|
||||
src={logo}
|
||||
alt={value}
|
||||
style={{ height: 24, width: 24, marginRight: 12, objectFit: "contain" }}
|
||||
/>
|
||||
)}
|
||||
<span>
|
||||
{ssoProviderDisplayNames[value] || value.charAt(0).toUpperCase() + value.slice(1) + " SSO"}
|
||||
</span>
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => prevValues.sso_provider !== currentValues.sso_provider}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const provider = getFieldValue("sso_provider");
|
||||
return provider ? renderProviderFields(provider) : null;
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Proxy Admin Email"
|
||||
name="user_email"
|
||||
rules={[{ required: true, message: "Please enter the email of the proxy admin" }]}
|
||||
>
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Proxy Base URL"
|
||||
name="proxy_base_url"
|
||||
normalize={(value) => value?.trim()}
|
||||
rules={[
|
||||
{ required: true, message: "Please enter the proxy base url" },
|
||||
{
|
||||
pattern: /^https?:\/\/.+/,
|
||||
message: "URL must start with http:// or https://",
|
||||
},
|
||||
{
|
||||
validator: (_, value) => {
|
||||
// Only check for trailing slash if the URL starts with http:// or https://
|
||||
if (value && /^https?:\/\/.+/.test(value) && value.endsWith("/")) {
|
||||
return Promise.reject("URL must not end with a trailing slash");
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TextInput placeholder="https://example.com" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => prevValues.sso_provider !== currentValues.sso_provider}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const provider = getFieldValue("sso_provider");
|
||||
return provider === "okta" || provider === "generic" ? (
|
||||
<Form.Item label="Use Role Mappings" name="use_role_mappings" valuePropName="checked">
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
) : null;
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => prevValues.use_role_mappings !== currentValues.use_role_mappings}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const useRoleMappings = getFieldValue("use_role_mappings");
|
||||
return useRoleMappings ? (
|
||||
<Form.Item
|
||||
label="Group Claim"
|
||||
name="group_claim"
|
||||
rules={[{ required: true, message: "Please enter the group claim" }]}
|
||||
>
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
) : null;
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => prevValues.use_role_mappings !== currentValues.use_role_mappings}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const useRoleMappings = getFieldValue("use_role_mappings");
|
||||
return useRoleMappings ? (
|
||||
<>
|
||||
<Form.Item label="Default Role" name="default_role" initialValue="Internal User">
|
||||
<Select>
|
||||
<Select.Option value="internal_user_viewer">Internal Viewer</Select.Option>
|
||||
<Select.Option value="internal_user">Internal User</Select.Option>
|
||||
<Select.Option value="proxy_admin_viewer">Admin Viewer</Select.Option>
|
||||
<Select.Option value="proxy_admin">Proxy Admin</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Proxy Admin Teams" name="proxy_admin_teams">
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Admin Viewer Teams" name="admin_viewer_teams">
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Internal User Teams" name="internal_user_teams">
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Internal Viewer Teams" name="internal_viewer_teams">
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
</>
|
||||
) : null;
|
||||
}}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseSSOSettingsForm;
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Form, Modal, Space } from "antd";
|
||||
import React, { useEffect } from "react";
|
||||
import BaseSSOSettingsForm from "./BaseSSOSettingsForm";
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
import { parseErrorMessage } from "@/components/shared/errorUtils";
|
||||
import { processSSOSettingsPayload } from "../utils";
|
||||
import { useSSOSettings } from "@/app/(dashboard)/hooks/sso/useSSOSettings";
|
||||
import { useEditSSOSettings } from "@/app/(dashboard)/hooks/sso/useEditSSOSettings";
|
||||
|
||||
interface EditSSOSettingsModalProps {
|
||||
isVisible: boolean;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const EditSSOSettingsModal: React.FC<EditSSOSettingsModalProps> = ({ isVisible, onCancel, onSuccess }) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// Use react-query hooks for SSO settings
|
||||
const ssoSettings = useSSOSettings();
|
||||
const { mutateAsync, isPending } = useEditSSOSettings();
|
||||
useEffect(() => {
|
||||
if (isVisible && ssoSettings.data && ssoSettings.data.values) {
|
||||
const ssoData = ssoSettings.data;
|
||||
console.log("Raw SSO data received:", ssoData); // Debug log
|
||||
console.log("SSO values:", ssoData.values); // Debug log
|
||||
console.log("user_email from API:", ssoData.values.user_email); // Debug log
|
||||
|
||||
// Determine which SSO provider is configured
|
||||
let selectedProvider = null;
|
||||
if (ssoData.values.google_client_id) {
|
||||
selectedProvider = "google";
|
||||
} else if (ssoData.values.microsoft_client_id) {
|
||||
selectedProvider = "microsoft";
|
||||
} else if (ssoData.values.generic_client_id) {
|
||||
// Check if it looks like Okta based on endpoints
|
||||
if (
|
||||
ssoData.values.generic_authorization_endpoint?.includes("okta") ||
|
||||
ssoData.values.generic_authorization_endpoint?.includes("auth0")
|
||||
) {
|
||||
selectedProvider = "okta";
|
||||
} else {
|
||||
selectedProvider = "generic";
|
||||
}
|
||||
}
|
||||
|
||||
// Extract role mappings if they exist
|
||||
let roleMappingFields = {};
|
||||
if (ssoData.values.role_mappings) {
|
||||
const roleMappings = ssoData.values.role_mappings;
|
||||
|
||||
// Helper function to join arrays into comma-separated strings
|
||||
const joinTeams = (teams: string[] | undefined): string => {
|
||||
if (!teams || teams.length === 0) return "";
|
||||
return teams.join(", ");
|
||||
};
|
||||
|
||||
roleMappingFields = {
|
||||
use_role_mappings: true,
|
||||
group_claim: roleMappings.group_claim,
|
||||
default_role: roleMappings.default_role || "internal_user",
|
||||
proxy_admin_teams: joinTeams(roleMappings.roles?.proxy_admin),
|
||||
admin_viewer_teams: joinTeams(roleMappings.roles?.proxy_admin_viewer),
|
||||
internal_user_teams: joinTeams(roleMappings.roles?.internal_user),
|
||||
internal_viewer_teams: joinTeams(roleMappings.roles?.internal_user_viewer),
|
||||
};
|
||||
}
|
||||
|
||||
// Set form values with existing data (excluding UI access control fields)
|
||||
const formValues = {
|
||||
sso_provider: selectedProvider,
|
||||
...ssoData.values,
|
||||
...roleMappingFields,
|
||||
};
|
||||
|
||||
console.log("Setting form values:", formValues); // Debug log
|
||||
|
||||
// Clear form first, then set values with a small delay to ensure proper initialization
|
||||
form.resetFields();
|
||||
setTimeout(() => {
|
||||
form.setFieldsValue(formValues);
|
||||
console.log("Form values set, current form values:", form.getFieldsValue()); // Debug log
|
||||
}, 100);
|
||||
}
|
||||
}, [isVisible, ssoSettings.data, form]);
|
||||
|
||||
// Enhanced form submission handler
|
||||
const handleFormSubmit = async (formValues: Record<string, any>) => {
|
||||
const payload = processSSOSettingsPayload(formValues);
|
||||
|
||||
await mutateAsync(payload, {
|
||||
onSuccess: () => {
|
||||
NotificationsManager.success("SSO settings updated successfully");
|
||||
onSuccess();
|
||||
},
|
||||
onError: (error) => {
|
||||
NotificationsManager.fromBackend("Failed to save SSO settings: " + parseErrorMessage(error));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Edit SSO Settings"
|
||||
open={isVisible}
|
||||
width={800}
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={handleCancel} disabled={isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button loading={isPending} onClick={() => form.submit()}>
|
||||
{isPending ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
<BaseSSOSettingsForm form={form} onFormSubmit={handleFormSubmit} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditSSOSettingsModal;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "antd";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
|
||||
export default function RedactableField({
|
||||
defaultHidden = true,
|
||||
value,
|
||||
}: {
|
||||
defaultHidden?: boolean;
|
||||
value: string | null;
|
||||
}) {
|
||||
const [isHidden, setIsHidden] = useState(defaultHidden);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-gray-600 flex-1">
|
||||
{value ? (
|
||||
isHidden ? (
|
||||
"•".repeat(value.length)
|
||||
) : (
|
||||
value
|
||||
)
|
||||
) : (
|
||||
<span className="text-gray-400 italic">Not configured</span>
|
||||
)}
|
||||
</span>
|
||||
{value && (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={isHidden ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
|
||||
onClick={() => setIsHidden(!isHidden)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+52
-32
@@ -3,11 +3,14 @@
|
||||
import { useSSOSettings, type SSOSettingsValues } from "@/app/(dashboard)/hooks/sso/useSSOSettings";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { Badge, Button, Card, Descriptions, Space, Typography } from "antd";
|
||||
import { Shield, Trash2 } from "lucide-react";
|
||||
import { Shield, Trash2, Edit } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import AddSSOSettingsModal from "./Modals/AddSSOSettingsModal";
|
||||
import DeleteSSOSettingsModal from "./Modals/DeleteSSOSettingsModal";
|
||||
import EditSSOSettingsModal from "./Modals/EditSSOSettingsModal";
|
||||
import SSOSettingsEmptyPlaceholder from "./SSOSettingsEmptyPlaceholder";
|
||||
import RedactableField from "./RedactableField";
|
||||
import { ssoProviderLogoMap, ssoProviderDisplayNames } from "./constants";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
@@ -16,6 +19,7 @@ export default function SSOSettings() {
|
||||
const { accessToken } = useAuthorized();
|
||||
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
|
||||
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
|
||||
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
|
||||
const isSSOConfigured =
|
||||
Boolean(ssoSettings?.values.google_client_id) ||
|
||||
Boolean(ssoSettings?.values.microsoft_client_id) ||
|
||||
@@ -39,12 +43,6 @@ export default function SSOSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
const renderRedactedValue = (value?: string | null) => (
|
||||
<span className="font-mono text-gray-600">
|
||||
{value ? "••••••••••••••••••••••••••••••••" : <span className="text-gray-400 italic">Not configured</span>}
|
||||
</span>
|
||||
);
|
||||
|
||||
const renderEndpointValue = (value?: string | null) => (
|
||||
<span className="font-mono text-gray-600 text-sm break-all">
|
||||
{value || <span className="text-gray-400 italic">Not configured</span>}
|
||||
@@ -67,44 +65,44 @@ export default function SSOSettings() {
|
||||
|
||||
const providerConfigs = {
|
||||
google: {
|
||||
providerText: "Google OAuth",
|
||||
providerText: ssoProviderDisplayNames.google,
|
||||
fields: [
|
||||
{
|
||||
label: "Client ID (Redacted)",
|
||||
render: (values: SSOSettingsValues) => renderRedactedValue(values.google_client_id),
|
||||
label: "Client ID",
|
||||
render: (values: SSOSettingsValues) => <RedactableField value={values.google_client_id} />,
|
||||
},
|
||||
{
|
||||
label: "Client Secret (Redacted)",
|
||||
render: (values: SSOSettingsValues) => renderRedactedValue(values.google_client_secret),
|
||||
label: "Client Secret",
|
||||
render: (values: SSOSettingsValues) => <RedactableField value={values.google_client_secret} />,
|
||||
},
|
||||
{ label: "Proxy Base URL", render: (values: SSOSettingsValues) => renderSimpleValue(values.proxy_base_url) },
|
||||
],
|
||||
},
|
||||
microsoft: {
|
||||
providerText: "Microsoft OAuth",
|
||||
providerText: ssoProviderDisplayNames.microsoft,
|
||||
fields: [
|
||||
{
|
||||
label: "Client ID (Redacted)",
|
||||
render: (values: SSOSettingsValues) => renderRedactedValue(values.microsoft_client_id),
|
||||
label: "Client ID",
|
||||
render: (values: SSOSettingsValues) => <RedactableField value={values.microsoft_client_id} />,
|
||||
},
|
||||
{
|
||||
label: "Client Secret (Redacted)",
|
||||
render: (values: SSOSettingsValues) => renderRedactedValue(values.microsoft_client_secret),
|
||||
label: "Client Secret",
|
||||
render: (values: SSOSettingsValues) => <RedactableField value={values.microsoft_client_secret} />,
|
||||
},
|
||||
{ label: "Tenant", render: (values: any) => renderSimpleValue(values.microsoft_tenant) },
|
||||
{ label: "Proxy Base URL", render: (values: SSOSettingsValues) => renderSimpleValue(values.proxy_base_url) },
|
||||
],
|
||||
},
|
||||
okta: {
|
||||
providerText: "Okta/Auth0",
|
||||
providerText: ssoProviderDisplayNames.okta,
|
||||
fields: [
|
||||
{
|
||||
label: "Client ID (Redacted)",
|
||||
render: (values: SSOSettingsValues) => renderRedactedValue(values.generic_client_id),
|
||||
label: "Client ID",
|
||||
render: (values: SSOSettingsValues) => <RedactableField value={values.generic_client_id} />,
|
||||
},
|
||||
{
|
||||
label: "Client Secret (Redacted)",
|
||||
render: (values: SSOSettingsValues) => renderRedactedValue(values.generic_client_secret),
|
||||
label: "Client Secret",
|
||||
render: (values: SSOSettingsValues) => <RedactableField value={values.generic_client_secret} />,
|
||||
},
|
||||
{
|
||||
label: "Authorization Endpoint",
|
||||
@@ -122,15 +120,15 @@ export default function SSOSettings() {
|
||||
],
|
||||
},
|
||||
generic: {
|
||||
providerText: "Generic OAuth",
|
||||
providerText: ssoProviderDisplayNames.generic,
|
||||
fields: [
|
||||
{
|
||||
label: "Client ID (Redacted)",
|
||||
render: (values: SSOSettingsValues) => renderRedactedValue(values.generic_client_id),
|
||||
label: "Client ID",
|
||||
render: (values: SSOSettingsValues) => <RedactableField value={values.generic_client_id} />,
|
||||
},
|
||||
{
|
||||
label: "Client Secret (Redacted)",
|
||||
render: (values: SSOSettingsValues) => renderRedactedValue(values.generic_client_secret),
|
||||
label: "Client Secret",
|
||||
render: (values: SSOSettingsValues) => <RedactableField value={values.generic_client_secret} />,
|
||||
},
|
||||
{
|
||||
label: "Authorization Endpoint",
|
||||
@@ -160,7 +158,16 @@ export default function SSOSettings() {
|
||||
return (
|
||||
<Descriptions bordered {...descriptionsConfig}>
|
||||
<Descriptions.Item label="Provider">
|
||||
<Badge status="success" text={config.providerText} />
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
{ssoProviderLogoMap[selectedProvider] && (
|
||||
<img
|
||||
src={ssoProviderLogoMap[selectedProvider]}
|
||||
alt={selectedProvider}
|
||||
style={{ height: 24, width: 24, objectFit: "contain" }}
|
||||
/>
|
||||
)}
|
||||
<span>{config.providerText}</span>
|
||||
</div>
|
||||
</Descriptions.Item>
|
||||
{config.fields.map((field, index) => (
|
||||
<Descriptions.Item key={index} label={field.label}>
|
||||
@@ -186,9 +193,14 @@ export default function SSOSettings() {
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{isSSOConfigured && (
|
||||
<Button danger icon={<Trash2 className="w-4 h-4" />} onClick={() => setIsDeleteModalVisible(true)}>
|
||||
Delete SSO Settings
|
||||
</Button>
|
||||
<>
|
||||
<Button icon={<Edit className="w-4 h-4" />} onClick={() => setIsEditModalVisible(true)}>
|
||||
Edit SSO Settings
|
||||
</Button>
|
||||
<Button danger icon={<Trash2 className="w-4 h-4" />} onClick={() => setIsDeleteModalVisible(true)}>
|
||||
Delete SSO Settings
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -214,7 +226,15 @@ export default function SSOSettings() {
|
||||
setIsAddModalVisible(false);
|
||||
refetch();
|
||||
}}
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
|
||||
<EditSSOSettingsModal
|
||||
isVisible={isEditModalVisible}
|
||||
onCancel={() => setIsEditModalVisible(false)}
|
||||
onSuccess={() => {
|
||||
setIsEditModalVisible(false);
|
||||
refetch();
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// SSO Provider logos
|
||||
export const ssoProviderLogoMap: Record<string, string> = {
|
||||
google: "https://artificialanalysis.ai/img/logos/google_small.svg",
|
||||
microsoft: "https://upload.wikimedia.org/wikipedia/commons/a/a8/Microsoft_Azure_Logo.svg",
|
||||
okta: "https://www.okta.com/sites/default/files/Okta_Logo_BrightBlue_Medium.png",
|
||||
generic: "",
|
||||
};
|
||||
|
||||
// SSO Provider display names (consistent between select dropdown and table)
|
||||
export const ssoProviderDisplayNames: Record<string, string> = {
|
||||
google: "Google SSO",
|
||||
microsoft: "Microsoft SSO",
|
||||
okta: "Okta / Auth0 SSO",
|
||||
generic: "Generic SSO",
|
||||
};
|
||||
@@ -0,0 +1,274 @@
|
||||
import { processSSOSettingsPayload } from "./utils";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("processSSOSettingsPayload", () => {
|
||||
describe("without role mappings", () => {
|
||||
it("should return all fields except role mapping fields when use_role_mappings is false", () => {
|
||||
const formValues = {
|
||||
proxy_admin_teams: "team1, team2",
|
||||
admin_viewer_teams: "viewer1",
|
||||
internal_user_teams: "user1",
|
||||
internal_viewer_teams: "viewer1",
|
||||
default_role: "proxy_admin",
|
||||
group_claim: "groups",
|
||||
use_role_mappings: false,
|
||||
other_field: "value",
|
||||
another_field: 123,
|
||||
};
|
||||
|
||||
const result = processSSOSettingsPayload(formValues);
|
||||
|
||||
expect(result).toEqual({
|
||||
other_field: "value",
|
||||
another_field: 123,
|
||||
});
|
||||
expect(result.role_mappings).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return all fields except role mapping fields when use_role_mappings is not present", () => {
|
||||
const formValues = {
|
||||
proxy_admin_teams: "team1",
|
||||
admin_viewer_teams: "viewer1",
|
||||
internal_user_teams: "user1",
|
||||
internal_viewer_teams: "viewer1",
|
||||
default_role: "proxy_admin",
|
||||
group_claim: "groups",
|
||||
other_field: "value",
|
||||
};
|
||||
|
||||
const result = processSSOSettingsPayload(formValues);
|
||||
|
||||
expect(result).toEqual({
|
||||
other_field: "value",
|
||||
});
|
||||
expect(result.role_mappings).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("with role mappings enabled", () => {
|
||||
it("should create role mappings with all team types populated", () => {
|
||||
const formValues = {
|
||||
proxy_admin_teams: "admin1, admin2",
|
||||
admin_viewer_teams: "viewer1, viewer2, viewer3",
|
||||
internal_user_teams: "user1",
|
||||
internal_viewer_teams: "internal_viewer1, internal_viewer2",
|
||||
default_role: "proxy_admin",
|
||||
group_claim: "groups",
|
||||
use_role_mappings: true,
|
||||
other_field: "value",
|
||||
};
|
||||
|
||||
const result = processSSOSettingsPayload(formValues);
|
||||
|
||||
expect(result.other_field).toBe("value");
|
||||
expect(result.role_mappings).toEqual({
|
||||
provider: "generic",
|
||||
group_claim: "groups",
|
||||
default_role: "proxy_admin",
|
||||
roles: {
|
||||
proxy_admin: ["admin1", "admin2"],
|
||||
proxy_admin_viewer: ["viewer1", "viewer2", "viewer3"],
|
||||
internal_user: ["user1"],
|
||||
internal_user_viewer: ["internal_viewer1", "internal_viewer2"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle empty team strings", () => {
|
||||
const formValues = {
|
||||
proxy_admin_teams: "",
|
||||
admin_viewer_teams: "",
|
||||
internal_user_teams: "",
|
||||
internal_viewer_teams: "",
|
||||
default_role: "internal_user",
|
||||
group_claim: "groups",
|
||||
use_role_mappings: true,
|
||||
};
|
||||
|
||||
const result = processSSOSettingsPayload(formValues);
|
||||
|
||||
expect(result.role_mappings.roles).toEqual({
|
||||
proxy_admin: [],
|
||||
proxy_admin_viewer: [],
|
||||
internal_user: [],
|
||||
internal_user_viewer: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle undefined team fields", () => {
|
||||
const formValues = {
|
||||
default_role: "internal_user_viewer",
|
||||
group_claim: "groups",
|
||||
use_role_mappings: true,
|
||||
};
|
||||
|
||||
const result = processSSOSettingsPayload(formValues);
|
||||
|
||||
expect(result.role_mappings.roles).toEqual({
|
||||
proxy_admin: [],
|
||||
proxy_admin_viewer: [],
|
||||
internal_user: [],
|
||||
internal_user_viewer: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle whitespace-only team strings", () => {
|
||||
const formValues = {
|
||||
proxy_admin_teams: " ",
|
||||
admin_viewer_teams: ", , ,",
|
||||
internal_user_teams: "user1, , user2",
|
||||
internal_viewer_teams: "viewer1, ,viewer2",
|
||||
default_role: "proxy_admin_viewer",
|
||||
group_claim: "groups",
|
||||
use_role_mappings: true,
|
||||
};
|
||||
|
||||
const result = processSSOSettingsPayload(formValues);
|
||||
|
||||
expect(result.role_mappings.roles).toEqual({
|
||||
proxy_admin: [],
|
||||
proxy_admin_viewer: [],
|
||||
internal_user: ["user1", "user2"],
|
||||
internal_user_viewer: ["viewer1", "viewer2"],
|
||||
});
|
||||
});
|
||||
|
||||
it("should trim whitespace from team names", () => {
|
||||
const formValues = {
|
||||
proxy_admin_teams: " admin1 , admin2 ",
|
||||
admin_viewer_teams: " viewer1 ",
|
||||
internal_user_teams: " user1 , user2 ",
|
||||
internal_viewer_teams: "viewer1,viewer2",
|
||||
default_role: "internal_user",
|
||||
group_claim: "groups",
|
||||
use_role_mappings: true,
|
||||
};
|
||||
|
||||
const result = processSSOSettingsPayload(formValues);
|
||||
|
||||
expect(result.role_mappings.roles).toEqual({
|
||||
proxy_admin: ["admin1", "admin2"],
|
||||
proxy_admin_viewer: ["viewer1"],
|
||||
internal_user: ["user1", "user2"],
|
||||
internal_user_viewer: ["viewer1", "viewer2"],
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter out empty strings after trimming", () => {
|
||||
const formValues = {
|
||||
proxy_admin_teams: "admin1,,admin2, , admin3",
|
||||
default_role: "internal_user",
|
||||
group_claim: "groups",
|
||||
use_role_mappings: true,
|
||||
};
|
||||
|
||||
const result = processSSOSettingsPayload(formValues);
|
||||
|
||||
expect(result.role_mappings.roles.proxy_admin).toEqual(["admin1", "admin2", "admin3"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("default role mapping", () => {
|
||||
it("should map internal_user_viewer correctly", () => {
|
||||
const formValues = {
|
||||
default_role: "internal_user_viewer",
|
||||
group_claim: "groups",
|
||||
use_role_mappings: true,
|
||||
};
|
||||
|
||||
const result = processSSOSettingsPayload(formValues);
|
||||
|
||||
expect(result.role_mappings.default_role).toBe("internal_user_viewer");
|
||||
});
|
||||
|
||||
it("should map internal_user correctly", () => {
|
||||
const formValues = {
|
||||
default_role: "internal_user",
|
||||
group_claim: "groups",
|
||||
use_role_mappings: true,
|
||||
};
|
||||
|
||||
const result = processSSOSettingsPayload(formValues);
|
||||
|
||||
expect(result.role_mappings.default_role).toBe("internal_user");
|
||||
});
|
||||
|
||||
it("should map proxy_admin_viewer correctly", () => {
|
||||
const formValues = {
|
||||
default_role: "proxy_admin_viewer",
|
||||
group_claim: "groups",
|
||||
use_role_mappings: true,
|
||||
};
|
||||
|
||||
const result = processSSOSettingsPayload(formValues);
|
||||
|
||||
expect(result.role_mappings.default_role).toBe("proxy_admin_viewer");
|
||||
});
|
||||
|
||||
it("should map proxy_admin correctly", () => {
|
||||
const formValues = {
|
||||
default_role: "proxy_admin",
|
||||
group_claim: "groups",
|
||||
use_role_mappings: true,
|
||||
};
|
||||
|
||||
const result = processSSOSettingsPayload(formValues);
|
||||
|
||||
expect(result.role_mappings.default_role).toBe("proxy_admin");
|
||||
});
|
||||
|
||||
it("should default to internal_user for unknown roles", () => {
|
||||
const formValues = {
|
||||
default_role: "unknown_role",
|
||||
group_claim: "groups",
|
||||
use_role_mappings: true,
|
||||
};
|
||||
|
||||
const result = processSSOSettingsPayload(formValues);
|
||||
|
||||
expect(result.role_mappings.default_role).toBe("internal_user");
|
||||
});
|
||||
|
||||
it("should default to internal_user for undefined default_role", () => {
|
||||
const formValues = {
|
||||
group_claim: "groups",
|
||||
use_role_mappings: true,
|
||||
};
|
||||
|
||||
const result = processSSOSettingsPayload(formValues);
|
||||
|
||||
expect(result.role_mappings.default_role).toBe("internal_user");
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle empty form values", () => {
|
||||
const result = processSSOSettingsPayload({});
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("should preserve other fields in the payload", () => {
|
||||
const formValues = {
|
||||
use_role_mappings: false,
|
||||
sso_provider: "google",
|
||||
client_id: "123",
|
||||
client_secret: "secret",
|
||||
redirect_url: "http://example.com",
|
||||
custom_field: { nested: "value" },
|
||||
array_field: [1, 2, 3],
|
||||
};
|
||||
|
||||
const result = processSSOSettingsPayload(formValues);
|
||||
|
||||
expect(result).toEqual({
|
||||
sso_provider: "google",
|
||||
client_id: "123",
|
||||
client_secret: "secret",
|
||||
redirect_url: "http://example.com",
|
||||
custom_field: { nested: "value" },
|
||||
array_field: [1, 2, 3],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Processes SSO settings form values and transforms them into the payload format expected by the API
|
||||
* Handles role mappings transformation and field extraction
|
||||
*/
|
||||
export const processSSOSettingsPayload = (formValues: Record<string, any>): Record<string, any> => {
|
||||
const {
|
||||
proxy_admin_teams,
|
||||
admin_viewer_teams,
|
||||
internal_user_teams,
|
||||
internal_viewer_teams,
|
||||
default_role,
|
||||
group_claim,
|
||||
use_role_mappings,
|
||||
...rest
|
||||
} = formValues;
|
||||
|
||||
const payload: any = {
|
||||
...rest,
|
||||
};
|
||||
|
||||
// Add role mappings if use_role_mappings is checked
|
||||
if (use_role_mappings) {
|
||||
// Helper function to split comma-separated string into array
|
||||
const splitTeams = (teams: string | undefined): string[] => {
|
||||
if (!teams || teams.trim() === "") return [];
|
||||
return teams
|
||||
.split(",")
|
||||
.map((team) => team.trim())
|
||||
.filter((team) => team.length > 0);
|
||||
};
|
||||
|
||||
// Map default role display values to backend values
|
||||
const defaultRoleMapping: Record<string, string> = {
|
||||
internal_user_viewer: "internal_user_viewer",
|
||||
internal_user: "internal_user",
|
||||
proxy_admin_viewer: "proxy_admin_viewer",
|
||||
proxy_admin: "proxy_admin",
|
||||
};
|
||||
|
||||
payload.role_mappings = {
|
||||
provider: "generic",
|
||||
group_claim,
|
||||
default_role: defaultRoleMapping[default_role] || "internal_user",
|
||||
roles: {
|
||||
proxy_admin: splitTeams(proxy_admin_teams),
|
||||
proxy_admin_viewer: splitTeams(admin_viewer_teams),
|
||||
internal_user: splitTeams(internal_user_teams),
|
||||
internal_user_viewer: splitTeams(internal_viewer_teams),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
Reference in New Issue
Block a user