Files
breakpilot-compliance/admin-compliance/app/sdk/rbac/page.tsx
Sharang Parnerkar d5287f4bdd refactor(admin): split rbac page.tsx into colocated components
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 22:50:55 +02:00

266 lines
11 KiB
TypeScript

'use client'
import React, { useState, useEffect, useCallback } from 'react'
import { useSDK } from '@/lib/sdk'
import type { Tenant, Namespace, Role, UserRole, LLMPolicy, TabId } from './_types'
import { apiFetch } from './_api'
import { CreateTenantModal } from './_components/CreateTenantModal'
import { CreateNamespaceModal } from './_components/CreateNamespaceModal'
import { CreateRoleModal } from './_components/CreateRoleModal'
import { AssignRoleModal } from './_components/AssignRoleModal'
import { PolicyModal } from './_components/PolicyModal'
import { TenantsTab } from './_components/TenantsTab'
import { NamespacesTab } from './_components/NamespacesTab'
import { RolesTab } from './_components/RolesTab'
import { UsersTab } from './_components/UsersTab'
import { PoliciesTab } from './_components/PoliciesTab'
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function RBACPage() {
const { state } = useSDK()
void state
const [activeTab, setActiveTab] = useState<TabId>('tenants')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
// Data
const [tenants, setTenants] = useState<Tenant[]>([])
const [selectedTenantId, setSelectedTenantId] = useState<string | null>(null)
const [namespaces, setNamespaces] = useState<Namespace[]>([])
const [roles, setRoles] = useState<Role[]>([])
const [userRoles, setUserRoles] = useState<UserRole[]>([])
const [policies, setPolicies] = useState<LLMPolicy[]>([])
// Modals
const [showTenantModal, setShowTenantModal] = useState(false)
const [showNamespaceModal, setShowNamespaceModal] = useState(false)
const [showRoleModal, setShowRoleModal] = useState(false)
const [showUserRoleModal, setShowUserRoleModal] = useState(false)
const [showPolicyModal, setShowPolicyModal] = useState(false)
const [editingPolicy, setEditingPolicy] = useState<LLMPolicy | null>(null)
// ─── Loaders ─────────────────────────────────────────────────────────
const loadTenants = useCallback(async () => {
setLoading(true); setError(null)
try {
const data = await apiFetch<Tenant[] | { tenants: Tenant[] }>('tenants')
setTenants(Array.isArray(data) ? data : data.tenants || [])
} catch (e) { setError(e instanceof Error ? e.message : 'Fehler') }
finally { setLoading(false) }
}, [])
const loadNamespaces = useCallback(async (tenantId: string) => {
setLoading(true); setError(null)
try {
const data = await apiFetch<Namespace[] | { namespaces: Namespace[] }>(`tenants/${tenantId}/namespaces`)
setNamespaces(Array.isArray(data) ? data : data.namespaces || [])
} catch (e) { setError(e instanceof Error ? e.message : 'Fehler') }
finally { setLoading(false) }
}, [])
const loadRoles = useCallback(async () => {
setLoading(true); setError(null)
try {
const data = await apiFetch<Role[] | { roles: Role[] }>('roles')
setRoles(Array.isArray(data) ? data : data.roles || [])
} catch (e) { setError(e instanceof Error ? e.message : 'Fehler') }
finally { setLoading(false) }
}, [])
const loadUserRoles = useCallback(async () => {
setLoading(true); setError(null)
try {
// Load for a specific user or all — the endpoint expects a userId
// We'll load the current user's roles as default
const data = await apiFetch<UserRole[] | { roles: UserRole[] }>('user-roles/00000000-0000-0000-0000-000000000001')
setUserRoles(Array.isArray(data) ? data : data.roles || [])
} catch (e) { setError(e instanceof Error ? e.message : 'Fehler') }
finally { setLoading(false) }
}, [])
const loadPolicies = useCallback(async () => {
setLoading(true); setError(null)
try {
const data = await apiFetch<LLMPolicy[] | { policies: LLMPolicy[] }>('llm/policies')
setPolicies(Array.isArray(data) ? data : data.policies || [])
} catch (e) { setError(e instanceof Error ? e.message : 'Fehler') }
finally { setLoading(false) }
}, [])
useEffect(() => {
if (activeTab === 'tenants') loadTenants()
else if (activeTab === 'namespaces') { loadTenants(); if (selectedTenantId) loadNamespaces(selectedTenantId) }
else if (activeTab === 'roles') loadRoles()
else if (activeTab === 'users') loadUserRoles()
else if (activeTab === 'policies') loadPolicies()
}, [activeTab, loadTenants, loadNamespaces, loadRoles, loadUserRoles, loadPolicies, selectedTenantId])
const showSuccessMsg = (msg: string) => {
setSuccess(msg)
setTimeout(() => setSuccess(null), 3000)
}
// ─── Actions ─────────────────────────────────────────────────────────
const deleteLLMPolicy = async (id: string) => {
if (!confirm('Policy wirklich loeschen?')) return
try {
await apiFetch(`llm/policies/${id}`, { method: 'DELETE' })
showSuccessMsg('Policy geloescht')
loadPolicies()
} catch (e) { setError(e instanceof Error ? e.message : 'Fehler') }
}
const togglePolicyActive = async (policy: LLMPolicy) => {
try {
await apiFetch(`llm/policies/${policy.id}`, {
method: 'PUT',
body: JSON.stringify({ ...policy, is_active: !policy.is_active }),
})
loadPolicies()
} catch (e) { setError(e instanceof Error ? e.message : 'Fehler') }
}
const revokeUserRole = async (userId: string, roleId: string) => {
if (!confirm('Rolle wirklich entziehen?')) return
try {
await apiFetch(`user-roles/${userId}/${roleId}`, { method: 'DELETE' })
showSuccessMsg('Rolle entzogen')
loadUserRoles()
} catch (e) { setError(e instanceof Error ? e.message : 'Fehler') }
}
// ─── Tabs ────────────────────────────────────────────────────────────
const tabs: { id: TabId; label: string }[] = [
{ id: 'tenants', label: 'Mandanten' },
{ id: 'namespaces', label: 'Namespaces' },
{ id: 'roles', label: 'Rollen' },
{ id: 'users', label: 'Benutzer' },
{ id: 'policies', label: 'LLM-Policies' },
]
return (
<div className="p-6 max-w-7xl mx-auto">
{/* Header */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900">RBAC Administration</h1>
<p className="text-gray-500 mt-1">Mandanten, Rollen, Berechtigungen und LLM-Policies verwalten</p>
</div>
{/* Tabs */}
<div className="flex gap-1 bg-gray-100 rounded-lg p-1 mb-6 overflow-x-auto">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === tab.id
? 'bg-white text-purple-700 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
{tab.label}
</button>
))}
</div>
{error && <div className="mb-4 p-3 bg-red-50 text-red-700 rounded-lg text-sm">{error}</div>}
{success && <div className="mb-4 p-3 bg-green-50 text-green-700 rounded-lg text-sm">{success}</div>}
{loading && (
<div className="flex items-center justify-center py-12">
<div className="w-8 h-8 border-4 border-purple-200 border-t-purple-600 rounded-full animate-spin" />
</div>
)}
{!loading && activeTab === 'tenants' && (
<TenantsTab
tenants={tenants}
onOpenCreate={() => setShowTenantModal(true)}
onSelectTenant={setSelectedTenantId}
setActiveTab={setActiveTab}
/>
)}
{!loading && activeTab === 'namespaces' && (
<NamespacesTab
tenants={tenants}
namespaces={namespaces}
selectedTenantId={selectedTenantId}
onSelectTenant={setSelectedTenantId}
onOpenCreate={() => setShowNamespaceModal(true)}
onLoadNamespaces={loadNamespaces}
/>
)}
{!loading && activeTab === 'roles' && (
<RolesTab roles={roles} onOpenCreate={() => setShowRoleModal(true)} />
)}
{!loading && activeTab === 'users' && (
<UsersTab
userRoles={userRoles}
onOpenAssign={() => setShowUserRoleModal(true)}
onRevoke={revokeUserRole}
setUserRoles={setUserRoles}
setLoading={setLoading}
setError={setError}
/>
)}
{!loading && activeTab === 'policies' && (
<PoliciesTab
policies={policies}
onCreate={() => { setEditingPolicy(null); setShowPolicyModal(true) }}
onEdit={(policy) => { setEditingPolicy(policy); setShowPolicyModal(true) }}
onDelete={deleteLLMPolicy}
onToggleActive={togglePolicyActive}
/>
)}
{/* ══════════════════════════════════════════════════════════════════ */}
{/* MODALS */}
{/* ══════════════════════════════════════════════════════════════════ */}
{showTenantModal && (
<CreateTenantModal
onClose={() => setShowTenantModal(false)}
onCreated={() => { setShowTenantModal(false); showSuccessMsg('Mandant erstellt'); loadTenants() }}
/>
)}
{showNamespaceModal && selectedTenantId && (
<CreateNamespaceModal
tenantId={selectedTenantId}
onClose={() => setShowNamespaceModal(false)}
onCreated={() => { setShowNamespaceModal(false); showSuccessMsg('Namespace erstellt'); loadNamespaces(selectedTenantId) }}
/>
)}
{showRoleModal && (
<CreateRoleModal
onClose={() => setShowRoleModal(false)}
onCreated={() => { setShowRoleModal(false); showSuccessMsg('Rolle erstellt'); loadRoles() }}
/>
)}
{showUserRoleModal && (
<AssignRoleModal
onClose={() => setShowUserRoleModal(false)}
onAssigned={() => { setShowUserRoleModal(false); showSuccessMsg('Rolle zugewiesen'); loadUserRoles() }}
/>
)}
{showPolicyModal && (
<PolicyModal
existing={editingPolicy}
onClose={() => { setShowPolicyModal(false); setEditingPolicy(null) }}
onSaved={() => { setShowPolicyModal(false); setEditingPolicy(null); showSuccessMsg('Policy gespeichert'); loadPolicies() }}
/>
)}
</div>
)
}