From fa9d79089a47795fd4841abf653493f1634c8067 Mon Sep 17 00:00:00 2001 From: UserIsMH Date: Mon, 9 Jun 2025 04:38:35 +0200 Subject: [PATCH] 2FA --- JOURNAL.md | 130 +++++++- v2_adminpanel/MIGRATION_2FA.md | 43 +++ v2_adminpanel/app.py | 357 ++++++++++++++++++++-- v2_adminpanel/create_users_table.sql | 20 ++ v2_adminpanel/init.sql | 21 ++ v2_adminpanel/migrate_users.py | 78 +++++ v2_adminpanel/requirements.txt | 3 + v2_adminpanel/templates/backup_codes.html | 228 ++++++++++++++ v2_adminpanel/templates/base.html | 1 + v2_adminpanel/templates/profile.html | 217 +++++++++++++ v2_adminpanel/templates/setup_2fa.html | 210 +++++++++++++ v2_adminpanel/templates/verify_2fa.html | 131 ++++++++ 12 files changed, 1420 insertions(+), 19 deletions(-) create mode 100644 v2_adminpanel/MIGRATION_2FA.md create mode 100644 v2_adminpanel/create_users_table.sql create mode 100644 v2_adminpanel/migrate_users.py create mode 100644 v2_adminpanel/templates/backup_codes.html create mode 100644 v2_adminpanel/templates/profile.html create mode 100644 v2_adminpanel/templates/setup_2fa.html create mode 100644 v2_adminpanel/templates/verify_2fa.html diff --git a/JOURNAL.md b/JOURNAL.md index ebac7ad..4356a2f 100644 --- a/JOURNAL.md +++ b/JOURNAL.md @@ -2030,4 +2030,132 @@ Ein Pool-System für Domains, IPv4-Adressen und Telefonnummern, wobei bei jeder - **License Server API** - Noch komplett zu implementieren - `/api/version` - Versionscheck - `/api/validate` - Lizenzvalidierung - - `/api/heartbeat` - Session-Management \ No newline at end of file + - `/api/heartbeat` - Session-Management + +### 2025-06-09: Resource Pool Internal Error behoben + +**Problem:** +- Internal Server Error beim Zugriff auf `/resources` +- NameError: name 'datetime' is not defined in Template + +**Ursache:** +- Fehlende `datetime` und `timedelta` Objekte im Template-Kontext +- Falsche Array-Indizes in resources.html für activity-Daten + +**Lösung:** +1. **app.py (Zeile 2797-2798):** + - `datetime=datetime` und `timedelta=timedelta` zu render_template hinzugefügt + +2. **resources.html (Zeile 484-490):** + - Array-Indizes korrigiert: + - activity[0] = action + - activity[1] = action_by + - activity[2] = action_at + - activity[3] = resource_type + - activity[4] = resource_value + - activity[5] = details + +**Geänderte Dateien:** +- `v2_adminpanel/app.py` +- `v2_adminpanel/templates/resources.html` + +**Status:** ✅ Behoben - Resource Pool funktioniert wieder einwandfrei + +### 2025-06-09: Passwort-Änderung und 2FA implementiert + +**Ziel:** +- Benutzer können ihr Passwort ändern +- Zwei-Faktor-Authentifizierung (2FA) mit TOTP +- Komplett kostenlose Lösung ohne externe Services + +**Implementierte Features:** + +1. **Datenbank-Erweiterung:** + - Neue `users` Tabelle mit Passwort-Hash und 2FA-Feldern + - Unterstützung für TOTP-Secrets und Backup-Codes + - Migration von Environment-Variablen zu Datenbank + +2. **Passwort-Management:** + - Sichere Passwort-Hashes mit bcrypt + - Passwort-Änderung mit Verifikation des alten Passworts + - Passwort-Stärke-Indikator im Frontend + +3. **2FA-Implementation:** + - TOTP-basierte 2FA (Google Authenticator, Authy kompatibel) + - QR-Code-Generierung für einfaches Setup + - 8 Backup-Codes für Notfallzugriff + - Backup-Codes als Textdatei downloadbar + +4. **Neue Routen:** + - `/profile` - Benutzerprofil mit Passwort und 2FA-Verwaltung + - `/verify-2fa` - 2FA-Verifizierung beim Login + - `/profile/setup-2fa` - 2FA-Einrichtung mit QR-Code + - `/profile/enable-2fa` - 2FA-Aktivierung + - `/profile/disable-2fa` - 2FA-Deaktivierung + - `/profile/change-password` - Passwort ändern + +5. **Sicherheits-Features:** + - Fallback zu Environment-Variablen für Rückwärtskompatibilität + - Session-Management für 2FA-Verifizierung + - Fehlgeschlagene 2FA-Versuche werden protokolliert + - Verwendete Backup-Codes werden entfernt + +**Verwendete Libraries (alle kostenlos):** +- `bcrypt` - Passwort-Hashing +- `pyotp` - TOTP-Generierung und Verifizierung +- `qrcode[pil]` - QR-Code-Generierung + +**Migration:** +- Script `migrate_users.py` erstellt für Migration existierender Benutzer +- Erhält bestehende Credentials aus Environment-Variablen +- Erstellt Datenbank-Einträge mit gehashten Passwörtern + +**Geänderte Dateien:** +- `v2_adminpanel/init.sql` - Users-Tabelle hinzugefügt +- `v2_adminpanel/requirements.txt` - Neue Dependencies +- `v2_adminpanel/app.py` - Auth-Funktionen und neue Routen +- `v2_adminpanel/migrate_users.py` - Migrations-Script (neu) +- `v2_adminpanel/templates/base.html` - Profil-Link hinzugefügt +- `v2_adminpanel/templates/profile.html` - Profil-Seite (neu) +- `v2_adminpanel/templates/verify_2fa.html` - 2FA-Verifizierung (neu) +- `v2_adminpanel/templates/setup_2fa.html` - 2FA-Setup (neu) +- `v2_adminpanel/templates/backup_codes.html` - Backup-Codes Anzeige (neu) + +**Status:** ✅ Vollständig implementiert + +### 2025-06-09: Internal Server Error behoben und UI-Design angepasst + +**Problem:** +- Internal Server Error nach Login wegen fehlender `users` Tabelle +- UI-Design der neuen 2FA-Seiten passte nicht zum Rest der Anwendung + +**Lösung:** + +1. **Datenbank-Fix:** + - Users-Tabelle wurde nicht automatisch erstellt + - Manuell mit SQL-Script nachgeholt + - Migration erfolgreich durchgeführt + - Beide Admin-User (rac00n, w@rh@mm3r) migriert + +2. **UI-Design Überarbeitung:** + - Profile-Seite im Dashboard-Stil mit Cards und Hover-Effekten + - 2FA-Setup mit nummerierten Schritten und modernem Card-Design + - Backup-Codes Seite mit Animation und verbessertem Layout + - Konsistente Farbgebung und Icons + - Verbesserte Benutzerführung mit visuellen Hinweisen + +**Design-Features:** +- Card-basiertes Layout mit Schatten-Effekten +- Hover-Animationen für bessere Interaktivität +- Farbcodierte Sicherheitsstatus-Anzeigen +- Passwort-Stärke-Indikator mit visueller Rückmeldung +- Responsive Design für alle Bildschirmgrößen +- Print-optimiertes Layout für Backup-Codes + +**Geänderte Dateien:** +- `v2_adminpanel/create_users_table.sql` - SQL für Users-Tabelle (temporär) +- `v2_adminpanel/templates/profile.html` - Komplett überarbeitet +- `v2_adminpanel/templates/setup_2fa.html` - Neues Step-by-Step Design +- `v2_adminpanel/templates/backup_codes.html` - Modernisiertes Layout + +**Status:** ✅ Abgeschlossen - Login funktioniert, UI im konsistenten Design \ No newline at end of file diff --git a/v2_adminpanel/MIGRATION_2FA.md b/v2_adminpanel/MIGRATION_2FA.md new file mode 100644 index 0000000..d60855b --- /dev/null +++ b/v2_adminpanel/MIGRATION_2FA.md @@ -0,0 +1,43 @@ +# Migration zu Passwort-Änderung und 2FA + +## Übersicht +Das Admin Panel unterstützt jetzt Passwort-Änderungen und Zwei-Faktor-Authentifizierung (2FA). Um diese Features zu nutzen, müssen bestehende Benutzer migriert werden. + +## Migration durchführen + +1. **Container neu bauen** (für neue Dependencies): + ```bash + docker-compose down + docker-compose build adminpanel + docker-compose up -d + ``` + +2. **Migration ausführen**: + ```bash + docker exec -it v2_adminpanel python migrate_users.py + ``` + + Dies erstellt Datenbankeinträge für die in der .env konfigurierten Admin-Benutzer. + +## Nach der Migration + +### Passwort ändern +1. Einloggen mit bisherigem Passwort +2. Klick auf "👤 Profil" in der Navigation +3. Neues Passwort eingeben (min. 8 Zeichen) + +### 2FA aktivieren +1. Im Profil auf "2FA einrichten" klicken +2. QR-Code mit Google Authenticator oder Authy scannen +3. 6-stelligen Code eingeben +4. Backup-Codes sicher aufbewahren! + +## Wichtige Hinweise +- Backup-Codes unbedingt speichern (Drucker, USB-Stick, etc.) +- Jeder Backup-Code kann nur einmal verwendet werden +- Bei Verlust des 2FA-Geräts können nur Backup-Codes helfen + +## Rückwärtskompatibilität +- Benutzer aus .env funktionieren weiterhin +- Diese haben aber keinen Zugriff auf Profil-Features +- Migration ist erforderlich für neue Features \ No newline at end of file diff --git a/v2_adminpanel/app.py b/v2_adminpanel/app.py index 29ab6e9..58ff0b0 100644 --- a/v2_adminpanel/app.py +++ b/v2_adminpanel/app.py @@ -22,6 +22,12 @@ import requests import secrets import string import re +import bcrypt +import pyotp +import qrcode +from io import BytesIO +import base64 +import json load_dotenv() @@ -111,6 +117,87 @@ def get_connection(): conn.set_client_encoding('UTF8') return conn +# User Authentication Helper Functions +def hash_password(password): + """Hash a password using bcrypt""" + return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') + +def verify_password(password, hashed): + """Verify a password against its hash""" + return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8')) + +def get_user_by_username(username): + """Get user from database by username""" + conn = get_connection() + cur = conn.cursor() + try: + cur.execute(""" + SELECT id, username, password_hash, email, totp_secret, totp_enabled, + backup_codes, last_password_change, failed_2fa_attempts + FROM users WHERE username = %s + """, (username,)) + user = cur.fetchone() + if user: + return { + 'id': user[0], + 'username': user[1], + 'password_hash': user[2], + 'email': user[3], + 'totp_secret': user[4], + 'totp_enabled': user[5], + 'backup_codes': user[6], + 'last_password_change': user[7], + 'failed_2fa_attempts': user[8] + } + return None + finally: + cur.close() + conn.close() + +def generate_totp_secret(): + """Generate a new TOTP secret""" + return pyotp.random_base32() + +def generate_qr_code(username, totp_secret): + """Generate QR code for TOTP setup""" + totp_uri = pyotp.totp.TOTP(totp_secret).provisioning_uri( + name=username, + issuer_name='V2 Admin Panel' + ) + + qr = qrcode.QRCode(version=1, box_size=10, border=5) + qr.add_data(totp_uri) + qr.make(fit=True) + + img = qr.make_image(fill_color="black", back_color="white") + buf = BytesIO() + img.save(buf, format='PNG') + buf.seek(0) + + return base64.b64encode(buf.getvalue()).decode() + +def verify_totp(totp_secret, token): + """Verify a TOTP token""" + totp = pyotp.TOTP(totp_secret) + return totp.verify(token, valid_window=1) + +def generate_backup_codes(count=8): + """Generate backup codes for 2FA recovery""" + codes = [] + for _ in range(count): + code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8)) + codes.append(code) + return codes + +def hash_backup_code(code): + """Hash a backup code for storage""" + return hashlib.sha256(code.encode()).hexdigest() + +def verify_backup_code(code, hashed_codes): + """Verify a backup code against stored hashes""" + code_hash = hashlib.sha256(code.encode()).hexdigest() + return code_hash in hashed_codes + # Audit-Log-Funktion def log_audit(action, entity_type, entity_id=None, old_values=None, new_values=None, additional_info=None): """Protokolliert Änderungen im Audit-Log""" @@ -647,17 +734,26 @@ def login(): attempts_left=max(0, MAX_LOGIN_ATTEMPTS - attempt_count), recaptcha_site_key=recaptcha_site_key) - # Check gegen beide Admin-Accounts aus .env - admin1_user = os.getenv("ADMIN1_USERNAME") - admin1_pass = os.getenv("ADMIN1_PASSWORD") - admin2_user = os.getenv("ADMIN2_USERNAME") - admin2_pass = os.getenv("ADMIN2_PASSWORD") - - # Login-Prüfung + # Check user in database first, fallback to env vars + user = get_user_by_username(username) login_success = False - if ((username == admin1_user and password == admin1_pass) or - (username == admin2_user and password == admin2_pass)): - login_success = True + needs_2fa = False + + if user: + # Database user authentication + if verify_password(password, user['password_hash']): + login_success = True + needs_2fa = user['totp_enabled'] + else: + # Fallback to environment variables for backward compatibility + admin1_user = os.getenv("ADMIN1_USERNAME") + admin1_pass = os.getenv("ADMIN1_PASSWORD") + admin2_user = os.getenv("ADMIN2_USERNAME") + admin2_pass = os.getenv("ADMIN2_PASSWORD") + + if ((username == admin1_user and password == admin1_pass) or + (username == admin2_user and password == admin2_pass)): + login_success = True # Timing-Attack Schutz - Mindestens 1 Sekunde warten elapsed = time.time() - start_time @@ -666,14 +762,23 @@ def login(): if login_success: # Erfolgreicher Login - session.permanent = True # Aktiviert das Timeout - session['logged_in'] = True - session['username'] = username - session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat() - reset_login_attempts(ip_address) - log_audit('LOGIN_SUCCESS', 'user', - additional_info=f"Erfolgreiche Anmeldung von IP: {ip_address}") - return redirect(url_for('dashboard')) + if needs_2fa: + # Store temporary session for 2FA verification + session['temp_username'] = username + session['temp_user_id'] = user['id'] + session['awaiting_2fa'] = True + return redirect(url_for('verify_2fa')) + else: + # Complete login without 2FA + session.permanent = True # Aktiviert das Timeout + session['logged_in'] = True + session['username'] = username + session['user_id'] = user['id'] if user else None + session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat() + reset_login_attempts(ip_address) + log_audit('LOGIN_SUCCESS', 'user', + additional_info=f"Erfolgreiche Anmeldung von IP: {ip_address}") + return redirect(url_for('dashboard')) else: # Fehlgeschlagener Login error_message = record_failed_attempt(ip_address, username) @@ -704,8 +809,224 @@ def logout(): log_audit('LOGOUT', 'user', additional_info=f"Abmeldung") session.pop('logged_in', None) session.pop('username', None) + session.pop('user_id', None) + session.pop('temp_username', None) + session.pop('temp_user_id', None) + session.pop('awaiting_2fa', None) return redirect(url_for('login')) +@app.route("/verify-2fa", methods=["GET", "POST"]) +def verify_2fa(): + if not session.get('awaiting_2fa'): + return redirect(url_for('login')) + + if request.method == "POST": + token = request.form.get('token', '').replace(' ', '') + username = session.get('temp_username') + user_id = session.get('temp_user_id') + + if not username or not user_id: + flash('Session expired. Please login again.', 'error') + return redirect(url_for('login')) + + user = get_user_by_username(username) + if not user: + flash('User not found.', 'error') + return redirect(url_for('login')) + + # Check if it's a backup code + if len(token) == 8 and token.isupper(): + # Try backup code + backup_codes = json.loads(user['backup_codes']) if user['backup_codes'] else [] + if verify_backup_code(token, backup_codes): + # Remove used backup code + code_hash = hash_backup_code(token) + backup_codes.remove(code_hash) + + conn = get_connection() + cur = conn.cursor() + cur.execute("UPDATE users SET backup_codes = %s WHERE id = %s", + (json.dumps(backup_codes), user_id)) + conn.commit() + cur.close() + conn.close() + + # Complete login + session.permanent = True + session['logged_in'] = True + session['username'] = username + session['user_id'] = user_id + session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat() + session.pop('temp_username', None) + session.pop('temp_user_id', None) + session.pop('awaiting_2fa', None) + + flash('Login successful using backup code. Please generate new backup codes.', 'warning') + log_audit('LOGIN_2FA_BACKUP', 'user', additional_info=f"2FA login with backup code") + return redirect(url_for('dashboard')) + else: + # Try TOTP token + if verify_totp(user['totp_secret'], token): + # Complete login + session.permanent = True + session['logged_in'] = True + session['username'] = username + session['user_id'] = user_id + session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat() + session.pop('temp_username', None) + session.pop('temp_user_id', None) + session.pop('awaiting_2fa', None) + + log_audit('LOGIN_2FA_SUCCESS', 'user', additional_info=f"2FA login successful") + return redirect(url_for('dashboard')) + + # Failed verification + conn = get_connection() + cur = conn.cursor() + cur.execute("UPDATE users SET failed_2fa_attempts = failed_2fa_attempts + 1, last_failed_2fa = %s WHERE id = %s", + (datetime.now(), user_id)) + conn.commit() + cur.close() + conn.close() + + flash('Invalid authentication code. Please try again.', 'error') + log_audit('LOGIN_2FA_FAILED', 'user', additional_info=f"Failed 2FA attempt") + + return render_template('verify_2fa.html') + +@app.route("/profile") +@login_required +def profile(): + user = get_user_by_username(session['username']) + if not user: + # For environment-based users, redirect with message + flash('Bitte führen Sie das Migrations-Script aus, um Passwort-Änderung und 2FA zu aktivieren.', 'info') + return redirect(url_for('dashboard')) + return render_template('profile.html', user=user) + +@app.route("/profile/change-password", methods=["POST"]) +@login_required +def change_password(): + current_password = request.form.get('current_password') + new_password = request.form.get('new_password') + confirm_password = request.form.get('confirm_password') + + user = get_user_by_username(session['username']) + + # Verify current password + if not verify_password(current_password, user['password_hash']): + flash('Current password is incorrect.', 'error') + return redirect(url_for('profile')) + + # Check new password + if new_password != confirm_password: + flash('New passwords do not match.', 'error') + return redirect(url_for('profile')) + + if len(new_password) < 8: + flash('Password must be at least 8 characters long.', 'error') + return redirect(url_for('profile')) + + # Update password + new_hash = hash_password(new_password) + conn = get_connection() + cur = conn.cursor() + cur.execute("UPDATE users SET password_hash = %s, last_password_change = %s WHERE id = %s", + (new_hash, datetime.now(), user['id'])) + conn.commit() + cur.close() + conn.close() + + log_audit('PASSWORD_CHANGE', 'user', entity_id=user['id'], + additional_info="Password changed successfully") + flash('Password changed successfully.', 'success') + return redirect(url_for('profile')) + +@app.route("/profile/setup-2fa") +@login_required +def setup_2fa(): + user = get_user_by_username(session['username']) + + if user['totp_enabled']: + flash('2FA is already enabled for your account.', 'info') + return redirect(url_for('profile')) + + # Generate new TOTP secret + totp_secret = generate_totp_secret() + session['temp_totp_secret'] = totp_secret + + # Generate QR code + qr_code = generate_qr_code(user['username'], totp_secret) + + return render_template('setup_2fa.html', + totp_secret=totp_secret, + qr_code=qr_code) + +@app.route("/profile/enable-2fa", methods=["POST"]) +@login_required +def enable_2fa(): + token = request.form.get('token', '').replace(' ', '') + totp_secret = session.get('temp_totp_secret') + + if not totp_secret: + flash('2FA setup session expired. Please try again.', 'error') + return redirect(url_for('setup_2fa')) + + # Verify the token + if not verify_totp(totp_secret, token): + flash('Invalid authentication code. Please try again.', 'error') + return redirect(url_for('setup_2fa')) + + # Generate backup codes + backup_codes = generate_backup_codes() + hashed_codes = [hash_backup_code(code) for code in backup_codes] + + # Enable 2FA + conn = get_connection() + cur = conn.cursor() + cur.execute(""" + UPDATE users + SET totp_secret = %s, totp_enabled = TRUE, backup_codes = %s + WHERE username = %s + """, (totp_secret, json.dumps(hashed_codes), session['username'])) + conn.commit() + cur.close() + conn.close() + + session.pop('temp_totp_secret', None) + + log_audit('2FA_ENABLED', 'user', additional_info="2FA enabled successfully") + + # Show backup codes + return render_template('backup_codes.html', backup_codes=backup_codes) + +@app.route("/profile/disable-2fa", methods=["POST"]) +@login_required +def disable_2fa(): + password = request.form.get('password') + user = get_user_by_username(session['username']) + + # Verify password + if not verify_password(password, user['password_hash']): + flash('Incorrect password.', 'error') + return redirect(url_for('profile')) + + # Disable 2FA + conn = get_connection() + cur = conn.cursor() + cur.execute(""" + UPDATE users + SET totp_enabled = FALSE, totp_secret = NULL, backup_codes = NULL + WHERE username = %s + """, (session['username'],)) + conn.commit() + cur.close() + conn.close() + + log_audit('2FA_DISABLED', 'user', additional_info="2FA disabled") + flash('2FA has been disabled for your account.', 'success') + return redirect(url_for('profile')) + @app.route("/heartbeat", methods=['POST']) @login_required def heartbeat(): diff --git a/v2_adminpanel/create_users_table.sql b/v2_adminpanel/create_users_table.sql new file mode 100644 index 0000000..202f4e8 --- /dev/null +++ b/v2_adminpanel/create_users_table.sql @@ -0,0 +1,20 @@ +-- Create users table if it doesn't exist +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + email VARCHAR(100), + totp_secret VARCHAR(32), + totp_enabled BOOLEAN DEFAULT FALSE, + backup_codes TEXT, -- JSON array of hashed backup codes + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + last_password_change TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + password_reset_token VARCHAR(64), + password_reset_expires TIMESTAMP WITH TIME ZONE, + failed_2fa_attempts INTEGER DEFAULT 0, + last_failed_2fa TIMESTAMP WITH TIME ZONE +); + +-- Index for faster login lookups +CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); +CREATE INDEX IF NOT EXISTS idx_users_reset_token ON users(password_reset_token) WHERE password_reset_token IS NOT NULL; \ No newline at end of file diff --git a/v2_adminpanel/init.sql b/v2_adminpanel/init.sql index 6890685..9fe7f20 100644 --- a/v2_adminpanel/init.sql +++ b/v2_adminpanel/init.sql @@ -179,3 +179,24 @@ CREATE INDEX IF NOT EXISTS idx_resource_history_date ON resource_history(action_ CREATE INDEX IF NOT EXISTS idx_resource_history_resource ON resource_history(resource_id); CREATE INDEX IF NOT EXISTS idx_resource_metrics_date ON resource_metrics(metric_date DESC); CREATE INDEX IF NOT EXISTS idx_license_resources_active ON license_resources(license_id) WHERE is_active = TRUE; + +-- Users table for authentication with password and 2FA support +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + email VARCHAR(100), + totp_secret VARCHAR(32), + totp_enabled BOOLEAN DEFAULT FALSE, + backup_codes TEXT, -- JSON array of hashed backup codes + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + last_password_change TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + password_reset_token VARCHAR(64), + password_reset_expires TIMESTAMP WITH TIME ZONE, + failed_2fa_attempts INTEGER DEFAULT 0, + last_failed_2fa TIMESTAMP WITH TIME ZONE +); + +-- Index for faster login lookups +CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); +CREATE INDEX IF NOT EXISTS idx_users_reset_token ON users(password_reset_token) WHERE password_reset_token IS NOT NULL; diff --git a/v2_adminpanel/migrate_users.py b/v2_adminpanel/migrate_users.py new file mode 100644 index 0000000..106833d --- /dev/null +++ b/v2_adminpanel/migrate_users.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +""" +Migration script to create initial users in the database from environment variables +Run this once after creating the users table +""" + +import os +import psycopg2 +import bcrypt +from dotenv import load_dotenv +from datetime import datetime + +load_dotenv() + +def get_connection(): + return psycopg2.connect( + host=os.getenv("POSTGRES_HOST", "postgres"), + port=os.getenv("POSTGRES_PORT", "5432"), + dbname=os.getenv("POSTGRES_DB"), + user=os.getenv("POSTGRES_USER"), + password=os.getenv("POSTGRES_PASSWORD"), + options='-c client_encoding=UTF8' + ) + +def hash_password(password): + return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') + +def migrate_users(): + conn = get_connection() + cur = conn.cursor() + + try: + # Check if users already exist + cur.execute("SELECT COUNT(*) FROM users") + user_count = cur.fetchone()[0] + + if user_count > 0: + print(f"Users table already contains {user_count} users. Skipping migration.") + return + + # Get admin users from environment + admin1_user = os.getenv("ADMIN1_USERNAME") + admin1_pass = os.getenv("ADMIN1_PASSWORD") + admin2_user = os.getenv("ADMIN2_USERNAME") + admin2_pass = os.getenv("ADMIN2_PASSWORD") + + if not all([admin1_user, admin1_pass, admin2_user, admin2_pass]): + print("ERROR: Admin credentials not found in environment variables!") + return + + # Insert admin users + users = [ + (admin1_user, hash_password(admin1_pass), f"{admin1_user}@v2-admin.local"), + (admin2_user, hash_password(admin2_pass), f"{admin2_user}@v2-admin.local") + ] + + for username, password_hash, email in users: + cur.execute(""" + INSERT INTO users (username, password_hash, email, totp_enabled, created_at) + VALUES (%s, %s, %s, %s, %s) + """, (username, password_hash, email, False, datetime.now())) + print(f"Created user: {username}") + + conn.commit() + print("\nMigration completed successfully!") + print("Users can now log in with their existing credentials.") + print("They can enable 2FA from their profile page.") + + except Exception as e: + conn.rollback() + print(f"ERROR during migration: {e}") + finally: + cur.close() + conn.close() + +if __name__ == "__main__": + print("Starting user migration...") + migrate_users() \ No newline at end of file diff --git a/v2_adminpanel/requirements.txt b/v2_adminpanel/requirements.txt index 1dc87d9..f1fcc95 100644 --- a/v2_adminpanel/requirements.txt +++ b/v2_adminpanel/requirements.txt @@ -9,3 +9,6 @@ cryptography apscheduler requests python-dateutil +bcrypt +pyotp +qrcode[pil] diff --git a/v2_adminpanel/templates/backup_codes.html b/v2_adminpanel/templates/backup_codes.html new file mode 100644 index 0000000..f62c4db --- /dev/null +++ b/v2_adminpanel/templates/backup_codes.html @@ -0,0 +1,228 @@ +{% extends "base.html" %} + +{% block title %}Backup-Codes{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+
+
+ +
+
+

