Files
xenia-pos-cloud/cloud_backend/routers/manager_auth.py
bonamin 17fb3a2589 fix(sysadmin): use site_id UUID string instead of integer PK throughout manager endpoints
The sysadmin panel URL uses site.site_id (UUID string) not site.id (int PK).
All manager account endpoints were querying/expecting the integer PK — none matched.

- GET /by-site/{site_id}: param type str, filter by Site.site_id
- POST /register: site_ids type list[str], query Site.site_id.in_()
- DELETE /site-access: site_id type str, query Site.site_id
- schemas/manager.py: ManagerRegisterRequest.site_ids list[int] → list[str]
- SiteDetailPage.jsx: remove Number() casts on siteId in add/remove calls

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

151 lines
5.8 KiB
Python

from datetime import datetime, timedelta, timezone
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import jwt, JWTError
from passlib.context import CryptContext
from pydantic import BaseModel
from sqlalchemy.orm import Session
from config import settings
from database import get_db
from models.manager_account import ManagerAccount, manager_site_access
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),
):
existing = db.query(ManagerAccount).filter(ManagerAccount.email == body.email).first()
sites = db.query(Site).filter(Site.site_id.in_(body.site_ids)).all()
if existing:
# Email already exists — just add the new site access links, don't recreate the account
for site in sites:
if site not in existing.sites:
existing.sites.append(site)
db.commit()
db.refresh(existing)
return existing
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))
# ── Admin-only: list managers for a site ─────────────────────────────────────
class ManagerBySiteOut(BaseModel):
id: int
email: str
full_name: Optional[str] = None
is_active: int
model_config = {"from_attributes": True}
@router.get("/by-site/{site_id}", response_model=list[ManagerBySiteOut])
def get_managers_by_site(
site_id: str,
db: Session = Depends(get_db),
_admin=Depends(get_current_admin),
):
site = db.query(Site).filter(Site.site_id == site_id).first()
if not site:
raise HTTPException(status_code=404, detail="Site not found")
return site.manager_accounts
# ── Admin-only: remove a manager's access to a site ──────────────────────────
class SiteAccessRemoveRequest(BaseModel):
manager_id: int
site_id: str # site_id UUID string, not integer PK
@router.delete("/site-access", status_code=status.HTTP_204_NO_CONTENT)
def remove_manager_site_access(
body: SiteAccessRemoveRequest,
db: Session = Depends(get_db),
_admin=Depends(get_current_admin),
):
manager = db.query(ManagerAccount).filter(ManagerAccount.id == body.manager_id).first()
if not manager:
raise HTTPException(status_code=404, detail="Manager not found")
site = db.query(Site).filter(Site.site_id == body.site_id).first()
if not site:
raise HTTPException(status_code=404, detail="Site not found")
if site in manager.sites:
manager.sites.remove(site)
db.commit()