feat(dashboard): header user dropdown (Syncfusion) + proper logout; docs: clarify architecture; build: add splitbuttons; bump alpha.10
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.
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
"""Merge all heads before user role migration
|
||||
|
||||
Revision ID: 488ce87c28ae
|
||||
Revises: 12ab34cd56ef, 15c357c0cf31, add_userrole_editor_and_column
|
||||
Create Date: 2025-10-15 05:46:17.984934
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '488ce87c28ae'
|
||||
down_revision: Union[str, None] = ('12ab34cd56ef', '15c357c0cf31', 'add_userrole_editor_and_column')
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
pass
|
||||
40
server/alembic/versions/add_userrole_editor_and_column.py
Normal file
40
server/alembic/versions/add_userrole_editor_and_column.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
Add editor role to UserRole enum and ensure role column exists on users table
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import enum
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'add_userrole_editor_and_column'
|
||||
down_revision = None # Set this to the latest revision in your repo
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
# Define the new enum including 'editor'
|
||||
class userrole_enum(enum.Enum):
|
||||
user = "user"
|
||||
editor = "editor"
|
||||
admin = "admin"
|
||||
superadmin = "superadmin"
|
||||
|
||||
def upgrade():
|
||||
# MySQL: check if 'role' column exists
|
||||
conn = op.get_bind()
|
||||
insp = sa.inspect(conn)
|
||||
columns = [col['name'] for col in insp.get_columns('users')]
|
||||
if 'role' not in columns:
|
||||
with op.batch_alter_table('users') as batch_op:
|
||||
batch_op.add_column(sa.Column('role', sa.Enum('user', 'editor', 'admin', 'superadmin', name='userrole'), nullable=False, server_default='user'))
|
||||
else:
|
||||
# If the column exists, alter the ENUM to add 'editor' if not present
|
||||
# MySQL: ALTER TABLE users MODIFY COLUMN role ENUM(...)
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE users MODIFY COLUMN role ENUM('user','editor','admin','superadmin') NOT NULL DEFAULT 'user'"
|
||||
))
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('users') as batch_op:
|
||||
batch_op.drop_column('role')
|
||||
# ### end Alembic commands ###
|
||||
@@ -20,19 +20,25 @@ with engine.connect() as conn:
|
||||
)
|
||||
print("✅ Default-Gruppe mit id=1 angelegt.")
|
||||
|
||||
# Admin-Benutzer anlegen, falls nicht vorhanden
|
||||
admin_user = os.getenv("DEFAULT_ADMIN_USERNAME", "infoscreen_admin")
|
||||
admin_pw = os.getenv("DEFAULT_ADMIN_PASSWORD", "Info_screen_admin25!")
|
||||
# Superadmin-Benutzer anlegen, falls nicht vorhanden
|
||||
admin_user = os.getenv("DEFAULT_SUPERADMIN_USERNAME", "superadmin")
|
||||
admin_pw = os.getenv("DEFAULT_SUPERADMIN_PASSWORD")
|
||||
|
||||
if not admin_pw:
|
||||
print("⚠️ DEFAULT_SUPERADMIN_PASSWORD nicht gesetzt. Superadmin wird nicht erstellt.")
|
||||
else:
|
||||
# Passwort hashen mit bcrypt
|
||||
hashed_pw = bcrypt.hashpw(admin_pw.encode(
|
||||
'utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||
# Prüfen, ob User existiert
|
||||
result = conn.execute(text(
|
||||
"SELECT COUNT(*) FROM users WHERE username=:username"), {"username": admin_user})
|
||||
if result.scalar() == 0:
|
||||
# Rolle: 1 = Admin (ggf. anpassen je nach Modell)
|
||||
conn.execute(
|
||||
text("INSERT INTO users (username, password_hash, role, is_active) VALUES (:username, :password_hash, 1, 1)"),
|
||||
{"username": admin_user, "password_hash": hashed_pw}
|
||||
)
|
||||
print(f"✅ Admin-Benutzer '{admin_user}' angelegt.")
|
||||
hashed_pw = bcrypt.hashpw(admin_pw.encode(
|
||||
'utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||
# Prüfen, ob User existiert
|
||||
result = conn.execute(text(
|
||||
"SELECT COUNT(*) FROM users WHERE username=:username"), {"username": admin_user})
|
||||
if result.scalar() == 0:
|
||||
# Rolle: 'superadmin' gemäß UserRole enum
|
||||
conn.execute(
|
||||
text("INSERT INTO users (username, password_hash, role, is_active) VALUES (:username, :password_hash, 'superadmin', 1)"),
|
||||
{"username": admin_user, "password_hash": hashed_pw}
|
||||
)
|
||||
print(f"✅ Superadmin-Benutzer '{admin_user}' angelegt.")
|
||||
else:
|
||||
print(f"ℹ️ Superadmin-Benutzer '{admin_user}' existiert bereits.")
|
||||
|
||||
176
server/permissions.py
Normal file
176
server/permissions.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
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)
|
||||
@@ -1,4 +1,5 @@
|
||||
from flask import Blueprint, jsonify, request
|
||||
from server.permissions import admin_or_higher
|
||||
from server.database import Session
|
||||
from models.models import AcademicPeriod
|
||||
from datetime import datetime
|
||||
@@ -61,6 +62,7 @@ def get_period_for_date():
|
||||
|
||||
|
||||
@academic_periods_bp.route('/active', methods=['POST'])
|
||||
@admin_or_higher
|
||||
def set_active_academic_period():
|
||||
data = request.get_json(silent=True) or {}
|
||||
period_id = data.get('id')
|
||||
|
||||
210
server/routes/auth.py
Normal file
210
server/routes/auth.py
Normal file
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
Authentication and user management routes.
|
||||
|
||||
This module provides endpoints for user authentication and role information.
|
||||
Currently implements a basic session-based auth that can be extended with
|
||||
JWT or Flask-Login later.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify, session
|
||||
import os
|
||||
from server.database import Session
|
||||
from models.models import User, UserRole
|
||||
import bcrypt
|
||||
import sys
|
||||
|
||||
sys.path.append('/workspace')
|
||||
|
||||
auth_bp = Blueprint("auth", __name__, url_prefix="/api/auth")
|
||||
|
||||
|
||||
@auth_bp.route("/login", methods=["POST"])
|
||||
def login():
|
||||
"""
|
||||
Authenticate a user and create a session.
|
||||
|
||||
Request body:
|
||||
{
|
||||
"username": "string",
|
||||
"password": "string"
|
||||
}
|
||||
|
||||
Returns:
|
||||
200: {
|
||||
"message": "Login successful",
|
||||
"user": {
|
||||
"id": int,
|
||||
"username": "string",
|
||||
"role": "string"
|
||||
}
|
||||
}
|
||||
401: {"error": "Invalid credentials"}
|
||||
400: {"error": "Username and password required"}
|
||||
"""
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return jsonify({"error": "Request body required"}), 400
|
||||
|
||||
username = data.get("username")
|
||||
password = data.get("password")
|
||||
|
||||
if not username or not password:
|
||||
return jsonify({"error": "Username and password required"}), 400
|
||||
|
||||
db_session = Session()
|
||||
try:
|
||||
# Find user by username
|
||||
user = db_session.query(User).filter_by(username=username).first()
|
||||
|
||||
if not user:
|
||||
return jsonify({"error": "Invalid credentials"}), 401
|
||||
|
||||
# Check if user is active
|
||||
if not user.is_active:
|
||||
return jsonify({"error": "Account is disabled"}), 401
|
||||
|
||||
# Verify password
|
||||
if not bcrypt.checkpw(password.encode('utf-8'), user.password_hash.encode('utf-8')):
|
||||
return jsonify({"error": "Invalid credentials"}), 401
|
||||
|
||||
# Create session
|
||||
session['user_id'] = user.id
|
||||
session['username'] = user.username
|
||||
session['role'] = user.role.value
|
||||
# Persist session across browser restarts (uses PERMANENT_SESSION_LIFETIME)
|
||||
session.permanent = True
|
||||
|
||||
return jsonify({
|
||||
"message": "Login successful",
|
||||
"user": {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"role": user.role.value
|
||||
}
|
||||
}), 200
|
||||
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
|
||||
@auth_bp.route("/logout", methods=["POST"])
|
||||
def logout():
|
||||
"""
|
||||
End the current user session.
|
||||
|
||||
Returns:
|
||||
200: {"message": "Logout successful"}
|
||||
"""
|
||||
session.clear()
|
||||
return jsonify({"message": "Logout successful"}), 200
|
||||
|
||||
|
||||
@auth_bp.route("/me", methods=["GET"])
|
||||
def get_current_user():
|
||||
"""
|
||||
Get the current authenticated user's information.
|
||||
|
||||
Returns:
|
||||
200: {
|
||||
"id": int,
|
||||
"username": "string",
|
||||
"role": "string",
|
||||
"is_active": bool
|
||||
}
|
||||
401: {"error": "Not authenticated"}
|
||||
"""
|
||||
user_id = session.get('user_id')
|
||||
|
||||
if not user_id:
|
||||
return jsonify({"error": "Not authenticated"}), 401
|
||||
|
||||
db_session = Session()
|
||||
try:
|
||||
user = db_session.query(User).filter_by(id=user_id).first()
|
||||
|
||||
if not user:
|
||||
# Session is stale, user was deleted
|
||||
session.clear()
|
||||
return jsonify({"error": "Not authenticated"}), 401
|
||||
|
||||
if not user.is_active:
|
||||
# User was deactivated
|
||||
session.clear()
|
||||
return jsonify({"error": "Account is disabled"}), 401
|
||||
|
||||
return jsonify({
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"role": user.role.value,
|
||||
"is_active": user.is_active
|
||||
}), 200
|
||||
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
|
||||
@auth_bp.route("/check", methods=["GET"])
|
||||
def check_auth():
|
||||
"""
|
||||
Quick check if user is authenticated (lighter than /me).
|
||||
|
||||
Returns:
|
||||
200: {"authenticated": true, "role": "string"}
|
||||
200: {"authenticated": false}
|
||||
"""
|
||||
user_id = session.get('user_id')
|
||||
role = session.get('role')
|
||||
|
||||
if user_id and role:
|
||||
return jsonify({
|
||||
"authenticated": True,
|
||||
"role": role
|
||||
}), 200
|
||||
|
||||
return jsonify({"authenticated": False}), 200
|
||||
|
||||
|
||||
@auth_bp.route("/dev-login-superadmin", methods=["POST"])
|
||||
def dev_login_superadmin():
|
||||
"""
|
||||
Development-only endpoint to quickly establish a superadmin session without a password.
|
||||
|
||||
Enabled only when ENV is 'development' or 'dev'. Returns 404 otherwise.
|
||||
"""
|
||||
env = os.environ.get("ENV", "production").lower()
|
||||
if env not in ("development", "dev"):
|
||||
# Pretend the route does not exist in non-dev environments
|
||||
return jsonify({"error": "Not found"}), 404
|
||||
|
||||
db_session = Session()
|
||||
try:
|
||||
# Prefer explicit username from env, else pick any superadmin
|
||||
preferred_username = os.environ.get("DEFAULT_SUPERADMIN_USERNAME", "superadmin")
|
||||
user = (
|
||||
db_session.query(User)
|
||||
.filter((User.username == preferred_username) | (User.role == UserRole.superadmin))
|
||||
.order_by(User.id.asc())
|
||||
.first()
|
||||
)
|
||||
if not user:
|
||||
return jsonify({
|
||||
"error": "No superadmin user found. Seed a superadmin first (DEFAULT_SUPERADMIN_PASSWORD)."
|
||||
}), 404
|
||||
|
||||
# Establish session
|
||||
session['user_id'] = user.id
|
||||
session['username'] = user.username
|
||||
session['role'] = user.role.value
|
||||
session.permanent = True
|
||||
|
||||
return jsonify({
|
||||
"message": "Dev login successful (superadmin)",
|
||||
"user": {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"role": user.role.value
|
||||
}
|
||||
}), 200
|
||||
finally:
|
||||
db_session.close()
|
||||
@@ -1,6 +1,7 @@
|
||||
from server.database import Session
|
||||
from models.models import Client, ClientGroup
|
||||
from flask import Blueprint, request, jsonify
|
||||
from server.permissions import admin_or_higher
|
||||
from server.mqtt_helper import publish_client_group, delete_client_group_message, publish_multiple_client_groups
|
||||
import sys
|
||||
sys.path.append('/workspace')
|
||||
@@ -9,6 +10,7 @@ clients_bp = Blueprint("clients", __name__, url_prefix="/api/clients")
|
||||
|
||||
|
||||
@clients_bp.route("/sync-all-groups", methods=["POST"])
|
||||
@admin_or_higher
|
||||
def sync_all_client_groups():
|
||||
"""
|
||||
Administrative Route: Synchronisiert alle bestehenden Client-Gruppenzuordnungen mit MQTT
|
||||
@@ -73,6 +75,7 @@ def get_clients_without_description():
|
||||
|
||||
|
||||
@clients_bp.route("/<uuid>/description", methods=["PUT"])
|
||||
@admin_or_higher
|
||||
def set_client_description(uuid):
|
||||
data = request.get_json()
|
||||
description = data.get("description", "").strip()
|
||||
@@ -127,6 +130,7 @@ def get_clients():
|
||||
|
||||
|
||||
@clients_bp.route("/group", methods=["PUT"])
|
||||
@admin_or_higher
|
||||
def update_clients_group():
|
||||
data = request.get_json()
|
||||
client_ids = data.get("client_ids", [])
|
||||
@@ -178,6 +182,7 @@ def update_clients_group():
|
||||
|
||||
|
||||
@clients_bp.route("/<uuid>", methods=["PATCH"])
|
||||
@admin_or_higher
|
||||
def update_client(uuid):
|
||||
data = request.get_json()
|
||||
session = Session()
|
||||
@@ -234,6 +239,7 @@ def get_clients_with_alive_status():
|
||||
|
||||
|
||||
@clients_bp.route("/<uuid>/restart", methods=["POST"])
|
||||
@admin_or_higher
|
||||
def restart_client(uuid):
|
||||
"""
|
||||
Route to restart a specific client by UUID.
|
||||
@@ -268,6 +274,7 @@ def restart_client(uuid):
|
||||
|
||||
|
||||
@clients_bp.route("/<uuid>", methods=["DELETE"])
|
||||
@admin_or_higher
|
||||
def delete_client(uuid):
|
||||
session = Session()
|
||||
client = session.query(Client).filter_by(uuid=uuid).first()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from flask import Blueprint, jsonify, request
|
||||
from server.permissions import editor_or_higher
|
||||
from server.database import Session
|
||||
from models.models import Conversion, ConversionStatus, EventMedia, MediaType
|
||||
from server.task_queue import get_queue
|
||||
@@ -19,6 +20,7 @@ def sha256_file(abs_path: str) -> str:
|
||||
|
||||
|
||||
@conversions_bp.route("/<int:media_id>/pdf", methods=["POST"])
|
||||
@editor_or_higher
|
||||
def ensure_conversion(media_id: int):
|
||||
session = Session()
|
||||
try:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from server.permissions import editor_or_higher
|
||||
from server.database import Session
|
||||
from models.models import EventException, Event
|
||||
from datetime import datetime, date
|
||||
@@ -7,6 +8,7 @@ event_exceptions_bp = Blueprint("event_exceptions", __name__, url_prefix="/api/e
|
||||
|
||||
|
||||
@event_exceptions_bp.route("", methods=["POST"])
|
||||
@editor_or_higher
|
||||
def create_exception():
|
||||
data = request.json
|
||||
session = Session()
|
||||
@@ -50,6 +52,7 @@ def create_exception():
|
||||
|
||||
|
||||
@event_exceptions_bp.route("/<exc_id>", methods=["PUT"])
|
||||
@editor_or_higher
|
||||
def update_exception(exc_id):
|
||||
data = request.json
|
||||
session = Session()
|
||||
@@ -77,6 +80,7 @@ def update_exception(exc_id):
|
||||
|
||||
|
||||
@event_exceptions_bp.route("/<exc_id>", methods=["DELETE"])
|
||||
@editor_or_higher
|
||||
def delete_exception(exc_id):
|
||||
session = Session()
|
||||
exc = session.query(EventException).filter_by(id=exc_id).first()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from re import A
|
||||
from flask import Blueprint, request, jsonify, send_from_directory
|
||||
from server.permissions import editor_or_higher
|
||||
from server.database import Session
|
||||
from models.models import EventMedia, MediaType, Conversion, ConversionStatus
|
||||
from server.task_queue import get_queue
|
||||
@@ -25,6 +26,7 @@ def get_param(key, default=None):
|
||||
|
||||
|
||||
@eventmedia_bp.route('/filemanager/operations', methods=['GET', 'POST'])
|
||||
@editor_or_higher
|
||||
def filemanager_operations():
|
||||
action = get_param('action')
|
||||
path = get_param('path', '/')
|
||||
@@ -115,6 +117,7 @@ def filemanager_operations():
|
||||
|
||||
|
||||
@eventmedia_bp.route('/filemanager/upload', methods=['POST'])
|
||||
@editor_or_higher
|
||||
def filemanager_upload():
|
||||
session = Session()
|
||||
# Korrigiert: Erst aus request.form, dann aus request.args lesen
|
||||
@@ -210,6 +213,7 @@ def list_media():
|
||||
|
||||
|
||||
@eventmedia_bp.route('/<int:media_id>', methods=['PUT'])
|
||||
@editor_or_higher
|
||||
def update_media(media_id):
|
||||
session = Session()
|
||||
media = session.query(EventMedia).get(media_id)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from server.permissions import editor_or_higher
|
||||
from server.database import Session
|
||||
from models.models import Event, EventMedia, MediaType, EventException
|
||||
from datetime import datetime, timezone, timedelta
|
||||
@@ -140,6 +141,7 @@ def get_event(event_id):
|
||||
|
||||
|
||||
@events_bp.route("/<event_id>", methods=["DELETE"]) # delete series or single event
|
||||
@editor_or_higher
|
||||
def delete_event(event_id):
|
||||
session = Session()
|
||||
event = session.query(Event).filter_by(id=event_id).first()
|
||||
@@ -162,7 +164,7 @@ def delete_event(event_id):
|
||||
|
||||
|
||||
@events_bp.route("/<event_id>/occurrences/<occurrence_date>", methods=["DELETE"]) # skip single occurrence
|
||||
|
||||
@editor_or_higher
|
||||
def delete_event_occurrence(event_id, occurrence_date):
|
||||
"""Delete a single occurrence of a recurring event by creating an EventException."""
|
||||
session = Session()
|
||||
@@ -217,6 +219,7 @@ def delete_event_occurrence(event_id, occurrence_date):
|
||||
|
||||
|
||||
@events_bp.route("/<event_id>/occurrences/<occurrence_date>/detach", methods=["POST"]) # detach single occurrence into standalone event
|
||||
@editor_or_higher
|
||||
def detach_event_occurrence(event_id, occurrence_date):
|
||||
"""BULLETPROOF: Detach single occurrence without touching master event."""
|
||||
session = Session()
|
||||
@@ -322,6 +325,7 @@ def detach_event_occurrence(event_id, occurrence_date):
|
||||
|
||||
|
||||
@events_bp.route("", methods=["POST"])
|
||||
@editor_or_higher
|
||||
def create_event():
|
||||
data = request.json
|
||||
session = Session()
|
||||
@@ -438,6 +442,7 @@ def create_event():
|
||||
|
||||
|
||||
@events_bp.route("/<event_id>", methods=["PUT"]) # update series or single event
|
||||
@editor_or_higher
|
||||
def update_event(event_id):
|
||||
data = request.json
|
||||
session = Session()
|
||||
|
||||
@@ -4,6 +4,7 @@ from models.models import Client
|
||||
from server.database import Session
|
||||
from models.models import ClientGroup
|
||||
from flask import Blueprint, request, jsonify
|
||||
from server.permissions import admin_or_higher, require_role
|
||||
from sqlalchemy import func
|
||||
import sys
|
||||
import os
|
||||
@@ -41,6 +42,7 @@ def is_client_alive(last_alive, is_active):
|
||||
|
||||
|
||||
@groups_bp.route("", methods=["POST"])
|
||||
@admin_or_higher
|
||||
def create_group():
|
||||
data = request.get_json()
|
||||
name = data.get("name")
|
||||
@@ -83,6 +85,7 @@ def get_groups():
|
||||
|
||||
|
||||
@groups_bp.route("/<int:group_id>", methods=["PUT"])
|
||||
@admin_or_higher
|
||||
def update_group(group_id):
|
||||
data = request.get_json()
|
||||
session = Session()
|
||||
@@ -106,6 +109,7 @@ def update_group(group_id):
|
||||
|
||||
|
||||
@groups_bp.route("/<int:group_id>", methods=["DELETE"])
|
||||
@admin_or_higher
|
||||
def delete_group(group_id):
|
||||
session = Session()
|
||||
group = session.query(ClientGroup).filter_by(id=group_id).first()
|
||||
@@ -119,6 +123,7 @@ def delete_group(group_id):
|
||||
|
||||
|
||||
@groups_bp.route("/byname/<string:group_name>", methods=["DELETE"])
|
||||
@admin_or_higher
|
||||
def delete_group_by_name(group_name):
|
||||
session = Session()
|
||||
group = session.query(ClientGroup).filter_by(name=group_name).first()
|
||||
@@ -132,6 +137,7 @@ def delete_group_by_name(group_name):
|
||||
|
||||
|
||||
@groups_bp.route("/byname/<string:old_name>", methods=["PUT"])
|
||||
@admin_or_higher
|
||||
def rename_group_by_name(old_name):
|
||||
data = request.get_json()
|
||||
new_name = data.get("newName")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from server.permissions import admin_or_higher
|
||||
from server.database import Session
|
||||
from models.models import SchoolHoliday
|
||||
from datetime import datetime
|
||||
@@ -22,6 +23,7 @@ def list_holidays():
|
||||
|
||||
|
||||
@holidays_bp.route("/upload", methods=["POST"])
|
||||
@admin_or_higher
|
||||
def upload_holidays():
|
||||
"""
|
||||
Accepts a CSV/TXT file upload (multipart/form-data).
|
||||
|
||||
@@ -8,6 +8,7 @@ from server.routes.holidays import holidays_bp
|
||||
from server.routes.academic_periods import academic_periods_bp
|
||||
from server.routes.groups import groups_bp
|
||||
from server.routes.clients import clients_bp
|
||||
from server.routes.auth import auth_bp
|
||||
from server.database import Session, engine
|
||||
from flask import Flask, jsonify, send_from_directory, request
|
||||
import glob
|
||||
@@ -17,8 +18,25 @@ sys.path.append('/workspace')
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Configure Flask session
|
||||
# In production, use a secure random key from environment variable
|
||||
app.config['SECRET_KEY'] = os.environ.get('FLASK_SECRET_KEY', 'dev-secret-key-change-in-production')
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||
# In production, set to True if using HTTPS
|
||||
app.config['SESSION_COOKIE_SECURE'] = os.environ.get('ENV', 'development').lower() == 'production'
|
||||
# Session lifetime: longer in development for convenience
|
||||
from datetime import timedelta
|
||||
_env = os.environ.get('ENV', 'development').lower()
|
||||
if _env in ('development', 'dev'):
|
||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30)
|
||||
else:
|
||||
# Keep modest in production; can be tuned via env later
|
||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=1)
|
||||
|
||||
# Blueprints importieren und registrieren
|
||||
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(clients_bp)
|
||||
app.register_blueprint(groups_bp)
|
||||
app.register_blueprint(events_bp)
|
||||
|
||||
Reference in New Issue
Block a user