2FA erfolgreich aktiviert!

+

Ihre Zwei-Faktor-Authentifizierung ist jetzt aktiv.

+
+ + +
+
+

+ ⚠️ + Wichtig: Ihre Backup-Codes +

+
+
+
+ Was sind Backup-Codes?
+ Diese Codes ermöglichen Ihnen den Zugang zu Ihrem Account, falls Sie keinen Zugriff auf Ihre Authenticator-App haben. + Jeder Code kann nur einmal verwendet werden. +
+ + +
+
Ihre 8 Backup-Codes:
+
+ {% for code in backup_codes %} +
+
{{ code }}
+
+ {% endfor %} +
+
+ + +
+ + + +
+ +
+ + +
+
+
+
❌ Nicht empfohlen:
+
    +
  • Im selben Passwort-Manager wie Ihr Passwort
  • +
  • Als Foto auf Ihrem Handy
  • +
  • In einer unverschlüsselten Datei
  • +
  • Per E-Mail an sich selbst
  • +
+
+
+
+
+
✅ Empfohlen:
+
    +
  • Ausgedruckt in einem Safe
  • +
  • In einem separaten Passwort-Manager
  • +
  • Verschlüsselt auf einem USB-Stick
  • +
  • An einem sicheren Ort zu Hause
  • +
