12 KiB
12 KiB
📊 Admin Dashboard Integration – 4 Optionen
Übersicht
Hier sind die Best Practices für die Integration eines Admin-Panels in die Flask-App:
Option 1: Embedded Admin-Route (Empfohlen für Beta)
Am schnellsten & sichersten für Beta-Phase
Wie es funktioniert:
- Admin-HTML wird direkt als Template serviert
- Nur für Admin-Benutzer zugänglich (/admin)
- Integriertes Logging-Dashboard
- Keine separate App nötig
Implementierung:
In app.py (nach den Error Handlers hinzufügen):
@app.route("/admin", methods=["GET"])
@auth.login_required
def admin_dashboard():
"""Admin Dashboard - nur für Admins"""
user = auth.current_user()
if user not in ADMIN_USERS:
log_access(user, "/admin", "GET", 403)
return jsonify({"error": "Unauthorized"}), 403
log_access(user, "/admin", "GET")
return render_template("admin.html", username=user)
@app.route("/api/admin/logs", methods=["GET"])
@auth.login_required
def get_admin_logs():
"""Logs als JSON für Dashboard"""
user = auth.current_user()
if user not in ADMIN_USERS:
return jsonify({"error": "Unauthorized"}), 403
logs = []
if os.path.exists(LOG_FILE):
with open(LOG_FILE, "r") as f:
logs = json.load(f)
# Optional: Letzte 100 Logs
return jsonify({"logs": logs[-100:]}), 200
@app.route("/api/admin/stats", methods=["GET"])
@auth.login_required
def get_admin_stats():
"""Statistiken für Dashboard"""
user = auth.current_user()
if user not in ADMIN_USERS:
return jsonify({"error": "Unauthorized"}), 403
logs = []
if os.path.exists(LOG_FILE):
with open(LOG_FILE, "r") as f:
logs = json.load(f)
# Statistiken berechnen
total_requests = len(logs)
unique_users = len(set(log["username"] for log in logs))
failed_logins = len([l for l in logs if l["status"] == 401])
return jsonify({
"total_requests": total_requests,
"unique_users": unique_users,
"failed_logins": failed_logins,
"last_update": datetime.now().isoformat()
}), 200
Neue Template-Datei: templates/admin.html
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Naue Admin Panel</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Arial, sans-serif;
background: #f4f6f9;
color: #333;
}
.admin-header {
background: linear-gradient(135deg, #003366 0%, #0066cc 100%);
color: white;
padding: 20px 32px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
display: flex;
justify-content: space-between;
align-items: center;
}
.admin-header h1 { font-size: 24px; }
.admin-header .user-info {
font-size: 12px;
opacity: 0.9;
}
.admin-container {
max-width: 1200px;
margin: 30px auto;
padding: 0 20px 60px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
border-left: 4px solid #0066cc;
}
.stat-card .label {
font-size: 12px;
color: #666;
text-transform: uppercase;
margin-bottom: 8px;
}
.stat-card .value {
font-size: 32px;
font-weight: 700;
color: #0066cc;
}
.logs-section {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
}
.logs-section h2 {
font-size: 18px;
margin-bottom: 20px;
color: #003366;
}
.logs-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.logs-table thead {
background: #f0f4fa;
border-bottom: 2px solid #0066cc;
}
.logs-table th {
padding: 12px;
text-align: left;
font-weight: 600;
color: #333;
}
.logs-table td {
padding: 10px 12px;
border-bottom: 1px solid #e8ecf0;
}
.logs-table tbody tr:hover {
background: #f8f9fa;
}
.status-200 { color: #28a745; font-weight: 600; }
.status-401 { color: #dc3545; font-weight: 600; }
.status-403 { color: #f0b429; font-weight: 600; }
.btn-refresh {
background: #0066cc;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-weight: 600;
margin-bottom: 20px;
}
.btn-refresh:hover { background: #0052a3; }
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.footer {
text-align: center;
font-size: 11px;
color: #aaa;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #e8ecf0;
}
</style>
</head>
<body>
<div class="admin-header">
<div>
<h1>🔐 Admin Panel</h1>
<div class="user-info">Roll Calculator – Naue</div>
</div>
<div class="user-info">
Angemeldet als: <strong>{{ username }}</strong>
</div>
</div>
<div class="admin-container">
<!-- Statistics -->
<div class="stats-grid" id="statsGrid">
<div class="loading">Statistiken werden geladen...</div>
</div>
<!-- Logs Section -->
<div class="logs-section">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2>📋 Access Logs</h2>
<button class="btn-refresh" onclick="refreshData()">🔄 Aktualisieren</button>
</div>
<table class="logs-table" id="logsTable">
<thead>
<tr>
<th>Zeitstempel</th>
<th>Benutzer</th>
<th>Endpoint</th>
<th>Methode</th>
<th>Status</th>
</tr>
</thead>
<tbody id="logsList">
<tr>
<td colspan="5" class="loading">Logs werden geladen...</td>
</tr>
</tbody>
</table>
</div>
<div class="footer">
Naue GmbH & Co. KG · Roll Calculator Admin Panel
</div>
</div>
<script>
async function refreshData() {
try {
// Fetch Statistics
const statsRes = await fetch('/api/admin/stats');
const stats = await statsRes.json();
// Fetch Logs
const logsRes = await fetch('/api/admin/logs');
const logsData = await logsRes.json();
// Display Statistics
const statsHtml = `
<div class="stat-card">
<div class="label">Gesamte Anfragen</div>
<div class="value">${stats.total_requests}</div>
</div>
<div class="stat-card">
<div class="label">Eindeutige Benutzer</div>
<div class="value">${stats.unique_users}</div>
</div>
<div class="stat-card">
<div class="label">Fehlgeschlagene Logins</div>
<div class="value" style="color: #dc3545;">${stats.failed_logins}</div>
</div>
`;
document.getElementById('statsGrid').innerHTML = statsHtml;
// Display Logs
const logsHtml = logsData.logs.reverse().map(log => {
const time = new Date(log.timestamp).toLocaleString('de-DE');
const statusClass = `status-${log.status}`;
return `
<tr>
<td>${time}</td>
<td>${log.username}</td>
<td>${log.endpoint}</td>
<td>${log.method}</td>
<td class="${statusClass}">${log.status}</td>
</tr>
`;
}).join('');
document.getElementById('logsList').innerHTML = logsHtml ||
'<tr><td colspan="5" style="text-align: center; padding: 20px;">Keine Logs verfügbar</td></tr>';
} catch (error) {
console.error('Fehler beim Laden der Daten:', error);
document.getElementById('logsList').innerHTML =
'<tr><td colspan="5" style="color: #dc3545;">Fehler beim Laden der Daten</td></tr>';
}
}
// Initial load
document.addEventListener('DOMContentLoaded', refreshData);
// Auto-refresh alle 30 Sekunden
setInterval(refreshData, 30000);
</script>
</body>
</html>
Zugriff:
- URL:
http://localhost:5000/admin - Nur für Admin-Benutzer (403 Unauthorized für andere)
- Automatische Log-Anzeige
Option 2: Separate Admin-App (für später)
Wenn Admin-Features wachsen:
Erstelle eine separate admin_app.py:
from flask import Flask, render_template
from flask_httpauth import HTTPBasicAuth
admin = Flask(__name__, template_folder='admin_templates')
auth_admin = HTTPBasicAuth()
ADMIN_USERS = {"admin": "admin_secure_pwd_2026"}
@auth_admin.verify_password
def verify(username, password):
if username in ADMIN_USERS and ADMIN_USERS[username] == password:
return username
return None
@admin.route("/", methods=["GET"])
@auth_admin.login_required
def dashboard():
return render_template("admin.html")
if __name__ == "__main__":
admin.run(port=5001, debug=False) # Anderer Port
Dann starten mit: python admin_app.py
Option 3: Reverse Proxy Setup (Production)
Mit Nginx als Reverse Proxy:
server {
listen 80;
server_name rollcalculator.naue.de;
# Main App (Port 5000)
location / {
proxy_pass http://localhost:5000;
proxy_set_header Host $host;
proxy_set_header Authorization $http_authorization;
}
# Admin (gleiche Auth, aber separates Template)
location /admin {
proxy_pass http://localhost:5000/admin;
proxy_set_header Host $host;
proxy_set_header Authorization $http_authorization;
}
}
Option 4: Vue.js / React Admin UI (für erweiterte Features)
Falls du später komplexere Admin-Features brauchst:
// static/js/admin-dashboard.js
class AdminDashboard {
constructor() {
this.stats = null;
this.logs = [];
this.init();
}
async init() {
await this.loadStats();
await this.loadLogs();
this.setupAutoRefresh();
}
async loadStats() {
const res = await fetch('/api/admin/stats');
this.stats = await res.json();
this.renderStats();
}
async loadLogs() {
const res = await fetch('/api/admin/logs');
this.logs = (await res.json()).logs;
this.renderLogs();
}
renderStats() {
// Render logic...
}
renderLogs() {
// Render logic...
}
setupAutoRefresh() {
setInterval(() => this.loadStats(), 30000);
setInterval(() => this.loadLogs(), 30000);
}
}
// Starten
new AdminDashboard();
🎯 Empfehlungen
| Phase | Option | Vorteile | Nachteile |
|---|---|---|---|
| Beta | Option 1 (Embedded) | ✅ Schnell, ✅ Sicher, ✅ Integriert | ⚠️ Einfaches UI |
| Production Beta | Option 1 + Option 3 | ✅ Mit HTTPS, ✅ Skalierbar | ⚠️ Nginx-Setup |
| Später erweitern | Option 4 (Vue/React) | ✅ Modern, ✅ Interactive | ⚠️ Komplexer |
| Separate Team | Option 2 (Separate App) | ✅ Unabhängig, ✅ Unterschiedliche Tech Stack | ⚠️ 2 Apps |
⚡ Quick Start: Option 1 implementieren
- Kopiere den Code oben in
app.py(die neuen Routes) - Erstelle
templates/admin.htmlmit dem HTML oben - Starte Flask neu:
python app.py - Öffne:
http://localhost:5000/admin - Login: admin / admin_secure_pwd_2026
Nächste Schritte
- admin.html als Template in templates/ speichern
- Routes in app.py hinzufügen
- Tests mit Admin-User durchführen
- Logs überprüfen
- Optional: Charts/Graphs hinzufügen (Chart.js)