Files
infoscreen/server/routes/auth.py
RobbStarkAustria 150937f2e2 docs(settings): Update README + Copilot instructions; bump Program Info to 2025.1.0-alpha.11
README: Add System Settings API endpoints; describe new tabbed Settings layout with role gating; add Vite dev proxy tip to use relative /api paths.
Copilot instructions: Note SystemSetting key–value store in data model; document system_settings.py (CRUD + supplement-table convenience endpoint); reference apiSystemSettings.ts; note defaults seeding via init_defaults.py.
Program Info: Bump version to 2025.1.0-alpha.11; changelog explicitly tied to the Settings page (Events tab: supplement-table URL moved; Academic Calendar: set active period; proxy note); README docs mention.
No functional changes to API or UI code in this commit; documentation and program info only.
2025-10-16 19:15:55 +00:00

219 lines
6.2 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
# For SQLAlchemy Enum(UserRole), ensure we return the string value
role_value = user.role.value if isinstance(user.role, UserRole) else str(user.role)
return jsonify({
"id": user.id,
"username": user.username,
"role": role_value,
"is_active": user.is_active
}), 200
except Exception as e:
# Avoid naked 500s; return a JSON error with minimal info (safe in dev)
env = os.environ.get("ENV", "production").lower()
msg = str(e) if env in ("development", "dev") else "Internal server error"
return jsonify({"error": msg}), 500
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()