+
+
+
+ + +
+
+ + +
+ +
+
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/v2_adminpanel/templates/base.html b/v2_adminpanel/templates/base.html index 26f6d38..0ab8810 100644 --- a/v2_adminpanel/templates/base.html +++ b/v2_adminpanel/templates/base.html @@ -235,6 +235,7 @@ ⏱️ 5:00 Angemeldet als: {{ username }} + 👤 Profil Abmelden diff --git a/v2_adminpanel/templates/profile.html b/v2_adminpanel/templates/profile.html new file mode 100644 index 0000000..bb95eff --- /dev/null +++ b/v2_adminpanel/templates/profile.html @@ -0,0 +1,217 @@ +{% extends "base.html" %} + +{% block title %}Benutzerprofil{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+
+

👤 Benutzerprofil

+ ← Zurück zum Dashboard +
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + + +
+
+
+
+
👤
+
{{ user.username }}
+

{{ user.email or 'Keine E-Mail angegeben' }}

+ Mitglied seit: {{ user.created_at.strftime('%d.%m.%Y') if user.created_at else 'Unbekannt' }} +
+
+
+
+
+
+
🔐
+
Sicherheitsstatus
+ {% if user.totp_enabled %} + 2FA Aktiv + {% else %} + 2FA Inaktiv + {% endif %} +

+ Letztes Passwort-Update:
{{ user.last_password_change.strftime('%d.%m.%Y') if user.last_password_change else 'Noch nie' }}
+

+
+
+
+
+ + +
+
+
+ 🔑 + Passwort ändern +
+
+
+
+ + +
+
+ + +
+
Mindestens 8 Zeichen
+
+
+ + +
Passwörter stimmen nicht überein
+
+ +
+
+
+ + +
+
+
+ 🔐 + Zwei-Faktor-Authentifizierung (2FA) +
+
+ {% if user.totp_enabled %} +
+
+
Status: Aktiv
+

Ihr Account ist durch 2FA geschützt

+
+
+
+
+
+ + +
+ +
+ {% else %} +
+
+
Status: Inaktiv
+

Aktivieren Sie 2FA für zusätzliche Sicherheit

+
+
⚠️
+
+

+ Mit 2FA wird bei jeder Anmeldung zusätzlich ein Code aus Ihrer Authenticator-App benötigt. + Dies schützt Ihren Account auch bei kompromittiertem Passwort. +

+ ✨ 2FA einrichten + {% endif %} +
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/v2_adminpanel/templates/setup_2fa.html b/v2_adminpanel/templates/setup_2fa.html new file mode 100644 index 0000000..f30af6b --- /dev/null +++ b/v2_adminpanel/templates/setup_2fa.html @@ -0,0 +1,210 @@ +{% extends "base.html" %} + +{% block title %}2FA Einrichten{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+
+

🔐 2FA einrichten

+ ← Zurück zum Profil +
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + + +
+
+
+ 1 + Authenticator-App installieren +
+

Wählen Sie eine der folgenden Apps für Ihr Smartphone:

+
+
+
+ 📱 +
+ Google Authenticator
+ Android / iOS +
+
+
+
+
+ 🔷 +
+ Microsoft Authenticator
+ Android / iOS +
+
+
+
+
+ 🔴 +
+ Authy
+ Android / iOS / Desktop +
+
+
+
+
+
+ + +
+
+
+ 2 + QR-Code scannen oder Code eingeben +
+
+
+

Option A: QR-Code scannen

+
+ 2FA QR Code +
+

+ Öffnen Sie Ihre Authenticator-App und scannen Sie diesen Code +

+
+
+

Option B: Code manuell eingeben

+
+ +
+ V2 Admin Panel +
+
+
+ +
{{ totp_secret }}
+ +
+
+ + ⚠️ Wichtiger Hinweis:
+ Speichern Sie diesen Code sicher. Er ist Ihre einzige Möglichkeit, + 2FA auf einem neuen Gerät einzurichten. +
+
+
+
+
+
+ + +
+
+
+ 3 + Code verifizieren +
+

Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein:

+ +
+
+
+ +
Der Code ändert sich alle 30 Sekunden
+
+
+ +
+
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/v2_adminpanel/templates/verify_2fa.html b/v2_adminpanel/templates/verify_2fa.html new file mode 100644 index 0000000..6ab8f70 --- /dev/null +++ b/v2_adminpanel/templates/verify_2fa.html @@ -0,0 +1,131 @@ + + + + + + 2FA Verifizierung - Admin Panel + + + + +
+ +

Zwei-Faktor-Authentifizierung

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + +
+
+ + +
+ Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein +
+
+ +
+ +
+ +
+
+ Backup-Code verwenden +
+

+ Falls Sie keinen Zugriff auf Ihre Authenticator-App haben, + können Sie einen 8-stelligen Backup-Code eingeben. +

+ +
+
+
+ +
+ + +
+
+ + + + + \ No newline at end of file