'use client' import React, { useState, useEffect, useCallback } from 'react' import { useSDK } from '@/lib/sdk' // ============================================================================= // TYPES // ============================================================================= interface Tenant { id: string name: string slug: string status: string user_limit: number llm_quota: number settings: Record created_at: string updated_at: string } interface Namespace { id: string tenant_id: string name: string slug: string isolation_level: string classification: string created_at: string } interface Role { id: string name: string description: string is_system: boolean permissions: string[] created_at: string } interface UserRole { id: string user_id: string role_id: string role_name: string namespace_id: string | null expires_at: string | null created_at: string } interface LLMPolicy { id: string name: string description: string tenant_id: string namespace_id: string | null allowed_models: string[] blocked_models: string[] rate_limit_rpm: number rate_limit_tpd: number pii_detection_required: boolean pii_redaction_required: boolean max_tokens_per_request: number is_active: boolean created_at: string updated_at: string } type TabId = 'tenants' | 'namespaces' | 'roles' | 'users' | 'policies' // ============================================================================= // API // ============================================================================= const API = '/api/sdk/v1/rbac' async function apiFetch(path: string, opts?: RequestInit): Promise { const res = await fetch(`${API}/${path}`, { headers: { 'Content-Type': 'application/json' }, ...opts, }) if (!res.ok) { const err = await res.text() throw new Error(`HTTP ${res.status}: ${err}`) } return res.json() } function formatDate(iso: string): string { return new Date(iso).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', }) } // ============================================================================= // MAIN PAGE // ============================================================================= export default function RBACPage() { const { state } = useSDK() const [activeTab, setActiveTab] = useState('tenants') const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [success, setSuccess] = useState(null) // Data const [tenants, setTenants] = useState([]) const [selectedTenantId, setSelectedTenantId] = useState(null) const [namespaces, setNamespaces] = useState([]) const [roles, setRoles] = useState([]) const [userRoles, setUserRoles] = useState([]) const [policies, setPolicies] = useState([]) // 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(null) // ─── Loaders ───────────────────────────────────────────────────────── const loadTenants = useCallback(async () => { setLoading(true); setError(null) try { const data = await apiFetch('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(`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('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('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('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 (
{/* Header */}

RBAC Administration

Mandanten, Rollen, Berechtigungen und LLM-Policies verwalten

{/* Tabs */}
{tabs.map(tab => ( ))}
{error &&
{error}
} {success &&
{success}
} {loading && (
)} {/* ══════════════════════════════════════════════════════════════════ */} {/* TENANTS TAB */} {/* ══════════════════════════════════════════════════════════════════ */} {!loading && activeTab === 'tenants' && (

{tenants.length} Mandanten

{tenants.map(t => (

{t.name}

{t.slug}
{t.status}
User-Limit {t.user_limit || 'unbegrenzt'}
LLM-Quota {t.llm_quota || 'unbegrenzt'}
Erstellt {formatDate(t.created_at)}
))}
{tenants.length === 0 && (
Keine Mandanten vorhanden
)}
)} {/* ══════════════════════════════════════════════════════════════════ */} {/* NAMESPACES TAB */} {/* ══════════════════════════════════════════════════════════════════ */} {!loading && activeTab === 'namespaces' && (
{selectedTenantId && ( )}
{!selectedTenantId ? (
Bitte einen Mandanten waehlen
) : (
{namespaces.length === 0 ? ( ) : namespaces.map(ns => ( ))}
Name Slug Isolation Klassifikation Erstellt
Keine Namespaces
{ns.name} {ns.slug} {ns.isolation_level} {ns.classification} {formatDate(ns.created_at)}
)}
)} {/* ══════════════════════════════════════════════════════════════════ */} {/* ROLES TAB */} {/* ══════════════════════════════════════════════════════════════════ */} {!loading && activeTab === 'roles' && (

{roles.length} Rollen

{roles.length === 0 ? ( ) : roles.map(role => ( ))}
Name Beschreibung Typ Berechtigungen
Keine Rollen
{role.name} {role.description} {role.is_system ? 'System' : 'Custom'}
{(role.permissions || []).slice(0, 4).map(p => ( {p} ))} {(role.permissions || []).length > 4 && ( +{role.permissions.length - 4} )}
)} {/* ══════════════════════════════════════════════════════════════════ */} {/* USERS TAB */} {/* ══════════════════════════════════════════════════════════════════ */} {!loading && activeTab === 'users' && (

Benutzer-Rollen

{/* User ID lookup */}
{ setLoading(true) apiFetch(`user-roles/${userId}`) .then(data => setUserRoles(Array.isArray(data) ? data : data.roles || [])) .catch(e => setError(e instanceof Error ? e.message : 'Fehler')) .finally(() => setLoading(false)) }} />
{userRoles.length === 0 ? ( ) : userRoles.map(ur => ( ))}
User-ID Rolle Namespace Ablauf Aktionen
Keine Rollen zugewiesen
{ur.user_id} {ur.role_name || ur.role_id} {ur.namespace_id || 'Global'} {ur.expires_at ? formatDate(ur.expires_at) : 'Unbegrenzt'}
)} {/* ══════════════════════════════════════════════════════════════════ */} {/* LLM POLICIES TAB */} {/* ══════════════════════════════════════════════════════════════════ */} {!loading && activeTab === 'policies' && (

{policies.length} LLM-Policies

{policies.map(policy => (

{policy.name}

{policy.description}

{(policy.allowed_models || []).length > 0 && (
Erlaubte Models:
{policy.allowed_models.map(m => ( {m} ))}
)}
Rate-Limit {policy.rate_limit_rpm} req/min, {policy.rate_limit_tpd} tok/tag
Max Tokens/Request {policy.max_tokens_per_request}
{policy.pii_detection_required && ( PII-Erkennung )} {policy.pii_redaction_required && ( PII-Redaktion )}
))}
{policies.length === 0 && (
Keine LLM-Policies vorhanden
)}
)} {/* ══════════════════════════════════════════════════════════════════ */} {/* MODALS */} {/* ══════════════════════════════════════════════════════════════════ */} {showTenantModal && ( setShowTenantModal(false)} onCreated={() => { setShowTenantModal(false); showSuccessMsg('Mandant erstellt'); loadTenants() }} /> )} {showNamespaceModal && selectedTenantId && ( setShowNamespaceModal(false)} onCreated={() => { setShowNamespaceModal(false); showSuccessMsg('Namespace erstellt'); loadNamespaces(selectedTenantId) }} /> )} {showRoleModal && ( setShowRoleModal(false)} onCreated={() => { setShowRoleModal(false); showSuccessMsg('Rolle erstellt'); loadRoles() }} /> )} {showUserRoleModal && ( setShowUserRoleModal(false)} onAssigned={() => { setShowUserRoleModal(false); showSuccessMsg('Rolle zugewiesen'); loadUserRoles() }} /> )} {showPolicyModal && ( { setShowPolicyModal(false); setEditingPolicy(null) }} onSaved={() => { setShowPolicyModal(false); setEditingPolicy(null); showSuccessMsg('Policy gespeichert'); loadPolicies() }} /> )}
) } // ============================================================================= // USER ROLE LOOKUP // ============================================================================= function UserRoleLookup({ onLoad }: { onLoad: (userId: string) => void }) { const [userId, setUserId] = useState('00000000-0000-0000-0000-000000000001') return (
setUserId(e.target.value)} placeholder="User-ID eingeben..." className="border border-gray-300 rounded-lg px-3 py-2 text-sm flex-1 font-mono" />
) } // ============================================================================= // MODAL BASE // ============================================================================= function ModalBase({ title, onClose, children }: { title: string; onClose: () => void; children: React.ReactNode }) { return (

