"use client"; import AutoProvisionConfigWidget from "@app/components/private/AutoProvisionConfigWidget"; import { SettingsContainer, SettingsSection, SettingsSectionBody, SettingsSectionDescription, SettingsSectionForm, SettingsSectionGrid, SettingsSectionHeader, SettingsSectionTitle } from "@app/components/Settings"; import HeaderTitle from "@app/components/SettingsSectionTitle"; import { StrategySelect } from "@app/components/StrategySelect"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Button } from "@app/components/ui/button"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { ListRolesResponse } from "@server/routers/role"; import { AxiosResponse } from "axios"; import { InfoIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import Image from "next/image"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; export default function Page() { const { env } = useEnvContext(); const api = createApiClient({ env }); const router = useRouter(); const [createLoading, setCreateLoading] = useState(false); const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]); const [roleMappingMode, setRoleMappingMode] = useState< "role" | "expression" >("role"); const { isUnlocked } = useLicenseStatusContext(); const t = useTranslations(); const { isPaidUser } = usePaidStatus(); const params = useParams(); const createIdpFormSchema = z.object({ name: z.string().min(2, { message: t("nameMin", { len: 2 }) }), type: z.enum(["oidc", "google", "azure"]), clientId: z.string().min(1, { message: t("idpClientIdRequired") }), clientSecret: z .string() .min(1, { message: t("idpClientSecretRequired") }), authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") }).optional(), tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") }).optional(), identifierPath: z .string() .min(1, { message: t("idpPathRequired") }) .optional(), emailPath: z.string().optional(), namePath: z.string().optional(), scopes: z .string() .min(1, { message: t("idpScopeRequired") }) .optional(), tenantId: z.string().optional(), autoProvision: z.boolean().default(false), roleMapping: z.string().nullable().optional(), roleId: z.number().nullable().optional() }); type CreateIdpFormValues = z.infer; interface ProviderTypeOption { id: "oidc" | "google" | "azure"; title: string; description: string; icon?: React.ReactNode; } const providerTypes: ReadonlyArray = [ { id: "oidc", title: "OAuth2/OIDC", description: t("idpOidcDescription") }, { id: "google", title: t("idpGoogleTitle"), description: t("idpGoogleDescription"), icon: ( {t("idpGoogleAlt")} ) }, { id: "azure", title: t("idpAzureTitle"), description: t("idpAzureDescription"), icon: ( {t("idpAzureAlt")} ) } ]; const form = useForm({ resolver: zodResolver(createIdpFormSchema), defaultValues: { name: "", type: "oidc", clientId: "", clientSecret: "", authUrl: "", tokenUrl: "", identifierPath: "sub", namePath: "name", emailPath: "email", scopes: "openid profile email", tenantId: "", autoProvision: false, roleMapping: null, roleId: null } }); // Fetch roles on component mount useEffect(() => { async function fetchRoles() { const res = await api .get< AxiosResponse >(`/org/${params.orgId}/roles`) .catch((e) => { console.error(e); toast({ variant: "destructive", title: t("accessRoleErrorFetch"), description: formatAxiosError( e, t("accessRoleErrorFetchDescription") ) }); }); if (res?.status === 200) { setRoles(res.data.data.roles); } } fetchRoles(); }, []); // Handle provider type changes and set defaults const handleProviderChange = (value: "oidc" | "google" | "azure") => { form.setValue("type", value); if (value === "google") { // Set Google defaults form.setValue( "authUrl", "https://accounts.google.com/o/oauth2/v2/auth" ); form.setValue("tokenUrl", "https://oauth2.googleapis.com/token"); form.setValue("identifierPath", "email"); form.setValue("emailPath", "email"); form.setValue("namePath", "name"); form.setValue("scopes", "openid profile email"); } else if (value === "azure") { // Set Azure Entra ID defaults (URLs will be constructed dynamically) form.setValue( "authUrl", "https://login.microsoftonline.com/{{TENANT_ID}}/oauth2/v2.0/authorize" ); form.setValue( "tokenUrl", "https://login.microsoftonline.com/{{TENANT_ID}}/oauth2/v2.0/token" ); form.setValue("identifierPath", "email"); form.setValue("emailPath", "email"); form.setValue("namePath", "name"); form.setValue("scopes", "openid profile email"); form.setValue("tenantId", ""); } else { // Reset to OIDC defaults form.setValue("authUrl", ""); form.setValue("tokenUrl", ""); form.setValue("identifierPath", "sub"); form.setValue("namePath", "name"); form.setValue("emailPath", "email"); form.setValue("scopes", "openid profile email"); } }; async function onSubmit(data: CreateIdpFormValues) { setCreateLoading(true); try { // Construct URLs dynamically for Azure provider let authUrl = data.authUrl; let tokenUrl = data.tokenUrl; if (data.type === "azure" && data.tenantId) { authUrl = authUrl?.replace("{{TENANT_ID}}", data.tenantId); tokenUrl = tokenUrl?.replace("{{TENANT_ID}}", data.tenantId); } const roleName = roles.find((r) => r.roleId === data.roleId)?.name; const payload = { name: data.name, clientId: data.clientId, clientSecret: data.clientSecret, authUrl: authUrl, tokenUrl: tokenUrl, identifierPath: data.identifierPath, emailPath: data.emailPath, namePath: data.namePath, autoProvision: data.autoProvision, roleMapping: roleMappingMode === "role" ? `'${roleName}'` : data.roleMapping || "", scopes: data.scopes, variant: data.type }; // Use the appropriate endpoint based on provider type const endpoint = "oidc"; const res = await api.put( `/org/${params.orgId}/idp/${endpoint}`, payload ); if (res.status === 201) { toast({ title: t("success"), description: t("idpCreatedDescription") }); router.push( `/${params.orgId}/settings/idp/${res.data.data.idpId}` ); } } catch (e) { toast({ title: t("error"), description: formatAxiosError(e), variant: "destructive" }); } finally { setCreateLoading(false); } } return ( <>
{t("idpTitle")} {t("idpCreateSettingsDescription")}
{t("idpType")}
{ handleProviderChange( value as "oidc" | "google" | "azure" ); }} cols={3} />
( {t("name")} {t("idpDisplayName")} )} />
{/* Auto Provision Settings */} {t("idpAutoProvisionUsers")} {t("idpAutoProvisionUsersDescription")}
{ form.setValue( "autoProvision", checked ); }} roleMappingMode={roleMappingMode} onRoleMappingModeChange={(data) => { setRoleMappingMode(data); // Clear roleId and roleMapping when mode changes form.setValue("roleId", null); form.setValue("roleMapping", null); }} roles={roles} roleIdFieldName="roleId" roleMappingFieldName="roleMapping" />
{form.watch("type") === "google" && ( {t("idpGoogleConfigurationTitle")} {t("idpGoogleConfigurationDescription")}
( {t("idpClientId")} {t( "idpGoogleClientIdDescription" )} )} /> ( {t("idpClientSecret")} {t( "idpGoogleClientSecretDescription" )} )} />
)} {form.watch("type") === "azure" && ( {t("idpAzureConfigurationTitle")} {t("idpAzureConfigurationDescription")}
( {t("idpTenantIdLabel")} {t( "idpAzureTenantIdDescription" )} )} /> ( {t("idpClientId")} {t( "idpAzureClientIdDescription2" )} )} /> ( {t("idpClientSecret")} {t( "idpAzureClientSecretDescription2" )} )} />
)} {form.watch("type") === "oidc" && ( {t("idpOidcConfigure")} {t("idpOidcConfigureDescription")}
( {t("idpClientId")} {t( "idpClientIdDescription" )} )} /> ( {t("idpClientSecret")} {t( "idpClientSecretDescription" )} )} /> ( {t("idpAuthUrl")} {t( "idpAuthUrlDescription" )} )} /> ( {t("idpTokenUrl")} {t( "idpTokenUrlDescription" )} )} /> {t("idpOidcConfigureAlert")} {t("idpOidcConfigureAlertDescription")}
{t("idpToken")} {t("idpTokenDescription")}
( {t("idpJmespathLabel")} {t( "idpJmespathLabelDescription" )} )} /> ( {t( "idpJmespathEmailPathOptional" )} {t( "idpJmespathEmailPathOptionalDescription" )} )} /> ( {t( "idpJmespathNamePathOptional" )} {t( "idpJmespathNamePathOptionalDescription" )} )} /> ( {t( "idpOidcConfigureScopes" )} {t( "idpOidcConfigureScopesDescription" )} )} />
)}
); }