import { useState, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { rbacApi, AccessPolicy, AdminRole } from '@/api/rbac'; import { PermissionGate } from '@/components/auth/PermissionGate'; import { usePlatform } from '@/platform/hooks/usePlatform'; import { useFocusTrap } from '@/hooks/useFocusTrap'; import { BackIcon, BoltIcon, CalendarIcon, ClockIcon, EditIcon, GlobeIcon, PlusIcon, ShieldIcon, TrashIcon, } from '@/components/icons'; interface PolicyConditions { time_range?: { start: string; end: string }; ip_whitelist?: string[]; rate_limit?: number; weekdays?: number[]; } function parseConditions(raw: Record): PolicyConditions { const result: PolicyConditions = {}; if (raw.time_range && typeof raw.time_range === 'object') { const tr = raw.time_range as Record; if (typeof tr.start === 'string' && typeof tr.end === 'string') { result.time_range = { start: tr.start, end: tr.end }; } } if (Array.isArray(raw.ip_whitelist)) { result.ip_whitelist = raw.ip_whitelist.filter((ip): ip is string => typeof ip === 'string'); } if (typeof raw.rate_limit === 'number') { result.rate_limit = raw.rate_limit; } if (Array.isArray(raw.weekdays)) { result.weekdays = raw.weekdays.filter((d): d is number => typeof d === 'number'); } return result; } interface EffectBadgeProps { effect: 'allow' | 'deny'; className?: string; } function EffectBadge({ effect, className }: EffectBadgeProps) { const { t } = useTranslation(); const isAllow = effect === 'allow'; return ( {isAllow ? t('admin.policies.effectAllow') : t('admin.policies.effectDeny')} ); } export default function AdminPolicies() { const { t } = useTranslation(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { capabilities } = usePlatform(); const [deleteConfirm, setDeleteConfirm] = useState(null); const deleteDialogRef = useFocusTrap(deleteConfirm !== null, { onEscape: () => setDeleteConfirm(null), }); const [formError, setFormError] = useState(null); // Queries const { data: policies, isLoading: policiesLoading, error: policiesError, } = useQuery({ queryKey: ['admin-policies'], queryFn: rbacApi.getPolicies, }); const { data: roles } = useQuery({ queryKey: ['admin-roles'], queryFn: rbacApi.getRoles, }); // Mutations const deleteMutation = useMutation({ mutationFn: rbacApi.deletePolicy, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-policies'] }); setDeleteConfirm(null); }, onError: () => { setDeleteConfirm(null); setFormError(t('admin.policies.errors.deleteFailed')); }, }); // Derived data const rolesMap = useMemo(() => { const map = new Map(); if (roles) { for (const role of roles) { map.set(role.id, role); } } return map; }, [roles]); const sortedPolicies = useMemo(() => { if (!policies) return []; return [...policies].sort((a, b) => b.priority - a.priority); }, [policies]); // Condition icons renderer const renderConditionIcons = useCallback( (conditions: Record) => { const parsed = parseConditions(conditions); const icons: React.ReactNode[] = []; if (parsed.time_range) { icons.push( {parsed.time_range.start}-{parsed.time_range.end} , ); } if (parsed.ip_whitelist && parsed.ip_whitelist.length > 0) { icons.push( {t('admin.policies.conditions.ipCount', { count: parsed.ip_whitelist.length })} , ); } if (parsed.rate_limit !== undefined) { icons.push( {t('admin.policies.conditions.rateValue', { count: parsed.rate_limit })} , ); } if (parsed.weekdays && parsed.weekdays.length > 0 && parsed.weekdays.length < 7) { const dayOrder = [1, 2, 3, 4, 5, 6, 0]; const sorted = dayOrder.filter((d) => parsed.weekdays!.includes(d)); const dayNames = sorted.map((d) => t(`admin.policies.conditions.day${d}`)); icons.push( {dayNames.join(', ')} , ); } return icons; }, [t], ); const getRoleName = useCallback( (policy: AccessPolicy): string => { if (policy.role_name) return policy.role_name; if (policy.role_id === null) return t('admin.policies.global'); const role = rolesMap.get(policy.role_id); return role?.name ?? t('admin.policies.unknownRole'); }, [rolesMap, t], ); return (
{/* Header */}
{!capabilities.hasBackButton && ( )}

{t('admin.policies.title')}

{t('admin.policies.subtitle')}

{/* Error message */} {formError && (

{formError}

)} {/* Stats Overview */} {sortedPolicies.length > 0 && (
{sortedPolicies.length}
{t('admin.policies.stats.total')}
{sortedPolicies.filter((p) => p.effect === 'allow').length}
{t('admin.policies.stats.allow')}
{sortedPolicies.filter((p) => p.effect === 'deny').length}
{t('admin.policies.stats.deny')}
{sortedPolicies.filter((p) => p.is_active).length}
{t('admin.policies.stats.active')}
)} {/* Policies List */} {policiesLoading ? (
) : policiesError ? (

{t('admin.policies.errors.loadFailed')}

) : sortedPolicies.length === 0 ? (

{t('admin.policies.noPolicies')}

) : (
{sortedPolicies.map((policy) => { const conditionIcons = renderConditionIcons(policy.conditions); const roleName = getRoleName(policy); return (
{/* Policy name + effect badge */}
{policy.name} {!policy.is_active && ( {t('admin.policies.inactiveBadge')} )}
{/* Resource + actions */}
{t( `admin.roles.form.permissionSections.${policy.resource}`, policy.resource, )} : {(policy.actions ?? []) .map((a) => t(`admin.roles.form.permissionActions.${a}`, a)) .join(', ')}
{/* Info row */}
{t('admin.policies.roleLabel')}: {roleName} {t('admin.policies.priorityLabel')}: {policy.priority}
{/* Condition icons */} {conditionIcons.length > 0 && (
{conditionIcons}
)}
{/* Actions */}
); })}
)} {/* Delete Confirmation */} {deleteConfirm !== null && (
setDeleteConfirm(null)} aria-hidden="true" />

{t('admin.policies.confirm.title')}

{t('admin.policies.confirm.text')}

)}
); }