This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/frontend/modules/rbac_admin.py
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
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>
2026-02-09 09:51:32 +01:00

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()">&times;</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()">&times;</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()">&times;</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()">&times;</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'] });
}
});
"""