266 lines
11 KiB
TypeScript
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>
|
|
)
|
|
}
|