480 lines
12 KiB
Markdown
480 lines
12 KiB
Markdown
# 📊 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):**
|
||
|
||
```python
|
||
@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`**
|
||
|
||
```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`:
|
||
|
||
```python
|
||
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:**
|
||
|
||
```nginx
|
||
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:
|
||
|
||
```javascript
|
||
// 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
|
||
|
||
1. **Kopiere den Code oben in `app.py`** (die neuen Routes)
|
||
2. **Erstelle `templates/admin.html`** mit dem HTML oben
|
||
3. **Starte Flask neu**: `python app.py`
|
||
4. **Öffne**: `http://localhost:5000/admin`
|
||
5. **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)
|
||
|