"use client"; import { useEffect, useState } from "react"; import { useParams, useRouter } 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 "./PolicyTable"; import { AxiosResponse } from "axios"; import { ListOrgsResponse } from "@server/routers/org"; 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 { Textarea } from "@app/components/ui/textarea"; import { InfoPopup } from "@app/components/ui/info-popup"; import { GetIdpResponse } from "@server/routers/idp"; import { SettingsContainer, SettingsSection, SettingsSectionHeader, SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, SettingsSectionFooter, SettingsSectionForm } from "@app/components/Settings"; type Organization = { orgId: string; name: string; }; const policyFormSchema = z.object({ orgId: z.string().min(1, { message: "Organization is required" }), roleMapping: z.string().optional(), orgMapping: z.string().optional() }); const defaultMappingsSchema = z.object({ defaultRoleMapping: z.string().optional(), defaultOrgMapping: z.string().optional() }); type PolicyFormValues = z.infer; type DefaultMappingsValues = z.infer; export default function PoliciesPage() { const { env } = useEnvContext(); const api = createApiClient({ env }); const router = useRouter(); const { idpId } = useParams(); 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 form = useForm({ resolver: zodResolver(policyFormSchema), defaultValues: { orgId: "", roleMapping: "", orgMapping: "" } }); const defaultMappingsForm = useForm({ resolver: zodResolver(defaultMappingsSchema), defaultValues: { defaultRoleMapping: "", defaultOrgMapping: "" } }); const loadIdp = async () => { try { const res = await api.get>( `/idp/${idpId}` ); if (res.status === 200) { const data = res.data.data; defaultMappingsForm.reset({ defaultRoleMapping: data.idp.defaultRoleMapping || "", defaultOrgMapping: data.idp.defaultOrgMapping || "" }); } } catch (e) { toast({ title: "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: "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: "Error", description: formatAxiosError(e), variant: "destructive" }); } }; useEffect(() => { async function load() { setPageLoading(true); await loadPolicies(); await loadIdp(); setPageLoading(false); } load(); }, [idpId]); const onAddPolicy = async (data: PolicyFormValues) => { setAddPolicyLoading(true); try { const res = await api.put(`/idp/${idpId}/org/${data.orgId}`, { roleMapping: data.roleMapping, orgMapping: data.orgMapping }); if (res.status === 201) { const newPolicy = { orgId: data.orgId, name: organizations.find((org) => org.orgId === data.orgId) ?.name || "", roleMapping: data.roleMapping, orgMapping: data.orgMapping }; setPolicies([...policies, newPolicy]); toast({ title: "Success", description: "Policy added successfully" }); setShowAddDialog(false); form.reset(); } } catch (e) { toast({ title: "Error", description: formatAxiosError(e), variant: "destructive" }); } finally { setAddPolicyLoading(false); } }; const onEditPolicy = async (data: PolicyFormValues) => { if (!editingPolicy) return; setEditPolicyLoading(true); try { const res = await api.post( `/idp/${idpId}/org/${editingPolicy.orgId}`, { roleMapping: data.roleMapping, orgMapping: data.orgMapping } ); if (res.status === 200) { setPolicies( policies.map((policy) => policy.orgId === editingPolicy.orgId ? { ...policy, roleMapping: data.roleMapping, orgMapping: data.orgMapping } : policy ) ); toast({ title: "Success", description: "Policy updated successfully" }); setShowAddDialog(false); setEditingPolicy(null); form.reset(); } } catch (e) { toast({ title: "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: "Success", description: "Policy deleted successfully" }); } } catch (e) { toast({ title: "Error", description: formatAxiosError(e), variant: "destructive" }); } finally { setDeletePolicyLoading(false); } }; const onUpdateDefaultMappings = async (data: DefaultMappingsValues) => { setUpdateDefaultMappingsLoading(true); try { const res = await api.post(`/idp/${idpId}/oidc`, { defaultRoleMapping: data.defaultRoleMapping, defaultOrgMapping: data.defaultOrgMapping }); if (res.status === 200) { toast({ title: "Success", description: "Default mappings updated successfully" }); } } catch (e) { toast({ title: "Error", description: formatAxiosError(e), variant: "destructive" }); } finally { setUpdateDefaultMappingsLoading(false); } }; if (pageLoading) { return null; } return ( <> About Organization Policies Organization policies are used to control access to organizations based on the user's ID token. You can specify JMESPath expressions to extract role and organization information from the ID token. For more information, see{" "} the documentation Default Mappings (Optional) The default mappings are used when when there is not an organization policy defined for an organization. You can specify the default role and organization mappings to fall back to here.
( Default Role Mapping The result of this expression must return the role name as defined in the organization as a string. )} /> ( Default Organization Mapping This expression must return the org ID or true for the user to be allowed to access the organization. )} />
{ loadOrganizations(); form.reset({ orgId: "", roleMapping: "", orgMapping: "" }); setEditingPolicy(null); setShowAddDialog(true); }} onEdit={(policy) => { setEditingPolicy(policy); form.reset({ orgId: policy.orgId, roleMapping: policy.roleMapping || "", orgMapping: policy.orgMapping || "" }); setShowAddDialog(true); }} />
{ setShowAddDialog(val); setEditingPolicy(null); form.reset(); }} > {editingPolicy ? "Edit Organization Policy" : "Add Organization Policy"} Configure access for an organization
( Organization {editingPolicy ? ( ) : ( No org found. {organizations.map( ( org ) => ( { form.setValue( "orgId", org.orgId ); }} > { org.name } ) )} )} )} /> ( Role Mapping Path (Optional) The result of this expression must return the role name as defined in the organization as a string. )} /> ( Organization Mapping Path (Optional) This expression must return the org ID or true for the user to be allowed to access the organization. )} />
); }