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'; const BackIcon = () => ( ); const PlusIcon = () => ( ); const EditIcon = () => ( ); const TrashIcon = () => ( ); const ShieldIcon = () => ( ); const ClockIcon = () => ( ); const GlobeIcon = () => ( ); const BoltIcon = () => ( ); const CalendarIcon = () => ( ); 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 [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 && ( navigate('/admin')} className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600" > )} {t('admin.policies.title')} {t('admin.policies.subtitle')} navigate('/admin/policies/create')} className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600" > {t('admin.policies.createPolicy')} {/* 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 */} navigate(`/admin/policies/${policy.id}/edit`)} className="flex-1 rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-dark-600 hover:text-dark-100 sm:flex-none" title={t('admin.policies.actions.edit')} > setDeleteConfirm(policy.id)} className="flex-1 rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-error-500/20 hover:text-error-400 sm:flex-none" title={t('admin.policies.actions.delete')} > ); })} )} {/* Delete Confirmation */} {deleteConfirm !== null && ( setDeleteConfirm(null)} aria-hidden="true" /> {t('admin.policies.confirm.title')} {t('admin.policies.confirm.text')} setDeleteConfirm(null)} className="px-4 py-2 text-dark-300 transition-colors hover:text-dark-100" > {t('admin.policies.confirm.cancel')} deleteMutation.mutate(deleteConfirm)} disabled={deleteMutation.isPending} className="rounded-lg bg-error-500 px-4 py-2 text-white transition-colors hover:bg-error-600 disabled:opacity-50" > {deleteMutation.isPending ? t('admin.policies.confirm.deleting') : t('admin.policies.confirm.delete')} )} ); }
{t('admin.policies.subtitle')}
{formError}
{t('admin.policies.errors.loadFailed')}
{t('admin.policies.noPolicies')}
{t('admin.policies.confirm.text')}