""" Authentication Routes Login, register, logout, and user info """ from fastapi import APIRouter, Depends, HTTPException, status, Response, Request from pydantic import BaseModel, EmailStr from sqlalchemy.orm import Session from slowapi import Limiter from slowapi.util import get_remote_address from database.connection import get_db from services.auth import ( create_user, authenticate_user, create_access_token, get_user_by_email ) from utils.auth_middleware import get_current_user from utils.helpers import validate_password, format_password_errors, get_notification_default from app import config router = APIRouter(prefix="/api/auth", tags=["auth"]) limiter = Limiter(key_func=get_remote_address) class RegisterRequest(BaseModel): email: EmailStr password: str name: str manager_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 manager_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) @limiter.limit(f"{config.RATE_LIMIT_REQUESTS}/minute") def register(request: Request, 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" ) # Validate password strength password_errors = validate_password(data.password) if password_errors: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=format_password_errors(password_errors) ) user = create_user( db=db, email=data.email, password=data.password, name=data.name, manager_id=data.manager_id ) config.logger.info(f"New user registered: {data.email}") token = create_access_token(user.id, user.email) return TokenResponse(access_token=token) @router.post("/login", response_model=TokenResponse) @limiter.limit(f"{config.RATE_LIMIT_REQUESTS}/minute") def login(request: Request, 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: config.logger.warning(f"Failed login attempt for: {data.email}") 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" ) config.logger.info(f"User logged in: {data.email}") 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, manager_id=user.manager_id, role=user.role, manager_parking_quota=user.manager_parking_quota, week_start_day=get_notification_default(user.week_start_day, 0), notify_weekly_parking=get_notification_default(user.notify_weekly_parking, 1), notify_daily_parking=get_notification_default(user.notify_daily_parking, 1), notify_daily_parking_hour=get_notification_default(user.notify_daily_parking_hour, 8), notify_daily_parking_minute=get_notification_default(user.notify_daily_parking_minute, 0), notify_parking_changes=get_notification_default(user.notify_parking_changes, 1) ) @router.get("/config") def get_auth_config(): """Get authentication configuration for frontend. Returns info about auth mode and external URLs. """ return { "authelia_enabled": config.AUTHELIA_ENABLED, "login_url": config.AUTHELIA_LOGIN_URL if config.AUTHELIA_ENABLED else None, "registration_url": config.REGISTRATION_URL if config.AUTHELIA_ENABLED else None } @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)