Files
xenia-pos-cloud/cloud_backend/routers/manager_auth.py
bonamin f291234c14 fix: switch manager auth from OAuth2PasswordBearer to HTTPBearer
OAuth2PasswordBearer caused Swagger UI to show a username/password/
client_id form instead of a simple token input, making the docs
unusable for testing. HTTPBearer shows a plain 'Bearer token' field,
consistent with how the local_backend handles auth.

No runtime behaviour change — token extraction is identical.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 00:49:09 +03:00

92 lines
4.0 KiB
Python

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))