{title}

{children}
) } // ============================================================================= // CREATE TENANT MODAL // ============================================================================= function CreateTenantModal({ onClose, onCreated }: { onClose: () => void; onCreated: () => void }) { const [form, setForm] = useState({ name: '', slug: '', user_limit: 100, llm_quota: 100000 }) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const handleSubmit = async () => { if (!form.name || !form.slug) { setError('Name und Slug sind Pflichtfelder'); return } setSaving(true) try { await apiFetch('tenants', { method: 'POST', body: JSON.stringify(form) }) onCreated() } catch (e) { setError(e instanceof Error ? e.message : 'Fehler') } finally { setSaving(false) } } return ( {error &&
{error}
}
setForm(f => ({ ...f, name: e.target.value }))} className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm" />
setForm(f => ({ ...f, slug: e.target.value }))} className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm font-mono" />
setForm(f => ({ ...f, user_limit: +e.target.value }))} className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm" />
setForm(f => ({ ...f, llm_quota: +e.target.value }))} className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm" />
) } // ============================================================================= // CREATE NAMESPACE MODAL // ============================================================================= function CreateNamespaceModal({ tenantId, onClose, onCreated }: { tenantId: string; onClose: () => void; onCreated: () => void }) { const [form, setForm] = useState({ name: '', slug: '', isolation_level: 'shared', classification: 'internal' }) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const handleSubmit = async () => { if (!form.name) { setError('Name ist Pflichtfeld'); return } setSaving(true) try { await apiFetch(`tenants/${tenantId}/namespaces`, { method: 'POST', body: JSON.stringify(form) }) onCreated() } catch (e) { setError(e instanceof Error ? e.message : 'Fehler') } finally { setSaving(false) } } return ( {error &&
{error}
}
setForm(f => ({ ...f, name: e.target.value }))} className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm" />
setForm(f => ({ ...f, slug: e.target.value }))} className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm font-mono" />
) } // ============================================================================= // CREATE ROLE MODAL // ============================================================================= function CreateRoleModal({ onClose, onCreated }: { onClose: () => void; onCreated: () => void }) { const [form, setForm] = useState({ name: '', description: '', permissions: '' }) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const handleSubmit = async () => { if (!form.name) { setError('Name ist Pflichtfeld'); return } setSaving(true) try { await apiFetch('roles', { method: 'POST', body: JSON.stringify({ name: form.name, description: form.description, permissions: form.permissions.split(',').map(s => s.trim()).filter(Boolean), }), }) onCreated() } catch (e) { setError(e instanceof Error ? e.message : 'Fehler') } finally { setSaving(false) } } return ( {error &&
{error}
}
setForm(f => ({ ...f, name: e.target.value }))} className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm" />
setForm(f => ({ ...f, description: e.target.value }))} className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm" />