A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1473 lines
40 KiB
Python
1473 lines
40 KiB
Python
"""
|
|
BreakPilot Studio - RBAC Admin Modul
|
|
|
|
Funktionen:
|
|
- Alle Lehrer anzeigen
|
|
- Alle verfuegbaren Rollen anzeigen
|
|
- Rollen zuweisen und entziehen
|
|
- Uebersicht ueber Rollenzuweisungen
|
|
|
|
Kommuniziert mit der RBAC API (/api/rbac/*)
|
|
"""
|
|
|
|
|
|
class RbacAdminModule:
|
|
"""Modul fuer Lehrer- und Rollenverwaltung."""
|
|
|
|
@staticmethod
|
|
def get_css() -> str:
|
|
"""CSS fuer das RBAC Admin Modul."""
|
|
return """
|
|
/* =============================================
|
|
RBAC ADMIN MODULE - Lehrer & Rollen
|
|
============================================= */
|
|
|
|
.panel-rbac-admin {
|
|
display: none;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
background: var(--bp-bg);
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.panel-rbac-admin.active {
|
|
display: flex;
|
|
}
|
|
|
|
/* RBAC Header */
|
|
.rbac-header {
|
|
padding: 24px 32px;
|
|
background: var(--bp-surface);
|
|
border-bottom: 1px solid var(--bp-border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.rbac-header h2 {
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
color: var(--bp-text);
|
|
margin: 0;
|
|
}
|
|
|
|
/* RBAC Tabs */
|
|
.rbac-tabs {
|
|
display: flex;
|
|
gap: 4px;
|
|
padding: 16px 32px;
|
|
background: var(--bp-surface);
|
|
border-bottom: 1px solid var(--bp-border);
|
|
}
|
|
|
|
.rbac-tab {
|
|
padding: 10px 20px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--bp-text-muted);
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.rbac-tab:hover {
|
|
background: var(--bp-bg);
|
|
color: var(--bp-text);
|
|
}
|
|
|
|
.rbac-tab.active {
|
|
background: var(--bp-primary);
|
|
color: white;
|
|
}
|
|
|
|
/* RBAC Content */
|
|
.rbac-content {
|
|
padding: 24px 32px;
|
|
flex: 1;
|
|
}
|
|
|
|
/* Role Summary Cards */
|
|
.rbac-summary-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 16px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.rbac-summary-card {
|
|
background: var(--bp-surface);
|
|
border: 1px solid var(--bp-border);
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.rbac-summary-card:hover {
|
|
border-color: var(--bp-primary);
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.rbac-summary-card.active {
|
|
border-color: var(--bp-primary);
|
|
background: var(--bp-primary-soft);
|
|
}
|
|
|
|
.rbac-summary-card-count {
|
|
font-size: 32px;
|
|
font-weight: 700;
|
|
color: var(--bp-primary);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.rbac-summary-card-label {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--bp-text);
|
|
}
|
|
|
|
.rbac-summary-card-category {
|
|
font-size: 11px;
|
|
color: var(--bp-text-muted);
|
|
margin-top: 4px;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/* Teacher Table */
|
|
.rbac-table-container {
|
|
background: var(--bp-surface);
|
|
border: 1px solid var(--bp-border);
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.rbac-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.rbac-table th,
|
|
.rbac-table td {
|
|
padding: 12px 16px;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--bp-border);
|
|
}
|
|
|
|
.rbac-table th {
|
|
background: var(--bp-bg);
|
|
font-weight: 600;
|
|
font-size: 12px;
|
|
color: var(--bp-text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.rbac-table tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.rbac-table tr:hover td {
|
|
background: var(--bp-bg);
|
|
}
|
|
|
|
/* Teacher Info */
|
|
.rbac-teacher-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.rbac-teacher-avatar {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
background: var(--bp-primary-soft);
|
|
color: var(--bp-primary);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.rbac-teacher-name {
|
|
font-weight: 500;
|
|
color: var(--bp-text);
|
|
}
|
|
|
|
.rbac-teacher-email {
|
|
font-size: 12px;
|
|
color: var(--bp-text-muted);
|
|
}
|
|
|
|
/* Role Badges */
|
|
.rbac-role-badges {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
}
|
|
|
|
.rbac-role-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 4px 10px;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
border-radius: 12px;
|
|
background: var(--bp-bg);
|
|
color: var(--bp-text-muted);
|
|
border: 1px solid var(--bp-border);
|
|
}
|
|
|
|
.rbac-role-badge.category-klausur {
|
|
background: #fff3cd;
|
|
color: #856404;
|
|
border-color: #ffc107;
|
|
}
|
|
|
|
.rbac-role-badge.category-zeugnis {
|
|
background: #d1ecf1;
|
|
color: #0c5460;
|
|
border-color: #17a2b8;
|
|
}
|
|
|
|
.rbac-role-badge.category-leitung {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
border-color: #28a745;
|
|
}
|
|
|
|
.rbac-role-badge.category-verwaltung {
|
|
background: #e2e3e5;
|
|
color: #383d41;
|
|
border-color: #6c757d;
|
|
}
|
|
|
|
.rbac-role-badge.category-admin {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
border-color: #dc3545;
|
|
}
|
|
|
|
/* Role Actions */
|
|
.rbac-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.rbac-btn {
|
|
padding: 6px 12px;
|
|
font-size: 12px;
|
|
border-radius: 6px;
|
|
border: 1px solid var(--bp-border);
|
|
background: var(--bp-surface);
|
|
color: var(--bp-text);
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.rbac-btn:hover {
|
|
background: var(--bp-bg);
|
|
border-color: var(--bp-primary);
|
|
}
|
|
|
|
.rbac-btn-primary {
|
|
background: var(--bp-primary);
|
|
color: white;
|
|
border-color: var(--bp-primary);
|
|
}
|
|
|
|
.rbac-btn-primary:hover {
|
|
background: var(--bp-primary-dark);
|
|
}
|
|
|
|
/* Role Assignment Modal */
|
|
.rbac-modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
z-index: 1000;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.rbac-modal.active {
|
|
display: flex;
|
|
}
|
|
|
|
.rbac-modal-content {
|
|
background: var(--bp-surface);
|
|
border-radius: 16px;
|
|
width: 90%;
|
|
max-width: 600px;
|
|
max-height: 80vh;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.rbac-modal-header {
|
|
padding: 20px 24px;
|
|
border-bottom: 1px solid var(--bp-border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.rbac-modal-header h3 {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.rbac-modal-close {
|
|
background: none;
|
|
border: none;
|
|
font-size: 24px;
|
|
cursor: pointer;
|
|
color: var(--bp-text-muted);
|
|
}
|
|
|
|
.rbac-modal-body {
|
|
padding: 24px;
|
|
overflow-y: auto;
|
|
flex: 1;
|
|
}
|
|
|
|
.rbac-modal-footer {
|
|
padding: 16px 24px;
|
|
border-top: 1px solid var(--bp-border);
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 12px;
|
|
}
|
|
|
|
/* Role Selector in Modal */
|
|
.rbac-role-selector {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 12px;
|
|
}
|
|
|
|
.rbac-role-option {
|
|
padding: 12px;
|
|
border: 1px solid var(--bp-border);
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.rbac-role-option:hover {
|
|
border-color: var(--bp-primary);
|
|
}
|
|
|
|
.rbac-role-option.selected {
|
|
border-color: var(--bp-primary);
|
|
background: var(--bp-primary-soft);
|
|
}
|
|
|
|
.rbac-role-option-name {
|
|
font-weight: 500;
|
|
color: var(--bp-text);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.rbac-role-option-desc {
|
|
font-size: 12px;
|
|
color: var(--bp-text-muted);
|
|
}
|
|
|
|
/* Loading & Empty States */
|
|
.rbac-loading,
|
|
.rbac-empty {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 48px;
|
|
color: var(--bp-text-muted);
|
|
}
|
|
|
|
.rbac-loading-spinner {
|
|
width: 32px;
|
|
height: 32px;
|
|
border: 3px solid var(--bp-border);
|
|
border-top-color: var(--bp-primary);
|
|
border-radius: 50%;
|
|
animation: rbac-spin 0.8s linear infinite;
|
|
}
|
|
|
|
@keyframes rbac-spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
"""
|
|
|
|
@staticmethod
|
|
def get_html() -> str:
|
|
"""HTML fuer das RBAC Admin Modul."""
|
|
return """
|
|
<!-- RBAC Admin Panel -->
|
|
<div class="panel-rbac-admin" id="panel-rbac-admin">
|
|
<div class="rbac-header">
|
|
<h2>Lehrer & Rollen</h2>
|
|
<div style="display: flex; gap: 8px;">
|
|
<button class="rbac-btn" onclick="rbacShowCreateTeacherModal()">
|
|
+ Neuer Lehrer
|
|
</button>
|
|
<button class="rbac-btn" onclick="rbacShowCreateRoleModal()">
|
|
+ Neue Rolle
|
|
</button>
|
|
<button class="rbac-btn rbac-btn-primary" onclick="rbacShowAssignModal()">
|
|
+ Rolle zuweisen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="rbac-tabs">
|
|
<button class="rbac-tab active" onclick="rbacSwitchTab('overview')">Uebersicht</button>
|
|
<button class="rbac-tab" onclick="rbacSwitchTab('teachers')">Alle Lehrer</button>
|
|
<button class="rbac-tab" onclick="rbacSwitchTab('roles')">Nach Rolle</button>
|
|
<button class="rbac-tab" onclick="rbacSwitchTab('manage-roles')">Rollen verwalten</button>
|
|
</div>
|
|
|
|
<div class="rbac-content">
|
|
<!-- Overview Tab -->
|
|
<div id="rbac-tab-overview" class="rbac-tab-content">
|
|
<div class="rbac-summary-grid" id="rbac-summary-grid">
|
|
<div class="rbac-loading">
|
|
<div class="rbac-loading-spinner"></div>
|
|
<p>Lade Rollen-Uebersicht...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Teachers Tab -->
|
|
<div id="rbac-tab-teachers" class="rbac-tab-content" style="display: none;">
|
|
<div class="rbac-table-container">
|
|
<table class="rbac-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Lehrer</th>
|
|
<th>Kuerzel</th>
|
|
<th>Rollen</th>
|
|
<th>Aktionen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="rbac-teachers-tbody">
|
|
<tr>
|
|
<td colspan="4">
|
|
<div class="rbac-loading">
|
|
<div class="rbac-loading-spinner"></div>
|
|
<p>Lade Lehrer...</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Roles Tab -->
|
|
<div id="rbac-tab-roles" class="rbac-tab-content" style="display: none;">
|
|
<div class="rbac-role-filter" style="margin-bottom: 16px;">
|
|
<select id="rbac-role-filter" class="bp-input" onchange="rbacFilterByRole()">
|
|
<option value="">-- Rolle auswaehlen --</option>
|
|
</select>
|
|
</div>
|
|
<div class="rbac-table-container">
|
|
<table class="rbac-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Lehrer</th>
|
|
<th>Email</th>
|
|
<th>Alle Rollen</th>
|
|
<th>Aktionen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="rbac-roles-tbody">
|
|
<tr>
|
|
<td colspan="4">
|
|
<div class="rbac-empty">
|
|
<p>Bitte waehlen Sie eine Rolle aus.</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Manage Roles Tab -->
|
|
<div id="rbac-tab-manage-roles" class="rbac-tab-content" style="display: none;">
|
|
<h3 style="margin-bottom: 16px;">System-Rollen (nicht bearbeitbar)</h3>
|
|
<div class="rbac-table-container" style="margin-bottom: 24px;">
|
|
<table class="rbac-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Rollen-Key</th>
|
|
<th>Anzeigename</th>
|
|
<th>Beschreibung</th>
|
|
<th>Kategorie</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="rbac-system-roles-tbody">
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<h3 style="margin-bottom: 16px;">Eigene Rollen</h3>
|
|
<div class="rbac-table-container">
|
|
<table class="rbac-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Rollen-Key</th>
|
|
<th>Anzeigename</th>
|
|
<th>Beschreibung</th>
|
|
<th>Kategorie</th>
|
|
<th>Aktionen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="rbac-custom-roles-tbody">
|
|
<tr>
|
|
<td colspan="5">
|
|
<div class="rbac-empty">
|
|
<p>Keine eigenen Rollen angelegt.</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Role Assignment Modal -->
|
|
<div class="rbac-modal" id="rbac-assign-modal">
|
|
<div class="rbac-modal-content">
|
|
<div class="rbac-modal-header">
|
|
<h3>Rolle zuweisen</h3>
|
|
<button class="rbac-modal-close" onclick="rbacCloseModal()">×</button>
|
|
</div>
|
|
<div class="rbac-modal-body">
|
|
<div style="margin-bottom: 16px;">
|
|
<label class="bp-label">Lehrer auswaehlen:</label>
|
|
<select id="rbac-modal-teacher" class="bp-input" style="width: 100%;">
|
|
<option value="">-- Lehrer auswaehlen --</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="bp-label">Rolle auswaehlen:</label>
|
|
<div class="rbac-role-selector" id="rbac-role-selector">
|
|
<!-- Wird dynamisch gefuellt -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="rbac-modal-footer">
|
|
<button class="rbac-btn" onclick="rbacCloseModal()">Abbrechen</button>
|
|
<button class="rbac-btn rbac-btn-primary" onclick="rbacAssignRole()">Zuweisen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create Teacher Modal -->
|
|
<div class="rbac-modal" id="rbac-create-teacher-modal">
|
|
<div class="rbac-modal-content">
|
|
<div class="rbac-modal-header">
|
|
<h3>Neuen Lehrer anlegen</h3>
|
|
<button class="rbac-modal-close" onclick="rbacCloseCreateTeacherModal()">×</button>
|
|
</div>
|
|
<div class="rbac-modal-body">
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px;">
|
|
<div>
|
|
<label class="bp-label">Vorname *</label>
|
|
<input type="text" id="rbac-teacher-firstname" class="bp-input" style="width: 100%;" placeholder="Vorname">
|
|
</div>
|
|
<div>
|
|
<label class="bp-label">Nachname *</label>
|
|
<input type="text" id="rbac-teacher-lastname" class="bp-input" style="width: 100%;" placeholder="Nachname">
|
|
</div>
|
|
</div>
|
|
<div style="margin-bottom: 16px;">
|
|
<label class="bp-label">E-Mail *</label>
|
|
<input type="email" id="rbac-teacher-email" class="bp-input" style="width: 100%;" placeholder="email@schule.de">
|
|
</div>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px;">
|
|
<div>
|
|
<label class="bp-label">Kuerzel</label>
|
|
<input type="text" id="rbac-teacher-code" class="bp-input" style="width: 100%;" placeholder="MUE" maxlength="5">
|
|
</div>
|
|
<div>
|
|
<label class="bp-label">Titel</label>
|
|
<input type="text" id="rbac-teacher-title" class="bp-input" style="width: 100%;" placeholder="Dr., Prof., StD">
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="bp-label">Initiale Rollen (optional):</label>
|
|
<div class="rbac-role-selector" id="rbac-new-teacher-roles" style="max-height: 200px; overflow-y: auto;">
|
|
<!-- Wird dynamisch gefuellt -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="rbac-modal-footer">
|
|
<button class="rbac-btn" onclick="rbacCloseCreateTeacherModal()">Abbrechen</button>
|
|
<button class="rbac-btn rbac-btn-primary" onclick="rbacCreateTeacher()">Lehrer anlegen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create Role Modal -->
|
|
<div class="rbac-modal" id="rbac-create-role-modal">
|
|
<div class="rbac-modal-content">
|
|
<div class="rbac-modal-header">
|
|
<h3>Neue Rolle anlegen</h3>
|
|
<button class="rbac-modal-close" onclick="rbacCloseCreateRoleModal()">×</button>
|
|
</div>
|
|
<div class="rbac-modal-body">
|
|
<div style="margin-bottom: 16px;">
|
|
<label class="bp-label">Rollen-Key * (z.B. "vertretung", "mentor")</label>
|
|
<input type="text" id="rbac-role-key" class="bp-input" style="width: 100%;" placeholder="rollen_key" pattern="[a-z_]+">
|
|
<small style="color: var(--bp-text-muted);">Nur Kleinbuchstaben und Unterstriche</small>
|
|
</div>
|
|
<div style="margin-bottom: 16px;">
|
|
<label class="bp-label">Anzeigename *</label>
|
|
<input type="text" id="rbac-role-displayname" class="bp-input" style="width: 100%;" placeholder="Vertretungslehrer/in">
|
|
</div>
|
|
<div style="margin-bottom: 16px;">
|
|
<label class="bp-label">Beschreibung *</label>
|
|
<textarea id="rbac-role-description" class="bp-input" style="width: 100%; min-height: 80px;" placeholder="Beschreibung der Rolle und ihrer Berechtigungen"></textarea>
|
|
</div>
|
|
<div>
|
|
<label class="bp-label">Kategorie *</label>
|
|
<select id="rbac-role-category" class="bp-input" style="width: 100%;">
|
|
<option value="klausur">Klausur</option>
|
|
<option value="zeugnis">Zeugnis</option>
|
|
<option value="leitung">Leitung</option>
|
|
<option value="verwaltung">Verwaltung</option>
|
|
<option value="admin">Administration</option>
|
|
<option value="other" selected>Sonstige</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="rbac-modal-footer">
|
|
<button class="rbac-btn" onclick="rbacCloseCreateRoleModal()">Abbrechen</button>
|
|
<button class="rbac-btn rbac-btn-primary" onclick="rbacCreateRole()">Rolle anlegen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Teacher Roles Modal -->
|
|
<div class="rbac-modal" id="rbac-edit-roles-modal">
|
|
<div class="rbac-modal-content">
|
|
<div class="rbac-modal-header">
|
|
<h3>Rollen bearbeiten: <span id="rbac-edit-teacher-name"></span></h3>
|
|
<button class="rbac-modal-close" onclick="rbacCloseEditRolesModal()">×</button>
|
|
</div>
|
|
<div class="rbac-modal-body">
|
|
<p style="margin-bottom: 16px; color: var(--bp-text-muted);">
|
|
Waehlen Sie die Rollen aus, die diesem Lehrer zugewiesen werden sollen.
|
|
Bereits zugewiesene Rollen sind markiert.
|
|
</p>
|
|
<div class="rbac-role-selector" id="rbac-edit-roles-selector" style="max-height: 400px; overflow-y: auto;">
|
|
<!-- Wird dynamisch gefuellt -->
|
|
</div>
|
|
</div>
|
|
<div class="rbac-modal-footer">
|
|
<button class="rbac-btn" onclick="rbacCloseEditRolesModal()">Abbrechen</button>
|
|
<button class="rbac-btn rbac-btn-primary" onclick="rbacSaveTeacherRoles()">Speichern</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
"""
|
|
|
|
@staticmethod
|
|
def get_js() -> str:
|
|
"""JavaScript fuer das RBAC Admin Modul."""
|
|
return """
|
|
// =============================================
|
|
// RBAC ADMIN MODULE
|
|
// =============================================
|
|
|
|
let rbacTeachers = [];
|
|
let rbacRoles = [];
|
|
let rbacCustomRoles = [];
|
|
let rbacSummary = null;
|
|
let rbacSelectedRole = null;
|
|
let rbacCurrentTab = 'overview';
|
|
let rbacEditingTeacher = null;
|
|
|
|
// Role category colors
|
|
const ROLE_CATEGORIES = {
|
|
klausur: 'category-klausur',
|
|
zeugnis: 'category-zeugnis',
|
|
leitung: 'category-leitung',
|
|
verwaltung: 'category-verwaltung',
|
|
admin: 'category-admin',
|
|
other: ''
|
|
};
|
|
|
|
// Initialize RBAC Module
|
|
window.loadRbacAdminModule = function() {
|
|
console.log('Loading RBAC Admin module');
|
|
rbacLoadData();
|
|
};
|
|
|
|
// Load all data
|
|
async function rbacLoadData() {
|
|
try {
|
|
await Promise.all([
|
|
rbacLoadSummary(),
|
|
rbacLoadTeachers(),
|
|
rbacLoadRoles(),
|
|
rbacLoadCustomRoles()
|
|
]);
|
|
} catch (error) {
|
|
console.error('Error loading RBAC data:', error);
|
|
}
|
|
}
|
|
|
|
// Load summary
|
|
async function rbacLoadSummary() {
|
|
try {
|
|
const response = await fetch('/api/rbac/summary');
|
|
if (response.ok) {
|
|
rbacSummary = await response.json();
|
|
rbacRenderSummary();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading summary:', error);
|
|
}
|
|
}
|
|
|
|
// Load teachers
|
|
async function rbacLoadTeachers() {
|
|
try {
|
|
const response = await fetch('/api/rbac/teachers');
|
|
if (response.ok) {
|
|
rbacTeachers = await response.json();
|
|
rbacRenderTeachers();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading teachers:', error);
|
|
}
|
|
}
|
|
|
|
// Load roles
|
|
async function rbacLoadRoles() {
|
|
try {
|
|
const response = await fetch('/api/rbac/roles');
|
|
if (response.ok) {
|
|
rbacRoles = await response.json();
|
|
rbacPopulateRoleSelector();
|
|
rbacPopulateRoleFilter();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading roles:', error);
|
|
}
|
|
}
|
|
|
|
// Render summary cards
|
|
function rbacRenderSummary() {
|
|
const grid = document.getElementById('rbac-summary-grid');
|
|
if (!rbacSummary) return;
|
|
|
|
// Total teachers card
|
|
let html = `
|
|
<div class="rbac-summary-card" onclick="rbacSwitchTab('teachers')">
|
|
<div class="rbac-summary-card-count">${rbacSummary.total_teachers}</div>
|
|
<div class="rbac-summary-card-label">Lehrer gesamt</div>
|
|
<div class="rbac-summary-card-category">Aktive Lehrkraefte</div>
|
|
</div>
|
|
`;
|
|
|
|
// Role cards
|
|
rbacSummary.roles.forEach(role => {
|
|
const categoryClass = ROLE_CATEGORIES[role.category] || '';
|
|
html += `
|
|
<div class="rbac-summary-card ${rbacSelectedRole === role.role ? 'active' : ''}"
|
|
onclick="rbacSelectRoleFromSummary('${role.role}')">
|
|
<div class="rbac-summary-card-count">${role.count}</div>
|
|
<div class="rbac-summary-card-label">${role.display_name}</div>
|
|
<div class="rbac-summary-card-category">${role.category}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
grid.innerHTML = html;
|
|
}
|
|
|
|
// Render teachers table
|
|
function rbacRenderTeachers() {
|
|
const tbody = document.getElementById('rbac-teachers-tbody');
|
|
if (!rbacTeachers.length) {
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="4">
|
|
<div class="rbac-empty">
|
|
<p>Keine Lehrer gefunden.</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
rbacTeachers.forEach(teacher => {
|
|
const initials = (teacher.first_name[0] + teacher.last_name[0]).toUpperCase();
|
|
const roleBadges = teacher.roles.map(role => {
|
|
const roleInfo = rbacRoles.find(r => r.role === role);
|
|
const category = roleInfo ? roleInfo.category : 'other';
|
|
const displayName = roleInfo ? roleInfo.display_name : role;
|
|
return `<span class="rbac-role-badge ${ROLE_CATEGORIES[category]}">${displayName}</span>`;
|
|
}).join('');
|
|
|
|
html += `
|
|
<tr>
|
|
<td>
|
|
<div class="rbac-teacher-info">
|
|
<div class="rbac-teacher-avatar">${initials}</div>
|
|
<div>
|
|
<div class="rbac-teacher-name">${teacher.title || ''} ${teacher.first_name} ${teacher.last_name}</div>
|
|
<div class="rbac-teacher-email">${teacher.email}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>${teacher.teacher_code || '-'}</td>
|
|
<td>
|
|
<div class="rbac-role-badges">
|
|
${roleBadges || '<span class="rbac-role-badge">Keine Rolle</span>'}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="rbac-actions">
|
|
<button class="rbac-btn" onclick="rbacEditTeacherRoles('${teacher.id}')">
|
|
Rollen bearbeiten
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
tbody.innerHTML = html;
|
|
}
|
|
|
|
// Populate role selector in modal
|
|
function rbacPopulateRoleSelector() {
|
|
const selector = document.getElementById('rbac-role-selector');
|
|
let html = '';
|
|
|
|
rbacRoles.forEach(role => {
|
|
html += `
|
|
<div class="rbac-role-option" data-role="${role.role}" onclick="rbacToggleRoleOption(this)">
|
|
<div class="rbac-role-option-name">${role.display_name}</div>
|
|
<div class="rbac-role-option-desc">${role.description}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
selector.innerHTML = html;
|
|
}
|
|
|
|
// Populate role filter dropdown
|
|
function rbacPopulateRoleFilter() {
|
|
const filter = document.getElementById('rbac-role-filter');
|
|
let html = '<option value="">-- Rolle auswaehlen --</option>';
|
|
|
|
rbacRoles.forEach(role => {
|
|
html += `<option value="${role.role}">${role.display_name}</option>`;
|
|
});
|
|
|
|
filter.innerHTML = html;
|
|
}
|
|
|
|
// Toggle role option in modal
|
|
function rbacToggleRoleOption(element) {
|
|
// Remove selected from all
|
|
document.querySelectorAll('.rbac-role-option').forEach(el => {
|
|
el.classList.remove('selected');
|
|
});
|
|
// Add selected to clicked
|
|
element.classList.add('selected');
|
|
}
|
|
|
|
// Switch tabs
|
|
function rbacSwitchTab(tab) {
|
|
rbacCurrentTab = tab;
|
|
|
|
// Update tab buttons
|
|
document.querySelectorAll('.rbac-tab').forEach((btn, index) => {
|
|
btn.classList.remove('active');
|
|
const tabNames = ['overview', 'teachers', 'roles', 'manage-roles'];
|
|
if (tabNames[index] === tab) {
|
|
btn.classList.add('active');
|
|
}
|
|
});
|
|
|
|
// Update tab content
|
|
document.querySelectorAll('.rbac-tab-content').forEach(content => {
|
|
content.style.display = 'none';
|
|
});
|
|
document.getElementById(`rbac-tab-${tab}`).style.display = 'block';
|
|
|
|
// Load manage roles data when switching to that tab
|
|
if (tab === 'manage-roles') {
|
|
rbacRenderManageRoles();
|
|
}
|
|
}
|
|
|
|
// Select role from summary
|
|
function rbacSelectRoleFromSummary(role) {
|
|
rbacSelectedRole = role;
|
|
rbacRenderSummary();
|
|
rbacSwitchTab('roles');
|
|
document.getElementById('rbac-role-filter').value = role;
|
|
rbacFilterByRole();
|
|
}
|
|
|
|
// Filter by role
|
|
async function rbacFilterByRole() {
|
|
const role = document.getElementById('rbac-role-filter').value;
|
|
const tbody = document.getElementById('rbac-roles-tbody');
|
|
|
|
if (!role) {
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="4">
|
|
<div class="rbac-empty">
|
|
<p>Bitte waehlen Sie eine Rolle aus.</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="4">
|
|
<div class="rbac-loading">
|
|
<div class="rbac-loading-spinner"></div>
|
|
<p>Lade Lehrer...</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
|
|
try {
|
|
const response = await fetch(`/api/rbac/roles/${role}/teachers`);
|
|
if (response.ok) {
|
|
const teachers = await response.json();
|
|
rbacRenderRoleTeachers(teachers);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error filtering by role:', error);
|
|
}
|
|
}
|
|
|
|
// Render teachers for role
|
|
function rbacRenderRoleTeachers(teachers) {
|
|
const tbody = document.getElementById('rbac-roles-tbody');
|
|
|
|
if (!teachers.length) {
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="4">
|
|
<div class="rbac-empty">
|
|
<p>Keine Lehrer mit dieser Rolle.</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
teachers.forEach(teacher => {
|
|
const initials = (teacher.first_name[0] + teacher.last_name[0]).toUpperCase();
|
|
const roleBadges = teacher.roles.map(role => {
|
|
const roleInfo = rbacRoles.find(r => r.role === role);
|
|
const category = roleInfo ? roleInfo.category : 'other';
|
|
const displayName = roleInfo ? roleInfo.display_name : role;
|
|
return `<span class="rbac-role-badge ${ROLE_CATEGORIES[category]}">${displayName}</span>`;
|
|
}).join('');
|
|
|
|
html += `
|
|
<tr>
|
|
<td>
|
|
<div class="rbac-teacher-info">
|
|
<div class="rbac-teacher-avatar">${initials}</div>
|
|
<div>
|
|
<div class="rbac-teacher-name">${teacher.title || ''} ${teacher.first_name} ${teacher.last_name}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>${teacher.email}</td>
|
|
<td>
|
|
<div class="rbac-role-badges">
|
|
${roleBadges}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="rbac-actions">
|
|
<button class="rbac-btn" onclick="rbacRemoveRole('${teacher.user_id}', '${document.getElementById('rbac-role-filter').value}')">
|
|
Rolle entziehen
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
tbody.innerHTML = html;
|
|
}
|
|
|
|
// Show assign modal
|
|
function rbacShowAssignModal() {
|
|
// Populate teacher dropdown
|
|
const teacherSelect = document.getElementById('rbac-modal-teacher');
|
|
let html = '<option value="">-- Lehrer auswaehlen --</option>';
|
|
rbacTeachers.forEach(teacher => {
|
|
html += `<option value="${teacher.user_id}">${teacher.first_name} ${teacher.last_name} (${teacher.teacher_code || teacher.email})</option>`;
|
|
});
|
|
teacherSelect.innerHTML = html;
|
|
|
|
// Show modal
|
|
document.getElementById('rbac-assign-modal').classList.add('active');
|
|
}
|
|
|
|
// Close modal
|
|
function rbacCloseModal() {
|
|
document.getElementById('rbac-assign-modal').classList.remove('active');
|
|
// Reset selections
|
|
document.querySelectorAll('.rbac-role-option').forEach(el => {
|
|
el.classList.remove('selected');
|
|
});
|
|
document.getElementById('rbac-modal-teacher').value = '';
|
|
}
|
|
|
|
// Assign role
|
|
async function rbacAssignRole() {
|
|
const teacherId = document.getElementById('rbac-modal-teacher').value;
|
|
const selectedRole = document.querySelector('.rbac-role-option.selected');
|
|
|
|
if (!teacherId) {
|
|
alert('Bitte waehlen Sie einen Lehrer aus.');
|
|
return;
|
|
}
|
|
|
|
if (!selectedRole) {
|
|
alert('Bitte waehlen Sie eine Rolle aus.');
|
|
return;
|
|
}
|
|
|
|
const role = selectedRole.dataset.role;
|
|
|
|
try {
|
|
const response = await fetch('/api/rbac/assignments', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
user_id: teacherId,
|
|
role: role,
|
|
resource_type: 'tenant',
|
|
resource_id: 'a0000000-0000-0000-0000-000000000001'
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
rbacCloseModal();
|
|
await rbacLoadData();
|
|
alert('Rolle erfolgreich zugewiesen!');
|
|
} else if (response.status === 409) {
|
|
alert('Diese Rolle ist bereits zugewiesen.');
|
|
} else {
|
|
const error = await response.json();
|
|
alert('Fehler: ' + (error.detail || 'Unbekannter Fehler'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error assigning role:', error);
|
|
alert('Fehler beim Zuweisen der Rolle.');
|
|
}
|
|
}
|
|
|
|
// Remove role
|
|
async function rbacRemoveRole(userId, role) {
|
|
if (!confirm('Moechten Sie diese Rolle wirklich entziehen?')) {
|
|
return;
|
|
}
|
|
|
|
// Find the assignment ID
|
|
try {
|
|
const response = await fetch(`/api/rbac/teachers/${userId}/roles`);
|
|
if (response.ok) {
|
|
// Get teacher by user_id - need to find assignment
|
|
// For simplicity, we reload data after removal
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
}
|
|
}
|
|
|
|
// Edit teacher roles - opens modal with multi-select
|
|
function rbacEditTeacherRoles(teacherId) {
|
|
const teacher = rbacTeachers.find(t => t.id === teacherId);
|
|
if (!teacher) return;
|
|
|
|
rbacEditingTeacher = teacher;
|
|
document.getElementById('rbac-edit-teacher-name').textContent = `${teacher.first_name} ${teacher.last_name}`;
|
|
|
|
// Populate role selector with checkboxes
|
|
const selector = document.getElementById('rbac-edit-roles-selector');
|
|
let html = '';
|
|
|
|
rbacRoles.forEach(role => {
|
|
const isAssigned = teacher.roles.includes(role.role);
|
|
html += `
|
|
<div class="rbac-role-option ${isAssigned ? 'selected' : ''}"
|
|
data-role="${role.role}" onclick="rbacToggleMultiRole(this)">
|
|
<div class="rbac-role-option-name">
|
|
<input type="checkbox" ${isAssigned ? 'checked' : ''} style="margin-right: 8px;">
|
|
${role.display_name}
|
|
</div>
|
|
<div class="rbac-role-option-desc">${role.description}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
// Also include custom roles
|
|
rbacCustomRoles.forEach(role => {
|
|
const isAssigned = teacher.roles.includes(role.role);
|
|
html += `
|
|
<div class="rbac-role-option ${isAssigned ? 'selected' : ''}"
|
|
data-role="${role.role}" onclick="rbacToggleMultiRole(this)">
|
|
<div class="rbac-role-option-name">
|
|
<input type="checkbox" ${isAssigned ? 'checked' : ''} style="margin-right: 8px;">
|
|
${role.display_name} <small>(Eigene Rolle)</small>
|
|
</div>
|
|
<div class="rbac-role-option-desc">${role.description}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
selector.innerHTML = html;
|
|
document.getElementById('rbac-edit-roles-modal').classList.add('active');
|
|
}
|
|
|
|
// Toggle multi-role selection
|
|
function rbacToggleMultiRole(element) {
|
|
element.classList.toggle('selected');
|
|
const checkbox = element.querySelector('input[type="checkbox"]');
|
|
if (checkbox) checkbox.checked = element.classList.contains('selected');
|
|
}
|
|
|
|
// Save teacher roles
|
|
async function rbacSaveTeacherRoles() {
|
|
if (!rbacEditingTeacher) return;
|
|
|
|
const selectedRoles = [];
|
|
document.querySelectorAll('#rbac-edit-roles-selector .rbac-role-option.selected').forEach(el => {
|
|
selectedRoles.push(el.dataset.role);
|
|
});
|
|
|
|
const currentRoles = rbacEditingTeacher.roles;
|
|
const rolesToAdd = selectedRoles.filter(r => !currentRoles.includes(r));
|
|
const rolesToRemove = currentRoles.filter(r => !selectedRoles.includes(r));
|
|
|
|
try {
|
|
// Add new roles
|
|
for (const role of rolesToAdd) {
|
|
await fetch('/api/rbac/assignments', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
user_id: rbacEditingTeacher.user_id,
|
|
role: role,
|
|
resource_type: 'tenant',
|
|
resource_id: 'a0000000-0000-0000-0000-000000000001'
|
|
})
|
|
});
|
|
}
|
|
|
|
// Remove roles
|
|
for (const role of rolesToRemove) {
|
|
// First get the assignment ID
|
|
const response = await fetch(`/api/rbac/teachers/${rbacEditingTeacher.id}/roles`);
|
|
if (response.ok) {
|
|
const assignments = await response.json();
|
|
const assignment = assignments.find(a => a.role === role && a.is_active);
|
|
if (assignment) {
|
|
await fetch(`/api/rbac/assignments/${assignment.id}`, { method: 'DELETE' });
|
|
}
|
|
}
|
|
}
|
|
|
|
rbacCloseEditRolesModal();
|
|
await rbacLoadData();
|
|
alert('Rollen erfolgreich aktualisiert!');
|
|
} catch (error) {
|
|
console.error('Error saving roles:', error);
|
|
alert('Fehler beim Speichern der Rollen.');
|
|
}
|
|
}
|
|
|
|
// Close edit roles modal
|
|
function rbacCloseEditRolesModal() {
|
|
document.getElementById('rbac-edit-roles-modal').classList.remove('active');
|
|
rbacEditingTeacher = null;
|
|
}
|
|
|
|
// =============================================
|
|
// CREATE TEACHER FUNCTIONS
|
|
// =============================================
|
|
|
|
function rbacShowCreateTeacherModal() {
|
|
// Populate role selector for new teacher
|
|
const selector = document.getElementById('rbac-new-teacher-roles');
|
|
let html = '';
|
|
|
|
rbacRoles.forEach(role => {
|
|
html += `
|
|
<div class="rbac-role-option" data-role="${role.role}" onclick="rbacToggleMultiRole(this)">
|
|
<div class="rbac-role-option-name">
|
|
<input type="checkbox" style="margin-right: 8px;">
|
|
${role.display_name}
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
selector.innerHTML = html;
|
|
document.getElementById('rbac-create-teacher-modal').classList.add('active');
|
|
}
|
|
|
|
function rbacCloseCreateTeacherModal() {
|
|
document.getElementById('rbac-create-teacher-modal').classList.remove('active');
|
|
// Clear form
|
|
document.getElementById('rbac-teacher-firstname').value = '';
|
|
document.getElementById('rbac-teacher-lastname').value = '';
|
|
document.getElementById('rbac-teacher-email').value = '';
|
|
document.getElementById('rbac-teacher-code').value = '';
|
|
document.getElementById('rbac-teacher-title').value = '';
|
|
}
|
|
|
|
async function rbacCreateTeacher() {
|
|
const firstName = document.getElementById('rbac-teacher-firstname').value.trim();
|
|
const lastName = document.getElementById('rbac-teacher-lastname').value.trim();
|
|
const email = document.getElementById('rbac-teacher-email').value.trim();
|
|
const teacherCode = document.getElementById('rbac-teacher-code').value.trim();
|
|
const title = document.getElementById('rbac-teacher-title').value.trim();
|
|
|
|
if (!firstName || !lastName || !email) {
|
|
alert('Bitte fuellen Sie alle Pflichtfelder aus (Vorname, Nachname, E-Mail).');
|
|
return;
|
|
}
|
|
|
|
// Get selected roles
|
|
const roles = [];
|
|
document.querySelectorAll('#rbac-new-teacher-roles .rbac-role-option.selected').forEach(el => {
|
|
roles.push(el.dataset.role);
|
|
});
|
|
|
|
try {
|
|
const response = await fetch('/api/rbac/teachers', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
first_name: firstName,
|
|
last_name: lastName,
|
|
email: email,
|
|
teacher_code: teacherCode || null,
|
|
title: title || null,
|
|
roles: roles
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
rbacCloseCreateTeacherModal();
|
|
await rbacLoadData();
|
|
alert('Lehrer erfolgreich angelegt!');
|
|
} else if (response.status === 409) {
|
|
alert('Ein Lehrer mit dieser E-Mail existiert bereits.');
|
|
} else {
|
|
const error = await response.json();
|
|
alert('Fehler: ' + (error.detail || 'Unbekannter Fehler'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating teacher:', error);
|
|
alert('Fehler beim Anlegen des Lehrers.');
|
|
}
|
|
}
|
|
|
|
// =============================================
|
|
// CREATE/MANAGE ROLE FUNCTIONS
|
|
// =============================================
|
|
|
|
// Load custom roles
|
|
async function rbacLoadCustomRoles() {
|
|
try {
|
|
const response = await fetch('/api/rbac/custom-roles');
|
|
if (response.ok) {
|
|
rbacCustomRoles = await response.json();
|
|
rbacRenderManageRoles();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading custom roles:', error);
|
|
}
|
|
}
|
|
|
|
// Render manage roles tab
|
|
function rbacRenderManageRoles() {
|
|
// System roles (built-in)
|
|
const systemTbody = document.getElementById('rbac-system-roles-tbody');
|
|
let systemHtml = '';
|
|
rbacRoles.forEach(role => {
|
|
systemHtml += `
|
|
<tr>
|
|
<td><code>${role.role}</code></td>
|
|
<td>${role.display_name}</td>
|
|
<td>${role.description}</td>
|
|
<td><span class="rbac-role-badge ${ROLE_CATEGORIES[role.category] || ''}">${role.category}</span></td>
|
|
</tr>
|
|
`;
|
|
});
|
|
systemTbody.innerHTML = systemHtml;
|
|
|
|
// Custom roles
|
|
const customTbody = document.getElementById('rbac-custom-roles-tbody');
|
|
if (!rbacCustomRoles.length) {
|
|
customTbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="5">
|
|
<div class="rbac-empty">
|
|
<p>Keine eigenen Rollen angelegt.</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
} else {
|
|
let customHtml = '';
|
|
rbacCustomRoles.forEach(role => {
|
|
customHtml += `
|
|
<tr>
|
|
<td><code>${role.role}</code></td>
|
|
<td>${role.display_name}</td>
|
|
<td>${role.description}</td>
|
|
<td><span class="rbac-role-badge ${ROLE_CATEGORIES[role.category] || ''}">${role.category}</span></td>
|
|
<td>
|
|
<div class="rbac-actions">
|
|
<button class="rbac-btn" onclick="rbacDeleteCustomRole('${role.role}')" style="color: var(--bp-danger);">
|
|
Loeschen
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
customTbody.innerHTML = customHtml;
|
|
}
|
|
}
|
|
|
|
function rbacShowCreateRoleModal() {
|
|
document.getElementById('rbac-create-role-modal').classList.add('active');
|
|
}
|
|
|
|
function rbacCloseCreateRoleModal() {
|
|
document.getElementById('rbac-create-role-modal').classList.remove('active');
|
|
// Clear form
|
|
document.getElementById('rbac-role-key').value = '';
|
|
document.getElementById('rbac-role-displayname').value = '';
|
|
document.getElementById('rbac-role-description').value = '';
|
|
document.getElementById('rbac-role-category').value = 'other';
|
|
}
|
|
|
|
async function rbacCreateRole() {
|
|
const roleKey = document.getElementById('rbac-role-key').value.trim().toLowerCase().replace(/[^a-z_]/g, '');
|
|
const displayName = document.getElementById('rbac-role-displayname').value.trim();
|
|
const description = document.getElementById('rbac-role-description').value.trim();
|
|
const category = document.getElementById('rbac-role-category').value;
|
|
|
|
if (!roleKey || !displayName || !description) {
|
|
alert('Bitte fuellen Sie alle Pflichtfelder aus.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/rbac/custom-roles', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
role_key: roleKey,
|
|
display_name: displayName,
|
|
description: description,
|
|
category: category
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
rbacCloseCreateRoleModal();
|
|
await rbacLoadData();
|
|
alert('Rolle erfolgreich angelegt!');
|
|
} else if (response.status === 409) {
|
|
alert('Eine Rolle mit diesem Key existiert bereits.');
|
|
} else {
|
|
const error = await response.json();
|
|
alert('Fehler: ' + (error.detail || 'Unbekannter Fehler'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating role:', error);
|
|
alert('Fehler beim Anlegen der Rolle.');
|
|
}
|
|
}
|
|
|
|
async function rbacDeleteCustomRole(roleKey) {
|
|
if (!confirm(`Moechten Sie die Rolle "${roleKey}" wirklich loeschen? Alle Zuweisungen dieser Rolle werden ebenfalls entfernt.`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/rbac/custom-roles/${roleKey}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (response.ok) {
|
|
await rbacLoadData();
|
|
alert('Rolle erfolgreich geloescht!');
|
|
} else {
|
|
const error = await response.json();
|
|
alert('Fehler: ' + (error.detail || 'Unbekannter Fehler'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting role:', error);
|
|
alert('Fehler beim Loeschen der Rolle.');
|
|
}
|
|
}
|
|
|
|
// Initialize on panel show
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Auto-load when panel becomes active
|
|
const observer = new MutationObserver((mutations) => {
|
|
mutations.forEach((mutation) => {
|
|
if (mutation.target.classList && mutation.target.classList.contains('active')) {
|
|
if (mutation.target.id === 'panel-rbac-admin') {
|
|
rbacLoadData();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
const panel = document.getElementById('panel-rbac-admin');
|
|
if (panel) {
|
|
observer.observe(panel, { attributes: true, attributeFilter: ['class'] });
|
|
}
|
|
});
|
|
"""
|