Key changes: - Removed office-centric model (deleted offices.py, office-rules) - Renamed to team-rules, managers are part of their own team - Team calendar visible to all (read-only for employees) - Admins can have a manager assigned
138 lines
4.0 KiB
Python
138 lines
4.0 KiB
Python
"""
|
|
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
|
|
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)
|
|
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,
|
|
manager_id=data.manager_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,
|
|
manager_id=user.manager_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)
|