Phase 1 — Python (klausur-service): 5 monoliths → 36 files - dsfa_corpus_ingestion.py (1,828 LOC → 5 files) - cv_ocr_engines.py (2,102 LOC → 7 files) - cv_layout.py (3,653 LOC → 10 files) - vocab_worksheet_api.py (2,783 LOC → 8 files) - grid_build_core.py (1,958 LOC → 6 files) Phase 2 — Go (edu-search-service, school-service): 8 monoliths → 19 files - staff_crawler.go (1,402 → 4), policy/store.go (1,168 → 3) - policy_handlers.go (700 → 2), repository.go (684 → 2) - search.go (592 → 2), ai_extraction_handlers.go (554 → 2) - seed_data.go (591 → 2), grade_service.go (646 → 2) Phase 3 — TypeScript (admin-lehrer): 45 monoliths → 220+ files - sdk/types.ts (2,108 → 16 domain files) - ai/rag/page.tsx (2,686 → 14 files) - 22 page.tsx files split into _components/ + _hooks/ - 11 component files split into sub-components - 10 SDK data catalogs added to loc-exceptions - Deleted dead backup index_original.ts (4,899 LOC) All original public APIs preserved via re-export facades. Zero new errors: Python imports verified, Go builds clean, TypeScript tsc --noEmit shows only pre-existing errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
179 lines
7.1 KiB
TypeScript
179 lines
7.1 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* RBAC Management Page
|
|
*
|
|
* Features:
|
|
* - Multi-tenant management
|
|
* - Namespace-based isolation (CFO use case)
|
|
* - Role management with permissions
|
|
* - User-Role assignments with scope
|
|
* - LLM access policies
|
|
*/
|
|
|
|
import { PagePurpose } from '@/components/common/PagePurpose'
|
|
import { useRbacData } from './useRbacData'
|
|
import { TABS } from './types'
|
|
import type { Tenant, Namespace, Role, LLMPolicy } from './types'
|
|
import { TenantsTable } from './_components/TenantsTable'
|
|
import { NamespacesTable } from './_components/NamespacesTable'
|
|
import { RolesTable } from './_components/RolesTable'
|
|
import { UserRolesTable } from './_components/UserRolesTable'
|
|
import { PoliciesTable } from './_components/PoliciesTable'
|
|
import { CreateModal } from './_components/CreateModal'
|
|
|
|
export default function RBACPage() {
|
|
const {
|
|
activeTab,
|
|
setActiveTab,
|
|
loading,
|
|
error,
|
|
tenants,
|
|
userRoles,
|
|
selectedTenantId,
|
|
setSelectedTenantId,
|
|
searchTerm,
|
|
setSearchTerm,
|
|
showCreateModal,
|
|
setShowCreateModal,
|
|
setEditItem,
|
|
handleCreate,
|
|
filteredData,
|
|
stats,
|
|
} = useRbacData()
|
|
|
|
return (
|
|
<div className="min-h-screen bg-slate-50 p-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-slate-900">RBAC Management</h1>
|
|
<p className="text-slate-600">Rollen, Berechtigungen & LLM-Zugriffskontrolle</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<select
|
|
value={selectedTenantId}
|
|
onChange={(e) => setSelectedTenantId(e.target.value)}
|
|
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
|
|
>
|
|
<option value="">Mandant waehlen...</option>
|
|
{tenants.map(t => (
|
|
<option key={t.id} value={t.id}>{t.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<PagePurpose
|
|
title="RBAC Management"
|
|
purpose="Verwalten Sie Multi-Tenant RBAC (Role-Based Access Control) mit Namespace-Isolation. Definieren Sie wer welche KI-Funktionen nutzen darf und welche Daten analysiert werden duerfen. CFO kann Gehaltsdaten analysieren, Entwickler nicht."
|
|
audience={['Admin', 'DSB', 'Compliance Officer']}
|
|
gdprArticles={['Art. 25 (Privacy by Design)', 'Art. 32 (Sicherheit)']}
|
|
architecture={{
|
|
services: ['AI Compliance SDK (Go)', 'PostgreSQL'],
|
|
databases: ['compliance_tenants', 'compliance_namespaces', 'compliance_roles', 'compliance_llm_policies'],
|
|
}}
|
|
relatedPages={[
|
|
{ name: 'Audit Trail', href: '/sdk/audit-report', description: 'LLM-Operationen protokollieren' },
|
|
]}
|
|
/>
|
|
|
|
{/* Statistics */}
|
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
|
|
{[
|
|
{ label: 'Mandanten', value: stats.tenants, border: 'border-slate-200', text: 'text-slate-500', bold: 'text-slate-900' },
|
|
{ label: 'Namespaces', value: stats.namespaces, border: 'border-blue-200', text: 'text-blue-600', bold: 'text-blue-700' },
|
|
{ label: 'Rollen', value: stats.roles, border: 'border-purple-200', text: 'text-purple-600', bold: 'text-purple-700' },
|
|
{ label: 'System-Rollen', value: stats.systemRoles, border: 'border-indigo-200', text: 'text-indigo-600', bold: 'text-indigo-700' },
|
|
{ label: 'LLM-Policies', value: stats.policies, border: 'border-teal-200', text: 'text-teal-600', bold: 'text-teal-700' },
|
|
{ label: 'Zuweisungen', value: stats.activeUsers, border: 'border-green-200', text: 'text-green-600', bold: 'text-green-700' },
|
|
].map(s => (
|
|
<div key={s.label} className={`bg-white rounded-xl p-4 border ${s.border}`}>
|
|
<p className={`text-sm ${s.text}`}>{s.label}</p>
|
|
<p className={`text-2xl font-bold ${s.bold}`}>{s.value}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Tabs + Content */}
|
|
<div className="bg-white rounded-xl shadow-sm border mb-6">
|
|
<div className="border-b">
|
|
<nav className="flex -mb-px">
|
|
{TABS.map(tab => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={`flex items-center gap-2 px-6 py-4 border-b-2 font-medium text-sm transition-colors ${
|
|
activeTab === tab.id
|
|
? 'border-primary-600 text-primary-600'
|
|
: 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'
|
|
}`}
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={tab.icon} />
|
|
</svg>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
|
|
{/* Search and Actions */}
|
|
<div className="p-4 border-b flex items-center justify-between">
|
|
<div className="flex-1 max-w-md">
|
|
<input
|
|
type="text"
|
|
placeholder="Suchen..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
|
|
/>
|
|
</div>
|
|
<button
|
|
onClick={() => setShowCreateModal(true)}
|
|
className="ml-4 px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 flex items-center gap-2"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
</svg>
|
|
Neu erstellen
|
|
</button>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-4">
|
|
{error && (
|
|
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{loading ? (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600" />
|
|
</div>
|
|
) : (
|
|
<>
|
|
{activeTab === 'tenants' && <TenantsTable tenants={filteredData() as Tenant[]} onEdit={setEditItem} />}
|
|
{activeTab === 'namespaces' && <NamespacesTable namespaces={filteredData() as Namespace[]} onEdit={setEditItem} />}
|
|
{activeTab === 'roles' && <RolesTable roles={filteredData() as Role[]} onEdit={setEditItem} />}
|
|
{activeTab === 'users' && <UserRolesTable userRoles={userRoles} onEdit={setEditItem} />}
|
|
{activeTab === 'policies' && <PoliciesTable policies={filteredData() as LLMPolicy[]} onEdit={setEditItem} />}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Create Modal */}
|
|
{showCreateModal && (
|
|
<CreateModal
|
|
type={activeTab}
|
|
tenantId={selectedTenantId}
|
|
onClose={() => setShowCreateModal(false)}
|
|
onSave={handleCreate}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|