Files
Projektarbeit-MYP/backend/blueprints/user_management.py
Till Tomczak 442d6d82dd 🔧 Update: Enhance user settings API to support PATCH method
**Änderungen:**
-  user_management.py: Erweitert die API zur Aktualisierung von Benutzereinstellungen, um sowohl POST- als auch PATCH-Anfragen zu unterstützen.

**Ergebnis:**
- Verbesserte Flexibilität bei der Aktualisierung von Benutzereinstellungen durch Unterstützung von PATCH-Anfragen.

🤖 Generated with [Claude Code](https://claude.ai/code)
2025-06-16 00:34:56 +02:00

627 lines
27 KiB
Python

"""
Vereinheitlichtes User-Management-Blueprint für das MYP System
Konsolidierte Implementierung aller benutzerbezogenen Funktionen:
- Benutzer-Selbstverwaltung (ursprünglich user.py)
- Administrative Benutzerverwaltung (ursprünglich users.py)
- Vereinheitlichte API-Schnittstellen
Funktionsbereiche:
- /user/* - Selbstverwaltung für eingeloggte Benutzer
- /admin/users/* - Administrative Benutzerverwaltung
- /api/users/* - Unified API Layer
Optimierungen:
- Einheitliche Database-Session-Verwaltung
- Konsistente Error-Handling und Logging
- Vollständige API-Kompatibilität zu beiden ursprünglichen Blueprints
Autor: MYP Team - Konsolidiert für IHK-Projektarbeit
Datum: 2025-06-09
"""
import json
from datetime import datetime
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash, make_response, abort
from flask_login import login_required, current_user
from werkzeug.security import check_password_hash
from sqlalchemy.exc import SQLAlchemyError
from functools import wraps
from models import User, UserPermission, get_cached_session
from utils.logging_config import get_logger
# ===== BLUEPRINT-KONFIGURATION =====
# Hauptblueprint für User-Management
users_blueprint = Blueprint('users', __name__)
# Logger für verschiedene Funktionsbereiche
user_logger = get_logger("user")
users_logger = get_logger("users")
# ===== DECORATOR-FUNKTIONEN =====
def users_admin_required(f):
"""
Decorator für Admin-Berechtigung bei Benutzerverwaltung.
Erweitert den Standard-Admin-Check um spezifische User-Management-Rechte.
"""
@wraps(f)
@login_required
def decorated_function(*args, **kwargs):
# Grundlegende Admin-Prüfung
if not current_user.is_authenticated:
users_logger.warning("Unauthenticated access attempt to user management")
abort(401)
# Admin-Status prüfen (doppelte Methode für Robustheit)
is_admin = False
if hasattr(current_user, 'is_admin') and current_user.is_admin:
is_admin = True
elif hasattr(current_user, 'role') and current_user.role == 'admin':
is_admin = True
if not is_admin:
users_logger.warning(f"Non-admin user {current_user.id} attempted to access user management")
abort(403)
users_logger.info(f"Admin access granted to {current_user.username} for function {f.__name__}")
return f(*args, **kwargs)
return decorated_function
# ===== BENUTZER-SELBSTVERWALTUNG (ursprünglich user.py) =====
@users_blueprint.route('/user/profile', methods=['GET'])
@login_required
def user_profile():
"""Benutzerprofil-Seite anzeigen"""
try:
user_logger.info(f"User {current_user.username} accessed profile page")
return render_template('profile.html', user=current_user)
except Exception as e:
user_logger.error(f"Error loading profile page: {str(e)}")
flash("Fehler beim Laden des Profils", "error")
return redirect(url_for('dashboard'))
@users_blueprint.route('/user/settings', methods=['GET'])
@login_required
def user_settings():
"""Benutzereinstellungen-Seite anzeigen"""
try:
user_logger.info(f"User {current_user.username} accessed settings page")
return render_template('settings.html', user=current_user)
except Exception as e:
user_logger.error(f"Error loading settings page: {str(e)}")
flash("Fehler beim Laden der Einstellungen", "error")
return redirect(url_for('dashboard'))
@users_blueprint.route('/user/update-profile', methods=['POST'])
@login_required
def update_profile_form():
"""Profil via Formular aktualisieren"""
try:
with get_cached_session() as session:
user = session.query(User).filter(User.id == current_user.id).first()
if not user:
flash("Benutzer nicht gefunden", "error")
return redirect(url_for('users.user_profile'))
# Formular-Daten extrahieren
user.name = request.form.get('name', user.name)
user.email = request.form.get('email', user.email)
user.department = request.form.get('department', user.department)
user.position = request.form.get('position', user.position)
user.phone = request.form.get('phone', user.phone)
user.bio = request.form.get('bio', user.bio)
user.updated_at = datetime.now()
session.commit()
user_logger.info(f"User {user.username} updated profile via form")
flash("Profil erfolgreich aktualisiert", "success")
return redirect(url_for('users.user_profile'))
except Exception as e:
user_logger.error(f"Error updating profile via form: {str(e)}")
flash("Fehler beim Aktualisieren des Profils", "error")
return redirect(url_for('users.user_profile'))
@users_blueprint.route('/user/profile', methods=['PUT'])
@login_required
def update_profile_api():
"""Profil via API aktualisieren"""
try:
data = request.get_json()
with get_cached_session() as session:
user = session.query(User).filter(User.id == current_user.id).first()
if not user:
return jsonify({"error": "Benutzer nicht gefunden"}), 404
# API-Daten verarbeiten
updatable_fields = ['name', 'email', 'department', 'position', 'phone', 'bio']
for field in updatable_fields:
if field in data:
setattr(user, field, data[field])
user.updated_at = datetime.now()
session.commit()
user_logger.info(f"User {user.username} updated profile via API")
return jsonify({
"success": True,
"message": "Profil erfolgreich aktualisiert"
})
except Exception as e:
user_logger.error(f"Error updating profile via API: {str(e)}")
return jsonify({"error": "Fehler beim Aktualisieren des Profils"}), 500
@users_blueprint.route('/api/user/settings', methods=['GET', 'POST', 'PATCH'])
@users_blueprint.route('/api/user/setting', methods=['GET', 'POST', 'PATCH'])
@login_required
def user_settings_api():
"""Benutzereinstellungen via API abrufen oder aktualisieren"""
try:
with get_cached_session() as session:
user = session.query(User).filter(User.id == current_user.id).first()
if not user:
return jsonify({"error": "Benutzer nicht gefunden"}), 404
if request.method == 'GET':
# Einstellungen abrufen
settings = {
'theme_preference': getattr(user, 'theme_preference', 'auto'),
'language_preference': getattr(user, 'language_preference', 'de'),
'email_notifications': getattr(user, 'email_notifications', True),
'browser_notifications': getattr(user, 'browser_notifications', True),
'dashboard_layout': getattr(user, 'dashboard_layout', 'default'),
'compact_mode': getattr(user, 'compact_mode', False),
'show_completed_jobs': getattr(user, 'show_completed_jobs', True),
'auto_refresh_interval': getattr(user, 'auto_refresh_interval', 30),
'privacy': {
'auto_logout': getattr(user, 'auto_logout_timeout', 0)
}
}
user_logger.info(f"User {user.username} retrieved settings via API")
return jsonify({
"success": True,
"settings": settings
})
elif request.method in ['POST', 'PATCH']:
# Einstellungen aktualisieren
data = request.get_json()
# Einstellungen aktualisieren
settings_fields = [
'theme_preference', 'language_preference', 'email_notifications',
'browser_notifications', 'dashboard_layout', 'compact_mode',
'show_completed_jobs', 'auto_refresh_interval'
]
for field in settings_fields:
if field in data:
setattr(user, field, data[field])
# Privacy-Einstellungen
if 'privacy' in data and isinstance(data['privacy'], dict):
if 'auto_logout' in data['privacy']:
user.auto_logout_timeout = data['privacy']['auto_logout']
user.updated_at = datetime.now()
session.commit()
user_logger.info(f"User {user.username} updated settings via API")
return jsonify({
"success": True,
"message": "Einstellungen erfolgreich aktualisiert"
})
except Exception as e:
user_logger.error(f"Error handling user settings API: {str(e)}")
return jsonify({"error": "Fehler beim Verarbeiten der Einstellungen"}), 500
@users_blueprint.route('/user/api/update-settings', methods=['POST'])
@login_required
def update_settings_api():
"""Benutzereinstellungen via API aktualisieren (Legacy-Kompatibilität)"""
try:
data = request.get_json()
with get_cached_session() as session:
user = session.query(User).filter(User.id == current_user.id).first()
if not user:
return jsonify({"error": "Benutzer nicht gefunden"}), 404
# Einstellungen aktualisieren
settings_fields = [
'theme_preference', 'language_preference', 'email_notifications',
'browser_notifications', 'dashboard_layout', 'compact_mode',
'show_completed_jobs', 'auto_refresh_interval'
]
for field in settings_fields:
if field in data:
setattr(user, field, data[field])
user.updated_at = datetime.now()
session.commit()
user_logger.info(f"User {user.username} updated settings via API")
return jsonify({
"success": True,
"message": "Einstellungen erfolgreich aktualisiert"
})
except Exception as e:
user_logger.error(f"Error updating settings via API: {str(e)}")
return jsonify({"error": "Fehler beim Aktualisieren der Einstellungen"}), 500
@users_blueprint.route('/user/update-settings', methods=['POST'])
@login_required
def update_settings_form():
"""Benutzereinstellungen via Formular aktualisieren"""
try:
with get_cached_session() as session:
user = session.query(User).filter(User.id == current_user.id).first()
if not user:
flash("Benutzer nicht gefunden", "error")
return redirect(url_for('users.user_settings'))
# Formular-Einstellungen verarbeiten
user.theme_preference = request.form.get('theme_preference', user.theme_preference)
user.language_preference = request.form.get('language_preference', user.language_preference)
user.email_notifications = 'email_notifications' in request.form
user.browser_notifications = 'browser_notifications' in request.form
user.dashboard_layout = request.form.get('dashboard_layout', user.dashboard_layout)
user.compact_mode = 'compact_mode' in request.form
user.show_completed_jobs = 'show_completed_jobs' in request.form
auto_refresh = request.form.get('auto_refresh_interval')
if auto_refresh:
user.auto_refresh_interval = int(auto_refresh)
user.updated_at = datetime.now()
session.commit()
user_logger.info(f"User {user.username} updated settings via form")
flash("Einstellungen erfolgreich aktualisiert", "success")
return redirect(url_for('users.user_settings'))
except Exception as e:
user_logger.error(f"Error updating settings via form: {str(e)}")
flash("Fehler beim Aktualisieren der Einstellungen", "error")
return redirect(url_for('users.user_settings'))
@users_blueprint.route('/user/change-password', methods=['POST'])
@login_required
def change_password():
"""Passwort ändern (unterstützt Form und JSON)"""
try:
# Daten aus Request extrahieren (Form oder JSON)
if request.is_json:
data = request.get_json()
current_password = data.get('current_password')
new_password = data.get('new_password')
confirm_password = data.get('confirm_password')
else:
current_password = request.form.get('current_password')
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')
# Validierung
if not all([current_password, new_password, confirm_password]):
error_msg = "Alle Passwort-Felder sind erforderlich"
if request.is_json:
return jsonify({"error": error_msg}), 400
flash(error_msg, "error")
return redirect(url_for('users.user_settings'))
if new_password != confirm_password:
error_msg = "Neue Passwörter stimmen nicht überein"
if request.is_json:
return jsonify({"error": error_msg}), 400
flash(error_msg, "error")
return redirect(url_for('users.user_settings'))
if len(new_password) < 8:
error_msg = "Neues Passwort muss mindestens 8 Zeichen lang sein"
if request.is_json:
return jsonify({"error": error_msg}), 400
flash(error_msg, "error")
return redirect(url_for('users.user_settings'))
# Aktuelles Passwort prüfen
with get_cached_session() as session:
user = session.query(User).filter(User.id == current_user.id).first()
if not user or not check_password_hash(user.password_hash, current_password):
error_msg = "Aktuelles Passwort ist falsch"
if request.is_json:
return jsonify({"error": error_msg}), 400
flash(error_msg, "error")
return redirect(url_for('users.user_settings'))
# Neues Passwort setzen
user.set_password(new_password)
user.updated_at = datetime.now()
session.commit()
user_logger.info(f"User {user.username} changed password successfully")
success_msg = "Passwort erfolgreich geändert"
if request.is_json:
return jsonify({"success": True, "message": success_msg})
flash(success_msg, "success")
return redirect(url_for('users.user_settings'))
except Exception as e:
user_logger.error(f"Error changing password: {str(e)}")
error_msg = "Fehler beim Ändern des Passworts"
if request.is_json:
return jsonify({"error": error_msg}), 500
flash(error_msg, "error")
return redirect(url_for('users.user_settings'))
@users_blueprint.route('/user/export', methods=['GET'])
@login_required
def export_user_data():
"""DSGVO-konformer Datenexport für Benutzer"""
try:
with get_cached_session() as session:
user = session.query(User).filter(User.id == current_user.id).first()
if not user:
return jsonify({"error": "Benutzer nicht gefunden"}), 404
# Umfassende Benutzerdaten sammeln
user_data = {
"export_info": {
"generated_at": datetime.now().isoformat(),
"user_id": user.id,
"export_type": "DSGVO_complete_data_export"
},
"personal_data": {
"username": user.username,
"email": user.email,
"name": user.name,
"department": user.department,
"position": user.position,
"phone": user.phone,
"bio": user.bio,
"role": user.role,
"active": user.active,
"created_at": user.created_at.isoformat() if user.created_at else None,
"updated_at": user.updated_at.isoformat() if user.updated_at else None,
"last_login": user.last_login.isoformat() if user.last_login else None
},
"preferences": {
"theme_preference": user.theme_preference,
"language_preference": user.language_preference,
"email_notifications": user.email_notifications,
"browser_notifications": user.browser_notifications,
"dashboard_layout": user.dashboard_layout,
"compact_mode": user.compact_mode,
"show_completed_jobs": user.show_completed_jobs,
"auto_refresh_interval": user.auto_refresh_interval
},
"system_info": {
"total_jobs_created": len(user.jobs) if hasattr(user, 'jobs') else 0,
"account_status": "active" if user.active else "inactive"
}
}
# Jobs-Daten hinzufügen (falls verfügbar)
if hasattr(user, 'jobs'):
user_data["job_history"] = []
for job in user.jobs:
job_info = {
"id": job.id,
"title": job.title,
"status": job.status,
"created_at": job.created_at.isoformat() if job.created_at else None,
"updated_at": job.updated_at.isoformat() if job.updated_at else None
}
user_data["job_history"].append(job_info)
# JSON-Response mit Download-Headers erstellen
response = make_response(jsonify(user_data))
response.headers['Content-Type'] = 'application/json; charset=utf-8'
response.headers['Content-Disposition'] = f'attachment; filename=user_data_export_{user.username}_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json'
user_logger.info(f"User {user.username} exported personal data (DSGVO)")
return response
except Exception as e:
user_logger.error(f"Error exporting user data: {str(e)}")
return jsonify({"error": "Fehler beim Exportieren der Benutzerdaten"}), 500
# ===== ADMINISTRATIVE BENUTZERVERWALTUNG (ursprünglich users.py) =====
@users_blueprint.route('/admin/users/<int:user_id>/permissions', methods=['GET'])
@users_admin_required
def user_permissions_page(user_id):
"""Admin-Seite für Benutzerberechtigungen"""
try:
with get_cached_session() as session:
user = session.query(User).filter(User.id == user_id).first()
if not user:
flash("Benutzer nicht gefunden", "error")
return redirect(url_for('admin.users_overview'))
# UserPermissions laden oder erstellen
permissions = session.query(UserPermission).filter(UserPermission.user_id == user_id).first()
if not permissions:
permissions = UserPermission(user_id=user_id)
session.add(permissions)
session.commit()
users_logger.info(f"Admin {current_user.username} accessed permissions for user {user.username}")
return render_template('admin/user_permissions.html', user=user, permissions=permissions)
except Exception as e:
users_logger.error(f"Error loading user permissions page: {str(e)}")
flash("Fehler beim Laden der Benutzerberechtigungen", "error")
return redirect(url_for('admin.users_overview'))
@users_blueprint.route('/api/users/<int:user_id>/permissions', methods=['GET'])
@users_admin_required
def get_user_permissions_api(user_id):
"""API-Endpunkt für Benutzerberechtigungen"""
try:
with get_cached_session() as session:
user = session.query(User).filter(User.id == user_id).first()
if not user:
return jsonify({"error": "Benutzer nicht gefunden"}), 404
permissions = session.query(UserPermission).filter(UserPermission.user_id == user_id).first()
if not permissions:
permissions = UserPermission(user_id=user_id)
session.add(permissions)
session.commit()
permissions_data = {
"user_id": user_id,
"username": user.username,
"can_start_jobs": permissions.can_start_jobs,
"needs_approval": permissions.needs_approval,
"can_approve_jobs": permissions.can_approve_jobs
}
return jsonify(permissions_data)
except Exception as e:
users_logger.error(f"Error getting user permissions via API: {str(e)}")
return jsonify({"error": "Fehler beim Abrufen der Benutzerberechtigungen"}), 500
@users_blueprint.route('/api/users/<int:user_id>/permissions', methods=['PUT'])
@users_admin_required
def update_user_permissions_api(user_id):
"""Benutzerberechtigungen via API aktualisieren"""
try:
data = request.get_json()
with get_cached_session() as session:
user = session.query(User).filter(User.id == user_id).first()
if not user:
return jsonify({"error": "Benutzer nicht gefunden"}), 404
permissions = session.query(UserPermission).filter(UserPermission.user_id == user_id).first()
if not permissions:
permissions = UserPermission(user_id=user_id)
session.add(permissions)
# Berechtigungen aktualisieren
if 'can_start_jobs' in data:
permissions.can_start_jobs = data['can_start_jobs']
if 'needs_approval' in data:
permissions.needs_approval = data['needs_approval']
if 'can_approve_jobs' in data:
permissions.can_approve_jobs = data['can_approve_jobs']
session.commit()
users_logger.info(f"Admin {current_user.username} updated permissions for user {user.username}")
return jsonify({
"success": True,
"message": "Berechtigungen erfolgreich aktualisiert"
})
except Exception as e:
users_logger.error(f"Error updating user permissions via API: {str(e)}")
return jsonify({"error": "Fehler beim Aktualisieren der Berechtigungen"}), 500
@users_blueprint.route('/admin/users/<int:user_id>/permissions/update', methods=['POST'])
@users_admin_required
def update_user_permissions_form(user_id):
"""Benutzerberechtigungen via Formular aktualisieren"""
try:
with get_cached_session() as session:
user = session.query(User).filter(User.id == user_id).first()
if not user:
flash("Benutzer nicht gefunden", "error")
return redirect(url_for('admin.users_overview'))
permissions = session.query(UserPermission).filter(UserPermission.user_id == user_id).first()
if not permissions:
permissions = UserPermission(user_id=user_id)
session.add(permissions)
# Formular-Daten verarbeiten
permissions.can_start_jobs = 'can_start_jobs' in request.form
permissions.needs_approval = 'needs_approval' in request.form
permissions.can_approve_jobs = 'can_approve_jobs' in request.form
session.commit()
users_logger.info(f"Admin {current_user.username} updated permissions for user {user.username} via form")
flash("Berechtigungen erfolgreich aktualisiert", "success")
return redirect(url_for('users.user_permissions_page', user_id=user_id))
except Exception as e:
users_logger.error(f"Error updating user permissions via form: {str(e)}")
flash("Fehler beim Aktualisieren der Berechtigungen", "error")
return redirect(url_for('users.user_permissions_page', user_id=user_id))
@users_blueprint.route('/admin/users/<int:user_id>/edit/permissions', methods=['GET'])
@users_admin_required
def edit_user_permissions_section(user_id):
"""Berechtigungsbereich für Benutzer bearbeiten"""
try:
with get_cached_session() as session:
user = session.query(User).filter(User.id == user_id).first()
if not user:
return jsonify({"error": "Benutzer nicht gefunden"}), 404
permissions = session.query(UserPermission).filter(UserPermission.user_id == user_id).first()
if not permissions:
permissions = UserPermission(user_id=user_id)
return render_template('admin/edit_user_permissions_section.html', user=user, permissions=permissions)
except Exception as e:
users_logger.error(f"Error loading user permissions edit section: {str(e)}")
return jsonify({"error": "Fehler beim Laden der Berechtigungsbearbeitung"}), 500
@users_blueprint.route('/api/users/<int:user_id>', methods=['GET'])
@users_admin_required
def get_user_details_api(user_id):
"""API-Endpunkt für detaillierte Benutzerdaten"""
try:
with get_cached_session() as session:
user = session.query(User).filter(User.id == user_id).first()
if not user:
return jsonify({"error": "Benutzer nicht gefunden"}), 404
user_data = {
"id": user.id,
"username": user.username,
"email": user.email,
"name": user.name,
"role": user.role,
"active": user.active,
"created_at": user.created_at.isoformat() if user.created_at else None,
"last_login": user.last_login.isoformat() if user.last_login else None,
"department": user.department,
"position": user.position,
"phone": user.phone,
"bio": user.bio,
"theme_preference": user.theme_preference,
"language_preference": user.language_preference,
"email_notifications": user.email_notifications,
"browser_notifications": user.browser_notifications
}
return jsonify(user_data)
except Exception as e:
users_logger.error(f"Error getting user details via API: {str(e)}")
return jsonify({"error": "Fehler beim Abrufen der Benutzerdaten"}), 500