Dieser Commit ist enthalten in:
Claude Project Manager
2025-07-05 17:51:16 +02:00
Commit 0d7d888502
1594 geänderte Dateien mit 122839 neuen und 0 gelöschten Zeilen

Datei anzeigen

@ -0,0 +1 @@
# Auth module initialization

Datei anzeigen

@ -0,0 +1,44 @@
from functools import wraps
from flask import session, redirect, url_for, flash, request
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import logging
from utils.audit import log_audit
logger = logging.getLogger(__name__)
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'logged_in' not in session:
return redirect(url_for('login'))
# Check if session has expired
if 'last_activity' in session:
last_activity = datetime.fromisoformat(session['last_activity'])
time_since_activity = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None) - last_activity
# Debug logging
logger.info(f"Session check for {session.get('username', 'unknown')}: "
f"Last activity: {last_activity}, "
f"Time since: {time_since_activity.total_seconds()} seconds")
if time_since_activity > timedelta(minutes=5):
# Session expired - Logout
username = session.get('username', 'unbekannt')
logger.info(f"Session timeout for user {username} - auto logout")
# Audit log for automatic logout (before session.clear()!)
try:
log_audit('AUTO_LOGOUT', 'session',
additional_info={'reason': 'Session timeout (5 minutes)', 'username': username})
except:
pass
session.clear()
flash('Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.', 'warning')
return redirect(url_for('login'))
# Activity is NOT automatically updated
# Only on explicit user actions (done by heartbeat)
return f(*args, **kwargs)
return decorated_function

Datei anzeigen

@ -0,0 +1,11 @@
import bcrypt
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'))

Datei anzeigen

@ -0,0 +1,124 @@
import random
import logging
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from flask import request
from db import execute_query, get_db_connection, get_db_cursor
from config import FAIL_MESSAGES, MAX_LOGIN_ATTEMPTS, BLOCK_DURATION_HOURS, EMAIL_ENABLED
from utils.audit import log_audit
from utils.network import get_client_ip
logger = logging.getLogger(__name__)
def check_ip_blocked(ip_address):
"""Check if an IP address is blocked"""
result = execute_query(
"""
SELECT blocked_until FROM login_attempts
WHERE ip_address = %s AND blocked_until IS NOT NULL
""",
(ip_address,),
fetch_one=True
)
if result and result[0]:
if result[0] > datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None):
return True, result[0]
return False, None
def record_failed_attempt(ip_address, username):
"""Record a failed login attempt"""
# Random error message
error_message = random.choice(FAIL_MESSAGES)
with get_db_connection() as conn:
with get_db_cursor(conn) as cur:
try:
# Check if IP already exists
cur.execute("""
SELECT attempt_count FROM login_attempts
WHERE ip_address = %s
""", (ip_address,))
result = cur.fetchone()
if result:
# Update existing entry
new_count = result[0] + 1
blocked_until = None
if new_count >= MAX_LOGIN_ATTEMPTS:
blocked_until = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None) + timedelta(hours=BLOCK_DURATION_HOURS)
# Email notification (if enabled)
if EMAIL_ENABLED:
send_security_alert_email(ip_address, username, new_count)
cur.execute("""
UPDATE login_attempts
SET attempt_count = %s,
last_attempt = CURRENT_TIMESTAMP,
blocked_until = %s,
last_username_tried = %s,
last_error_message = %s
WHERE ip_address = %s
""", (new_count, blocked_until, username, error_message, ip_address))
else:
# Create new entry
cur.execute("""
INSERT INTO login_attempts
(ip_address, attempt_count, last_username_tried, last_error_message)
VALUES (%s, 1, %s, %s)
""", (ip_address, username, error_message))
conn.commit()
# Audit log
log_audit('LOGIN_FAILED', 'user',
additional_info=f"IP: {ip_address}, User: {username}, Message: {error_message}")
except Exception as e:
logger.error(f"Rate limiting error: {e}")
conn.rollback()
return error_message
def reset_login_attempts(ip_address):
"""Reset login attempts for an IP"""
execute_query(
"DELETE FROM login_attempts WHERE ip_address = %s",
(ip_address,)
)
def get_login_attempts(ip_address):
"""Get the number of login attempts for an IP"""
result = execute_query(
"SELECT attempt_count FROM login_attempts WHERE ip_address = %s",
(ip_address,),
fetch_one=True
)
return result[0] if result else 0
def send_security_alert_email(ip_address, username, attempt_count):
"""Send a security alert email"""
subject = f"⚠️ SICHERHEITSWARNUNG: {attempt_count} fehlgeschlagene Login-Versuche"
body = f"""
WARNUNG: Mehrere fehlgeschlagene Login-Versuche erkannt!
IP-Adresse: {ip_address}
Versuchter Benutzername: {username}
Anzahl Versuche: {attempt_count}
Zeit: {datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y-%m-%d %H:%M:%S')}
Die IP-Adresse wurde für 24 Stunden gesperrt.
Dies ist eine automatische Nachricht vom v2-Docker Admin Panel.
"""
# TODO: Email sending implementation when SMTP is configured
logger.warning(f"Sicherheitswarnung: {attempt_count} fehlgeschlagene Versuche von IP {ip_address}")
print(f"E-Mail würde gesendet: {subject}")

Datei anzeigen

@ -0,0 +1,57 @@
import pyotp
import qrcode
import random
import string
import hashlib
from io import BytesIO
import base64
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