from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.orm import Session from database import get_db from models import User from schemas import LoginRequest, TokenResponse, UserOut, UserCreate, ChangePasswordRequest, TimezoneUpdate from auth import verify_password, hash_password, create_access_token, get_current_user router = APIRouter(prefix="/api/auth", tags=["auth"]) def _make_token(user: User) -> str: return create_access_token(user.id, user.username, user.is_admin, user.timezone) @router.post("/login", response_model=TokenResponse) def login(body: LoginRequest, 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 TokenResponse(access_token=_make_token(user)) @router.post("/register", response_model=TokenResponse, status_code=201) def register(body: UserCreate, 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") # Default timezone to UTC; user can change it in settings 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 TokenResponse(access_token=_make_token(user)) @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=TokenResponse) def update_timezone( body: TimezoneUpdate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): try: ZoneInfo(body.timezone) # validate it's a real IANA 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 a fresh token with the updated timezone embedded return TokenResponse(access_token=_make_token(current_user))