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.
211 lines
5.8 KiB
Python
211 lines
5.8 KiB
Python
"""
|
|
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()
|