from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from fastapi import APIRouter, Depends, HTTPException, Response, status from sqlalchemy import select from sqlalchemy.orm import Session from database import get_db from models import User from schemas import LoginRequest, UserOut, UserCreate, ChangePasswordRequest, TimezoneUpdate, AuthResponse from auth import ( verify_password, hash_password, create_access_token, get_current_user, set_auth_cookie, clear_auth_cookie, token_to_user_payload, ) router = APIRouter(prefix="/api/auth", tags=["auth"]) def _issue(response: Response, user: User, admin_id=None) -> AuthResponse: token = create_access_token(user.id, user.username, user.is_admin, user.timezone, admin_id=admin_id) set_auth_cookie(response, token) return AuthResponse(user=token_to_user_payload(token)) @router.post("/login", response_model=AuthResponse) def login(body: LoginRequest, response: Response, db: Session = Depends(get_db)): user = db.scalars(select(User).where(User.username == body.username)).first() if not user or not verify_password(body.password, user.hashed_password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid username or password", ) if user.is_disabled: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Account is disabled. Contact your administrator.", ) return _issue(response, user) @router.post("/register", response_model=AuthResponse, status_code=201) def register(body: UserCreate, response: Response, db: Session = Depends(get_db)): existing = db.scalars(select(User).where(User.username == body.username)).first() if existing: raise HTTPException(status_code=409, detail="Username already taken") user = User( username=body.username, hashed_password=hash_password(body.password), is_admin=False, timezone="UTC", ) db.add(user) db.commit() db.refresh(user) return _issue(response, user) @router.post("/logout") def logout(response: Response): clear_auth_cookie(response) return {"detail": "Logged out"} @router.get("/me", response_model=UserOut) def me(current_user: User = Depends(get_current_user)): return current_user @router.post("/change-password") def change_password( body: ChangePasswordRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): if not verify_password(body.current_password, current_user.hashed_password): raise HTTPException(status_code=400, detail="Current password is incorrect") current_user.hashed_password = hash_password(body.new_password) db.commit() return {"detail": "Password updated"} @router.put("/timezone", response_model=AuthResponse) def update_timezone( body: TimezoneUpdate, response: Response, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): try: ZoneInfo(body.timezone) except ZoneInfoNotFoundError: raise HTTPException(status_code=400, detail=f"Unknown timezone: {body.timezone}") current_user.timezone = body.timezone db.commit() db.refresh(current_user) return _issue(response, current_user)