Features added: - Admin authentication system with is_admin field - Complete admin dashboard with user statistics - User management (create, edit, delete, toggle admin) - Protected admin routes with @admin_required decorator - Security features (prevent self-deletion, last admin protection) - Responsive admin UI with Bootstrap integration - Database migration script for admin field - Admin navigation link for authorized users Technical improvements: - Enhanced 3-column dashboard layout (tables | editor | saved queries) - Removed plus button and made right sidebar more compact - Admin user (admin/admin123) automatically created with admin privileges - Full CRUD operations for user management - Flash messages for user feedback - Form validation and error handling
154 lines
7.0 KiB
HTML
154 lines
7.0 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Benutzerverwaltung{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h2><i class="fas fa-users"></i> Benutzerverwaltung</h2>
|
|
<div>
|
|
<a href="{{ url_for('admin.create_user') }}" class="btn btn-success">
|
|
<i class="fas fa-user-plus"></i> Neuen Benutzer erstellen
|
|
</a>
|
|
<a href="{{ url_for('admin.admin_dashboard') }}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-arrow-left"></i> Admin Dashboard
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{% if users %}
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Benutzername</th>
|
|
<th>E-Mail</th>
|
|
<th>Rolle</th>
|
|
<th>Erstellt am</th>
|
|
<th>Aktionen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for user in users %}
|
|
<tr id="user-row-{{ user.id }}">
|
|
<td>{{ user.id }}</td>
|
|
<td>
|
|
<strong>{{ user.username }}</strong>
|
|
{% if user.id == current_user.id %}
|
|
<span class="badge bg-primary ms-1">Sie</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ user.email }}</td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
{% if user.is_admin %}
|
|
<span class="badge bg-danger me-2">
|
|
<i class="fas fa-user-shield"></i> Administrator
|
|
</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary me-2">
|
|
<i class="fas fa-user"></i> Benutzer
|
|
</span>
|
|
{% endif %}
|
|
|
|
{% if user.id != current_user.id %}
|
|
<button class="btn btn-xs btn-outline-info"
|
|
onclick="toggleAdmin({{ user.id }})"
|
|
title="Rolle wechseln">
|
|
<i class="fas fa-exchange-alt"></i>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
<td>{{ user.created_at.strftime('%d.%m.%Y %H:%M') }}</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<a href="{{ url_for('admin.edit_user', user_id=user.id) }}"
|
|
class="btn btn-outline-primary" title="Bearbeiten">
|
|
<i class="fas fa-edit"></i>
|
|
</a>
|
|
|
|
{% if user.id != current_user.id %}
|
|
<form method="POST" action="{{ url_for('admin.delete_user', user_id=user.id) }}"
|
|
class="d-inline" onsubmit="return confirm('Benutzer {{ user.username }} wirklich löschen?')">
|
|
<button type="submit" class="btn btn-outline-danger" title="Löschen">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="card">
|
|
<div class="card-body text-center py-5">
|
|
<i class="fas fa-users fa-3x text-muted mb-3"></i>
|
|
<h4>Keine Benutzer gefunden</h4>
|
|
<p class="text-muted">Erstellen Sie den ersten Benutzer.</p>
|
|
<a href="{{ url_for('admin.create_user') }}" class="btn btn-success">
|
|
<i class="fas fa-user-plus"></i> Ersten Benutzer erstellen
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
async function toggleAdmin(userId) {
|
|
try {
|
|
const response = await fetch(`/admin/users/${userId}/toggle_admin`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showAlert(data.message, 'success');
|
|
// Seite neu laden um Änderungen anzuzeigen
|
|
setTimeout(() => window.location.reload(), 1000);
|
|
} else {
|
|
showAlert(data.error, 'error');
|
|
}
|
|
} catch (error) {
|
|
showAlert(`Fehler: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
function showAlert(message, type = 'info') {
|
|
const alertClass = type === 'error' ? 'alert-danger' :
|
|
type === 'success' ? 'alert-success' : 'alert-info';
|
|
|
|
const alertHtml = `
|
|
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
|
|
${message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
`;
|
|
|
|
// Alert am Anfang der Seite einfügen
|
|
const container = document.querySelector('.container-fluid');
|
|
const existingAlert = container.querySelector('.alert');
|
|
if (existingAlert) {
|
|
existingAlert.remove();
|
|
}
|
|
|
|
container.insertAdjacentHTML('afterbegin', alertHtml);
|
|
}
|
|
</script>
|
|
{% endblock %} |