Dashboard Add top-right user dropdown using Syncfusion DropDownButton: shows username + role; menu entries “Profil” and “Abmelden”. Replace custom dropdown logic with Syncfusion component; position at header’s right edge. Update /logout page to call backend logout and redirect to /login (reliable user switching). Build/Config Add @syncfusion/ej2-react-splitbuttons and @syncfusion/ej2-splitbuttons dependencies. Update Vite optimizeDeps.include to pre-bundle splitbuttons and avoid import-analysis errors. Docs README: Rework Architecture Overview with clearer data flow: Listener consumes MQTT (discovery/heartbeats) and updates API. Scheduler reads from API and publishes events via MQTT to clients. Clients send via MQTT and receive via MQTT. Worker receives commands directly from API and reports results back (no MQTT). Explicit note: MariaDB is accessed exclusively by the API Server; Dashboard never talks to DB directly. README: Add SplitButtons to “Syncfusion Components Used”; add troubleshooting steps for @syncfusion/ej2-react-splitbuttons import issues (optimizeDeps + volume reset). Copilot instructions: Document header user menu and splitbuttons technical notes (deps, optimizeDeps, dev-container node_modules volume). Program info Bump to 2025.1.0-alpha.10 with changelog: UI: Header user menu (DropDownButton with username/role; Profil/Abmelden). Frontend: Syncfusion SplitButtons integration + Vite pre-bundling config. Fix: Added README guidance for splitbuttons import errors. No breaking changes.
177 lines
5.2 KiB
Python
177 lines
5.2 KiB
Python
"""
|
|
Permission decorators for role-based access control.
|
|
|
|
This module provides decorators to protect Flask routes based on user roles.
|
|
"""
|
|
|
|
from functools import wraps
|
|
from flask import session, jsonify
|
|
import os
|
|
from models.models import UserRole
|
|
|
|
|
|
def require_auth(f):
|
|
"""
|
|
Require user to be authenticated.
|
|
|
|
Usage:
|
|
@app.route('/protected')
|
|
@require_auth
|
|
def protected_route():
|
|
return "You are logged in"
|
|
"""
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
user_id = session.get('user_id')
|
|
if not user_id:
|
|
return jsonify({"error": "Authentication required"}), 401
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
|
|
def require_role(*allowed_roles):
|
|
"""
|
|
Require user to have one of the specified roles.
|
|
|
|
Args:
|
|
*allowed_roles: Variable number of role strings or UserRole enum values
|
|
|
|
Usage:
|
|
@app.route('/admin-only')
|
|
@require_role('admin', 'superadmin')
|
|
def admin_route():
|
|
return "Admin access"
|
|
|
|
# Or using enum:
|
|
@require_role(UserRole.admin, UserRole.superadmin)
|
|
def admin_route():
|
|
return "Admin access"
|
|
"""
|
|
# Convert all roles to strings for comparison
|
|
allowed_role_strings = set()
|
|
for role in allowed_roles:
|
|
if isinstance(role, UserRole):
|
|
allowed_role_strings.add(role.value)
|
|
elif isinstance(role, str):
|
|
allowed_role_strings.add(role)
|
|
else:
|
|
raise ValueError(f"Invalid role type: {type(role)}")
|
|
|
|
def decorator(f):
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
user_id = session.get('user_id')
|
|
user_role = session.get('role')
|
|
|
|
if not user_id or not user_role:
|
|
return jsonify({"error": "Authentication required"}), 401
|
|
|
|
# In development, allow superadmin to bypass all checks to prevent blocking
|
|
env = os.environ.get('ENV', 'production').lower()
|
|
if env in ('development', 'dev') and user_role == UserRole.superadmin.value:
|
|
return f(*args, **kwargs)
|
|
|
|
if user_role not in allowed_role_strings:
|
|
return jsonify({
|
|
"error": "Insufficient permissions",
|
|
"required_roles": list(allowed_role_strings),
|
|
"your_role": user_role
|
|
}), 403
|
|
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
return decorator
|
|
|
|
|
|
def require_any_role(*allowed_roles):
|
|
"""
|
|
Alias for require_role for better readability.
|
|
Require user to have ANY of the specified roles.
|
|
|
|
Usage:
|
|
@require_any_role('editor', 'admin', 'superadmin')
|
|
def edit_route():
|
|
return "Can edit"
|
|
"""
|
|
return require_role(*allowed_roles)
|
|
|
|
|
|
def require_all_roles(*required_roles):
|
|
"""
|
|
Require user to have ALL of the specified roles.
|
|
Note: This is typically not needed since users only have one role,
|
|
but included for completeness.
|
|
|
|
Usage:
|
|
@require_all_roles('admin')
|
|
def strict_route():
|
|
return "Must have all roles"
|
|
"""
|
|
# Convert all roles to strings
|
|
required_role_strings = set()
|
|
for role in required_roles:
|
|
if isinstance(role, UserRole):
|
|
required_role_strings.add(role.value)
|
|
elif isinstance(role, str):
|
|
required_role_strings.add(role)
|
|
|
|
def decorator(f):
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
user_id = session.get('user_id')
|
|
user_role = session.get('role')
|
|
|
|
if not user_id or not user_role:
|
|
return jsonify({"error": "Authentication required"}), 401
|
|
|
|
# For single-role systems, check if user role is in required set
|
|
if user_role not in required_role_strings:
|
|
return jsonify({
|
|
"error": "Insufficient permissions",
|
|
"required_roles": list(required_role_strings),
|
|
"your_role": user_role
|
|
}), 403
|
|
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
return decorator
|
|
|
|
|
|
def superadmin_only(f):
|
|
"""
|
|
Convenience decorator for superadmin-only routes.
|
|
|
|
Usage:
|
|
@app.route('/critical-settings')
|
|
@superadmin_only
|
|
def critical_settings():
|
|
return "Superadmin only"
|
|
"""
|
|
return require_role(UserRole.superadmin)(f)
|
|
|
|
|
|
def admin_or_higher(f):
|
|
"""
|
|
Convenience decorator for admin and superadmin routes.
|
|
|
|
Usage:
|
|
@app.route('/settings')
|
|
@admin_or_higher
|
|
def settings():
|
|
return "Admin or superadmin"
|
|
"""
|
|
return require_role(UserRole.admin, UserRole.superadmin)(f)
|
|
|
|
|
|
def editor_or_higher(f):
|
|
"""
|
|
Convenience decorator for editor, admin, and superadmin routes.
|
|
|
|
Usage:
|
|
@app.route('/events', methods=['POST'])
|
|
@editor_or_higher
|
|
def create_event():
|
|
return "Can create events"
|
|
"""
|
|
return require_role(UserRole.editor, UserRole.admin, UserRole.superadmin)(f)
|