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

137
app/routes/auth.py Normal file
View File

@@ -0,0 +1,137 @@
"""
Authentication Routes
Login, register, logout, and user info
"""
from fastapi import APIRouter, Depends, HTTPException, status, Response
from pydantic import BaseModel, EmailStr
from sqlalchemy.orm import Session
from database.connection import get_db
from services.auth import (
create_user, authenticate_user, create_access_token,
get_user_by_email, hash_password, verify_password
)
from utils.auth_middleware import get_current_user
from app import config
import re
router = APIRouter(prefix="/api/auth", tags=["auth"])
class RegisterRequest(BaseModel):
email: EmailStr
password: str
name: str
office_id: str | None = None
class LoginRequest(BaseModel):
email: EmailStr
password: str
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
class UserResponse(BaseModel):
id: str
email: str
name: str | None
office_id: str | None
role: str
manager_parking_quota: int | None = None
week_start_day: int = 0
# Notification preferences
notify_weekly_parking: int = 1
notify_daily_parking: int = 1
notify_daily_parking_hour: int = 8
notify_daily_parking_minute: int = 0
notify_parking_changes: int = 1
@router.post("/register", response_model=TokenResponse)
def register(data: RegisterRequest, db: Session = Depends(get_db)):
"""Register a new user"""
if get_user_by_email(db, data.email):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
if len(data.password) < 8:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Password must be at least 8 characters"
)
user = create_user(
db=db,
email=data.email,
password=data.password,
name=data.name,
office_id=data.office_id
)
token = create_access_token(user.id, user.email)
return TokenResponse(access_token=token)
@router.post("/login", response_model=TokenResponse)
def login(data: LoginRequest, response: Response, db: Session = Depends(get_db)):
"""Login with email and password"""
user = authenticate_user(db, data.email, data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials"
)
token = create_access_token(user.id, user.email)
response.set_cookie(
key="session_token",
value=token,
httponly=True,
max_age=config.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
samesite="lax"
)
return TokenResponse(access_token=token)
@router.post("/logout")
def logout(response: Response):
"""Logout and clear session"""
response.delete_cookie("session_token")
return {"message": "Logged out"}
@router.get("/me", response_model=UserResponse)
def get_me(user=Depends(get_current_user)):
"""Get current user info"""
return UserResponse(
id=user.id,
email=user.email,
name=user.name,
office_id=user.office_id,
role=user.role,
manager_parking_quota=user.manager_parking_quota,
week_start_day=user.week_start_day or 0,
notify_weekly_parking=user.notify_weekly_parking if user.notify_weekly_parking is not None else 1,
notify_daily_parking=user.notify_daily_parking if user.notify_daily_parking is not None else 1,
notify_daily_parking_hour=user.notify_daily_parking_hour if user.notify_daily_parking_hour is not None else 8,
notify_daily_parking_minute=user.notify_daily_parking_minute if user.notify_daily_parking_minute is not None else 0,
notify_parking_changes=user.notify_parking_changes if user.notify_parking_changes is not None else 1
)
@router.get("/holidays/{year}")
def get_holidays(year: int):
"""Get public holidays for a given year"""
from services.holidays import get_holidays_for_year
if year < 2000 or year > 2100:
raise HTTPException(status_code=400, detail="Year must be between 2000 and 2100")
return get_holidays_for_year(year)