Snapshot of in-progress work across local_backend, manager_dashboard, and waiter_pwa (pricing, chat, fiscal, prep zones, recovery codes, CRM, inventory, permissions), plus the nginx/docker-compose deploy fixes for the Unraid + NPM reverse-proxy setup. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
154 lines
5.3 KiB
Python
154 lines
5.3 KiB
Python
import jwt
|
|
from datetime import datetime, timedelta, timezone
|
|
from fastapi import Depends, HTTPException, status
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
from sqlalchemy.orm import Session
|
|
|
|
from database import get_db
|
|
from config import settings
|
|
from models.user import User
|
|
|
|
bearer = HTTPBearer()
|
|
|
|
# In-memory token blacklist (cleared on restart — acceptable for local use)
|
|
_blacklisted_tokens: set[str] = set()
|
|
|
|
TOKEN_EXPIRY_HOURS = 8
|
|
|
|
|
|
MASTER_USER_ID = 0 # sentinel — never a real DB row
|
|
|
|
|
|
def _make_ghost_superadmin():
|
|
"""Synthetic in-memory superadmin — plain namespace, never touches SQLAlchemy."""
|
|
from types import SimpleNamespace
|
|
return SimpleNamespace(
|
|
id=MASTER_USER_ID,
|
|
username=settings.MASTER_USERNAME,
|
|
full_name="Xenia Support",
|
|
role="superadmin",
|
|
is_active=True,
|
|
pin_hash=None,
|
|
password_hash=None,
|
|
email=None,
|
|
nickname=None,
|
|
mobile_phone=None,
|
|
note=None,
|
|
avatar_url=None,
|
|
waiter_settings=None,
|
|
hourly_rate=None,
|
|
created_at=datetime(2024, 1, 1, tzinfo=timezone.utc),
|
|
zone_assignments=[],
|
|
perm_access_dashboard=True,
|
|
perm_access_waiter_app=True,
|
|
perm_access_kds=True,
|
|
perm_cancel_orders=True,
|
|
perm_apply_discounts=True,
|
|
perm_modify_prices=True,
|
|
perm_open_orders=True,
|
|
perm_close_orders=True,
|
|
perm_view_reports=True,
|
|
perm_manage_staff=True,
|
|
perm_manage_tables=True,
|
|
perm_manage_menu=True,
|
|
perm_manage_settings=True,
|
|
)
|
|
|
|
|
|
def make_token(user: User) -> str:
|
|
payload = {
|
|
"sub": str(user.id),
|
|
"username": user.username,
|
|
"role": user.role,
|
|
"exp": datetime.now(timezone.utc) + timedelta(hours=TOKEN_EXPIRY_HOURS),
|
|
}
|
|
return jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")
|
|
|
|
|
|
def decode_token(token: str) -> dict:
|
|
if token in _blacklisted_tokens:
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token revoked")
|
|
try:
|
|
return jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
|
|
except jwt.ExpiredSignatureError:
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired")
|
|
except jwt.InvalidTokenError:
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
|
|
|
|
|
def blacklist_token(token: str):
|
|
_blacklisted_tokens.add(token)
|
|
|
|
|
|
def get_current_user(
|
|
credentials: HTTPAuthorizationCredentials = Depends(bearer),
|
|
db: Session = Depends(get_db),
|
|
) -> User:
|
|
payload = decode_token(credentials.credentials)
|
|
if int(payload["sub"]) == MASTER_USER_ID and payload.get("role") == "superadmin":
|
|
return _make_ghost_superadmin()
|
|
user = db.query(User).filter(User.id == int(payload["sub"]), User.is_active == True).first()
|
|
if not user:
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
|
|
return user
|
|
|
|
|
|
def _has_perm(user: User, perm: str) -> bool:
|
|
"""Superadmins always have every permission."""
|
|
if user.role == "superadmin":
|
|
return True
|
|
return bool(getattr(user, perm, False))
|
|
|
|
|
|
def require_manager(user: User = Depends(get_current_user)) -> User:
|
|
"""Allow anyone with dashboard access (formerly role-based manager check)."""
|
|
if not _has_perm(user, "perm_access_dashboard"):
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Dashboard access required")
|
|
return user
|
|
|
|
|
|
def require_staff_manager(user: User = Depends(get_current_user)) -> User:
|
|
if not _has_perm(user, "perm_manage_staff"):
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Staff management permission required")
|
|
return user
|
|
|
|
|
|
def require_reports(user: User = Depends(get_current_user)) -> User:
|
|
if not _has_perm(user, "perm_view_reports"):
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Reports permission required")
|
|
return user
|
|
|
|
|
|
def require_menu_manager(user: User = Depends(get_current_user)) -> User:
|
|
if not _has_perm(user, "perm_manage_menu"):
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Menu management permission required")
|
|
return user
|
|
|
|
|
|
def require_settings_manager(user: User = Depends(get_current_user)) -> User:
|
|
if not _has_perm(user, "perm_manage_settings"):
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Settings management permission required")
|
|
return user
|
|
|
|
|
|
def require_kds(user: User = Depends(get_current_user)) -> User:
|
|
if not _has_perm(user, "perm_access_kds"):
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Δεν έχετε πρόσβαση στο KDS")
|
|
return user
|
|
|
|
|
|
def require_tables_manager(user: User = Depends(get_current_user)) -> User:
|
|
if not _has_perm(user, "perm_manage_tables"):
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Tables management permission required")
|
|
return user
|
|
|
|
|
|
def require_superadmin(user: User = Depends(get_current_user)) -> User:
|
|
if user.role != "superadmin":
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Superadmin access required")
|
|
return user
|
|
|
|
|
|
# Kept for any callers that still use the old name
|
|
require_sysadmin = require_superadmin
|