Initial commit: Parking Manager

Features:
- Manager-centric parking spot management
- Fair assignment algorithm (parking/presence ratio)
- Presence tracking calendar
- Closing days (specific & weekly recurring)
- Guarantees and exclusions
- Authelia/LLDAP integration for SSO

Stack:
- FastAPI backend
- SQLite database
- Vanilla JS frontend
- Docker deployment
This commit is contained in:
Stefano Manfredi
2025-11-26 23:37:50 +00:00
commit c74a0ed350
49 changed files with 9094 additions and 0 deletions

0
utils/__init__.py Normal file
View File

170
utils/auth_middleware.py Normal file
View File

@@ -0,0 +1,170 @@
"""
Authentication Middleware
JWT token validation and Authelia header authentication for protected routes
"""
from fastapi import Depends, HTTPException, status, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
from datetime import datetime
import uuid
from database.connection import get_db
from database.models import User
from services.auth import decode_token, get_user_by_id, get_user_by_email
from app import config
security = HTTPBearer(auto_error=False)
def get_role_from_groups(groups: list[str]) -> str:
"""Map Authelia groups to application roles"""
if config.AUTHELIA_ADMIN_GROUP in groups:
return "admin"
if config.AUTHELIA_MANAGER_GROUP in groups:
return "manager"
return "employee"
def get_or_create_authelia_user(
email: str,
name: str,
groups: list[str],
db: Session
) -> User:
"""Get existing user or create from Authelia headers"""
user = get_user_by_email(db, email)
role = get_role_from_groups(groups)
if user:
# Update role if changed in LLDAP
if user.role != role:
user.role = role
user.updated_at = datetime.utcnow().isoformat()
db.commit()
db.refresh(user)
# Update name if changed
if user.name != name and name:
user.name = name
user.updated_at = datetime.utcnow().isoformat()
db.commit()
db.refresh(user)
return user
# Create new user from Authelia
user = User(
id=str(uuid.uuid4()),
email=email,
name=name or email.split("@")[0],
role=role,
password_hash=None, # No password for Authelia users
created_at=datetime.utcnow().isoformat(),
updated_at=datetime.utcnow().isoformat()
)
db.add(user)
db.commit()
db.refresh(user)
return user
def get_current_user(
request: Request,
credentials: HTTPAuthorizationCredentials = Depends(security),
db: Session = Depends(get_db)
):
"""
Extract and validate user from JWT token or Authelia headers.
Authelia mode takes precedence when enabled.
"""
# Authelia mode: trust headers from reverse proxy
if config.AUTHELIA_ENABLED:
remote_user = request.headers.get(config.AUTHELIA_HEADER_USER)
remote_email = request.headers.get(config.AUTHELIA_HEADER_EMAIL, remote_user)
remote_name = request.headers.get(config.AUTHELIA_HEADER_NAME, "")
remote_groups = request.headers.get(config.AUTHELIA_HEADER_GROUPS, "")
if not remote_user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated (Authelia headers missing)"
)
# Parse groups (comma-separated)
groups = [g.strip() for g in remote_groups.split(",") if g.strip()]
# Get or create user
return get_or_create_authelia_user(remote_email, remote_name, groups, db)
# JWT mode: validate Bearer token
if not credentials:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated"
)
token = credentials.credentials
payload = decode_token(token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token"
)
user_id = payload.get("sub")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token payload"
)
user = get_user_by_id(db, user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found"
)
return user
def require_admin(user=Depends(get_current_user)):
"""Require admin role"""
if user.role != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin role required"
)
return user
def require_manager_or_admin(user=Depends(get_current_user)):
"""Require manager or admin role"""
if user.role not in ["admin", "manager"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Manager or admin role required"
)
return user
def check_manager_access_to_user(current_user, target_user, db: Session) -> bool:
"""
Check if current_user (manager) has access to target_user.
Admins always have access. Managers can only access users in their managed offices.
Returns True if access granted, raises HTTPException if not.
"""
if current_user.role == "admin":
return True
if current_user.role == "manager":
managed_office_ids = [m.office_id for m in current_user.managed_offices]
if target_user.office_id not in managed_office_ids:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User not in your managed offices"
)
return True
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)