added login screen and admin console
This commit is contained in:
@@ -0,0 +1,479 @@
|
||||
# 📊 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)
|
||||
|
||||
Reference in New Issue
Block a user