"use client"; import { useEffect, useState } from "react"; import { useParams } from "next/navigation"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { Button } from "@app/components/ui/button"; import { Input } from "@app/components/ui/input"; import { Credenza, CredenzaBody, CredenzaClose, CredenzaContent, CredenzaDescription, CredenzaFooter, CredenzaHeader, CredenzaTitle } from "@app/components/Credenza"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@app/components/ui/form"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { InfoIcon, ExternalLink, CheckIcon } from "lucide-react"; import PolicyTable, { PolicyRow } from "@app/components/PolicyTable"; import { AxiosResponse } from "axios"; import { ListOrgsResponse } from "@server/routers/org"; import { ListRolesResponse } from "@server/routers/role"; import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover"; import { cn } from "@app/lib/cn"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@app/components/ui/command"; import { CaretSortIcon } from "@radix-ui/react-icons"; import Link from "next/link"; import { GetIdpResponse } from "@server/routers/idp"; import { SettingsContainer, SettingsSection, SettingsSectionHeader, SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, SettingsSectionFooter, SettingsSectionForm } from "@app/components/Settings"; import { useTranslations } from "next-intl"; import RoleMappingConfigFields from "@app/components/RoleMappingConfigFields"; import { compileRoleMappingExpression, createMappingBuilderRule, defaultRoleMappingConfig, detectRoleMappingConfig, MappingBuilderRule, RoleMappingMode } from "@app/lib/idpRoleMapping"; type Organization = { orgId: string; name: string; }; function resetRoleMappingStateFromDetected( setMode: (m: RoleMappingMode) => void, setFixed: (v: string[]) => void, setClaim: (v: string) => void, setRules: (v: MappingBuilderRule[]) => void, setRaw: (v: string) => void, stored: string | null | undefined ) { const d = detectRoleMappingConfig(stored); setMode(d.mode); setFixed(d.fixedRoleNames); setClaim(d.mappingBuilder.claimPath); setRules(d.mappingBuilder.rules); setRaw(d.rawExpression); } export default function PoliciesPage() { const { env } = useEnvContext(); const api = createApiClient({ env }); const { idpId } = useParams(); const t = useTranslations(); const [pageLoading, setPageLoading] = useState(true); const [addPolicyLoading, setAddPolicyLoading] = useState(false); const [editPolicyLoading, setEditPolicyLoading] = useState(false); const [deletePolicyLoading, setDeletePolicyLoading] = useState(false); const [updateDefaultMappingsLoading, setUpdateDefaultMappingsLoading] = useState(false); const [policies, setPolicies] = useState([]); const [organizations, setOrganizations] = useState([]); const [showAddDialog, setShowAddDialog] = useState(false); const [editingPolicy, setEditingPolicy] = useState(null); const [defaultRoleMappingMode, setDefaultRoleMappingMode] = useState("fixedRoles"); const [defaultFixedRoleNames, setDefaultFixedRoleNames] = useState< string[] >([]); const [defaultMappingBuilderClaimPath, setDefaultMappingBuilderClaimPath] = useState("groups"); const [defaultMappingBuilderRules, setDefaultMappingBuilderRules] = useState([createMappingBuilderRule()]); const [defaultRawRoleExpression, setDefaultRawRoleExpression] = useState(""); const [policyRoleMappingMode, setPolicyRoleMappingMode] = useState("fixedRoles"); const [policyFixedRoleNames, setPolicyFixedRoleNames] = useState( [] ); const [policyMappingBuilderClaimPath, setPolicyMappingBuilderClaimPath] = useState("groups"); const [policyMappingBuilderRules, setPolicyMappingBuilderRules] = useState< MappingBuilderRule[] >([createMappingBuilderRule()]); const [policyRawRoleExpression, setPolicyRawRoleExpression] = useState(""); const [policyOrgRoles, setPolicyOrgRoles] = useState< { roleId: number; name: string }[] >([]); const policyFormSchema = z.object({ orgId: z.string().min(1, { message: t("orgRequired") }), orgMapping: z.string().optional() }); const defaultMappingsSchema = z.object({ defaultOrgMapping: z.string().optional() }); type PolicyFormValues = z.infer; type DefaultMappingsValues = z.infer; const form = useForm({ resolver: zodResolver(policyFormSchema), defaultValues: { orgId: "", orgMapping: "" } }); const policyFormOrgId = form.watch("orgId"); const defaultMappingsForm = useForm({ resolver: zodResolver(defaultMappingsSchema), defaultValues: { defaultOrgMapping: "" } }); const loadIdp = async () => { try { const res = await api.get>( `/idp/${idpId}` ); if (res.status === 200) { const data = res.data.data; defaultMappingsForm.reset({ defaultOrgMapping: data.idp.defaultOrgMapping || "" }); resetRoleMappingStateFromDetected( setDefaultRoleMappingMode, setDefaultFixedRoleNames, setDefaultMappingBuilderClaimPath, setDefaultMappingBuilderRules, setDefaultRawRoleExpression, data.idp.defaultRoleMapping ); } } catch (e) { toast({ title: t("error"), description: formatAxiosError(e), variant: "destructive" }); } }; const loadPolicies = async () => { try { const res = await api.get(`/idp/${idpId}/org`); if (res.status === 200) { setPolicies(res.data.data.policies); } } catch (e) { toast({ title: t("error"), description: formatAxiosError(e), variant: "destructive" }); } }; const loadOrganizations = async () => { try { const res = await api.get>("/orgs"); if (res.status === 200) { const existingOrgIds = policies.map((p) => p.orgId); const availableOrgs = res.data.data.orgs.filter( (org) => !existingOrgIds.includes(org.orgId) ); setOrganizations(availableOrgs); } } catch (e) { toast({ title: t("error"), description: formatAxiosError(e), variant: "destructive" }); } }; useEffect(() => { async function load() { setPageLoading(true); await loadPolicies(); await loadIdp(); setPageLoading(false); } load(); }, [idpId]); useEffect(() => { if (!showAddDialog) { return; } const orgId = editingPolicy?.orgId || policyFormOrgId; if (!orgId) { setPolicyOrgRoles([]); return; } let cancelled = false; (async () => { const res = await api .get>(`/org/${orgId}/roles`) .catch((e) => { console.error(e); toast({ variant: "destructive", title: t("accessRoleErrorFetch"), description: formatAxiosError( e, t("accessRoleErrorFetchDescription") ) }); return null; }); if (!cancelled && res?.status === 200) { setPolicyOrgRoles(res.data.data.roles); } })(); return () => { cancelled = true; }; }, [showAddDialog, editingPolicy?.orgId, policyFormOrgId, api, t]); function resetPolicyDialogRoleMappingState() { const d = defaultRoleMappingConfig(); setPolicyRoleMappingMode(d.mode); setPolicyFixedRoleNames(d.fixedRoleNames); setPolicyMappingBuilderClaimPath(d.mappingBuilder.claimPath); setPolicyMappingBuilderRules(d.mappingBuilder.rules); setPolicyRawRoleExpression(d.rawExpression); } const onAddPolicy = async (data: PolicyFormValues) => { const roleMappingExpression = compileRoleMappingExpression({ mode: policyRoleMappingMode, fixedRoleNames: policyFixedRoleNames, mappingBuilder: { claimPath: policyMappingBuilderClaimPath, rules: policyMappingBuilderRules }, rawExpression: policyRawRoleExpression }); setAddPolicyLoading(true); try { const res = await api.put(`/idp/${idpId}/org/${data.orgId}`, { roleMapping: roleMappingExpression, orgMapping: data.orgMapping }); if (res.status === 201) { const newPolicy = { orgId: data.orgId, name: organizations.find((org) => org.orgId === data.orgId) ?.name || "", roleMapping: roleMappingExpression, orgMapping: data.orgMapping }; setPolicies([...policies, newPolicy]); toast({ title: t("success"), description: t("orgPolicyAddedDescription") }); setShowAddDialog(false); form.reset(); resetPolicyDialogRoleMappingState(); } } catch (e) { toast({ title: t("error"), description: formatAxiosError(e), variant: "destructive" }); } finally { setAddPolicyLoading(false); } }; const onEditPolicy = async (data: PolicyFormValues) => { if (!editingPolicy) return; const roleMappingExpression = compileRoleMappingExpression({ mode: policyRoleMappingMode, fixedRoleNames: policyFixedRoleNames, mappingBuilder: { claimPath: policyMappingBuilderClaimPath, rules: policyMappingBuilderRules }, rawExpression: policyRawRoleExpression }); setEditPolicyLoading(true); try { const res = await api.post( `/idp/${idpId}/org/${editingPolicy.orgId}`, { roleMapping: roleMappingExpression, orgMapping: data.orgMapping } ); if (res.status === 200) { setPolicies( policies.map((policy) => policy.orgId === editingPolicy.orgId ? { ...policy, roleMapping: roleMappingExpression, orgMapping: data.orgMapping } : policy ) ); toast({ title: t("success"), description: t("orgPolicyUpdatedDescription") }); setShowAddDialog(false); setEditingPolicy(null); form.reset(); resetPolicyDialogRoleMappingState(); } } catch (e) { toast({ title: t("error"), description: formatAxiosError(e), variant: "destructive" }); } finally { setEditPolicyLoading(false); } }; const onDeletePolicy = async (orgId: string) => { setDeletePolicyLoading(true); try { const res = await api.delete(`/idp/${idpId}/org/${orgId}`); if (res.status === 200) { setPolicies( policies.filter((policy) => policy.orgId !== orgId) ); toast({ title: t("success"), description: t("orgPolicyDeletedDescription") }); } } catch (e) { toast({ title: t("error"), description: formatAxiosError(e), variant: "destructive" }); } finally { setDeletePolicyLoading(false); } }; const onUpdateDefaultMappings = async (data: DefaultMappingsValues) => { const defaultRoleMappingExpression = compileRoleMappingExpression({ mode: defaultRoleMappingMode, fixedRoleNames: defaultFixedRoleNames, mappingBuilder: { claimPath: defaultMappingBuilderClaimPath, rules: defaultMappingBuilderRules }, rawExpression: defaultRawRoleExpression }); setUpdateDefaultMappingsLoading(true); try { const res = await api.post(`/idp/${idpId}/oidc`, { defaultRoleMapping: defaultRoleMappingExpression, defaultOrgMapping: data.defaultOrgMapping }); if (res.status === 200) { toast({ title: t("success"), description: t("defaultMappingsUpdatedDescription") }); } } catch (e) { toast({ title: t("error"), description: formatAxiosError(e), variant: "destructive" }); } finally { setUpdateDefaultMappingsLoading(false); } }; if (pageLoading) { return null; } return ( <> { loadOrganizations(); form.reset({ orgId: "", orgMapping: "" }); setEditingPolicy(null); resetPolicyDialogRoleMappingState(); setShowAddDialog(true); }} onEdit={(policy) => { setEditingPolicy(policy); form.reset({ orgId: policy.orgId, orgMapping: policy.orgMapping || "" }); resetRoleMappingStateFromDetected( setPolicyRoleMappingMode, setPolicyFixedRoleNames, setPolicyMappingBuilderClaimPath, setPolicyMappingBuilderRules, setPolicyRawRoleExpression, policy.roleMapping ); setShowAddDialog(true); }} /> {t("defaultMappingsOptional")} {t("defaultMappingsOptionalDescription")}
( {t("defaultMappingsOrg")} {t( "defaultMappingsOrgDescription" )} )} />
{ setShowAddDialog(val); if (!val) { setEditingPolicy(null); form.reset(); resetPolicyDialogRoleMappingState(); } }} > {editingPolicy ? t("orgPoliciesEdit") : t("orgPoliciesAdd")} {t("orgPolicyConfig")}
( {t("org")} {editingPolicy ? ( ) : ( {t( "orgNotFound" )} {organizations.map( ( org ) => ( { form.setValue( "orgId", org.orgId ); }} > { org.name } ) )} )} )} /> ( {t("orgMappingPathOptional")} {t( "defaultMappingsOrgDescription" )} )} />
); }