from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from jose import jwt, JWTError from passlib.context import CryptContext from sqlalchemy.orm import Session from config import settings from database import get_db from models.manager_account import ManagerAccount from models.site import Site from auth_utils import get_current_admin from schemas.manager import ( ManagerRegisterRequest, ManagerLoginRequest, ManagerTokenOut, ManagerOut, ) router = APIRouter() _pwd = CryptContext(schemes=["bcrypt"], deprecated="auto") _bearer = HTTPBearer() ALGORITHM = "HS256" # ── Shared dependency: decode manager JWT ───────────────────────────────────── def _get_current_manager(credentials: HTTPAuthorizationCredentials = Depends(_bearer), db: Session = Depends(get_db)) -> ManagerAccount: try: payload = jwt.decode(credentials.credentials, settings.MANAGER_JWT_SECRET, algorithms=[ALGORITHM]) manager_id = int(payload.get("sub", 0)) except (JWTError, ValueError): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") manager = db.query(ManagerAccount).filter(ManagerAccount.id == manager_id).first() if not manager or not manager.is_active: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Manager not found or inactive") return manager def _create_manager_token(manager: ManagerAccount) -> str: site_ids = [s.id for s in manager.sites] payload = { "sub": str(manager.id), "email": manager.email, "site_ids": site_ids, "exp": datetime.now(timezone.utc) + timedelta(hours=settings.MANAGER_JWT_EXPIRE_HOURS), } return jwt.encode(payload, settings.MANAGER_JWT_SECRET, algorithm=ALGORITHM) # ── Admin-only: register a manager ─────────────────────────────────────────── @router.post("/register", response_model=ManagerOut, status_code=status.HTTP_201_CREATED) def register_manager( body: ManagerRegisterRequest, db: Session = Depends(get_db), _admin=Depends(get_current_admin), ): if db.query(ManagerAccount).filter(ManagerAccount.email == body.email).first(): raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Email already registered") sites = db.query(Site).filter(Site.id.in_(body.site_ids)).all() manager = ManagerAccount( email=body.email, password_hash=_pwd.hash(body.password), full_name=body.full_name, sites=sites, ) db.add(manager) db.commit() db.refresh(manager) return manager # ── Public: manager login ───────────────────────────────────────────────────── @router.post("/login", response_model=ManagerTokenOut) def login_manager(body: ManagerLoginRequest, db: Session = Depends(get_db)): manager = db.query(ManagerAccount).filter(ManagerAccount.email == body.email).first() if not manager or not _pwd.verify(body.password, manager.password_hash): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") if not manager.is_active: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Account disabled") return ManagerTokenOut(access_token=_create_manager_token(manager)) # ── Public: refresh ─────────────────────────────────────────────────────────── @router.post("/refresh", response_model=ManagerTokenOut) def refresh_manager_token(manager: ManagerAccount = Depends(_get_current_manager)): return ManagerTokenOut(access_token=_create_manager_token(manager))