"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 "../../../../../components/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"; import { useTranslations } from "next-intl"; type Organization = { orgId: string; name: string; }; export default function PoliciesPage() { const { env } = useEnvContext(); const api = createApiClient({ env }); const router = useRouter(); 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 policyFormSchema = z.object({ orgId: z.string().min(1, { message: t('orgRequired') }), 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; 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: 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]); 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: t('success'), description: t('orgPolicyAddedDescription') }); setShowAddDialog(false); form.reset(); } } catch (e) { toast({ title: t('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: t('success'), description: t('orgPolicyUpdatedDescription') }); setShowAddDialog(false); setEditingPolicy(null); form.reset(); } } 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) => { setUpdateDefaultMappingsLoading(true); try { const res = await api.post(`/idp/${idpId}/oidc`, { defaultRoleMapping: data.defaultRoleMapping, 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 ( <> {t('orgPoliciesAbout')} {/*TODO(vlalx): Validate replacing */} {t('orgPoliciesAboutDescription')}{" "} {t('orgPoliciesAboutDescriptionLink')} {t('defaultMappingsOptional')} {t('defaultMappingsOptionalDescription')}
( {t('defaultMappingsRole')} {t('defaultMappingsRoleDescription')} )} /> ( {t('defaultMappingsOrg')} {t('defaultMappingsOrgDescription')} )} />
{ 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 ? t('orgPoliciesEdit') : t('orgPoliciesAdd')} {t('orgPolicyConfig')}
( {t('org')} {editingPolicy ? ( ) : ( {t('orgNotFound')} {organizations.map( ( org ) => ( { form.setValue( "orgId", org.orgId ); }} > { org.name } ) )} )} )} /> ( {t('roleMappingPathOptional')} {t('defaultMappingsRoleDescription')} )} /> ( {t('orgMappingPathOptional')} {t('defaultMappingsOrgDescription')} )} />
); }