feat: bump client-services (accumulated feature work + deploy fixes)
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>
This commit is contained in:
@@ -1,17 +1,22 @@
|
||||
import bcrypt
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from database import get_db
|
||||
from config import settings
|
||||
from models.user import User
|
||||
from schemas.auth import LoginRequest, TokenResponse, UpdateMeRequest
|
||||
from pydantic import BaseModel as _PydanticBase
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class LoginByIdRequest(_PydanticBase):
|
||||
waiter_id: int
|
||||
pin: str
|
||||
from schemas.user import UserOut
|
||||
from routers.deps import get_current_user, make_token, decode_token, blacklist_token
|
||||
from routers.deps import get_current_user, make_token, decode_token, blacklist_token, _make_ghost_superadmin
|
||||
from routers.recovery import try_recovery_code
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -28,7 +33,7 @@ def login_no_auth(body: NoAuthLoginRequest, db: Session = Depends(get_db)):
|
||||
if not setting or setting.value != "none":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="No-auth login is not enabled.")
|
||||
user = db.query(User).filter(User.username == body.username, User.is_active == True).first()
|
||||
if not user or user.role not in ("manager", "sysadmin"):
|
||||
if not user or not user.perm_access_dashboard:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
|
||||
token = make_token(user)
|
||||
return TokenResponse(access_token=token, user=UserOut.model_validate(user))
|
||||
@@ -36,6 +41,18 @@ def login_no_auth(body: NoAuthLoginRequest, db: Session = Depends(get_db)):
|
||||
|
||||
@router.post("/login", response_model=TokenResponse)
|
||||
def login(body: LoginRequest, db: Session = Depends(get_db)):
|
||||
# Break-glass master account — checked before DB, never stored as a real user
|
||||
if (
|
||||
settings.MASTER_USERNAME
|
||||
and settings.MASTER_PASSWORD
|
||||
and body.username == settings.MASTER_USERNAME
|
||||
and body.password == settings.MASTER_PASSWORD
|
||||
):
|
||||
_logger.warning("MASTER LOGIN USED from username=%s", body.username)
|
||||
ghost = _make_ghost_superadmin()
|
||||
token = make_token(ghost)
|
||||
return TokenResponse(access_token=token, user=UserOut.model_validate(ghost))
|
||||
|
||||
user = db.query(User).filter(User.username == body.username, User.is_active == True).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
|
||||
@@ -46,6 +63,10 @@ def login(body: LoginRequest, db: Session = Depends(get_db)):
|
||||
elif body.pin and user.pin_hash:
|
||||
authenticated = bcrypt.checkpw(body.pin.encode(), user.pin_hash.encode())
|
||||
|
||||
# If normal auth failed, check if the submitted value is a recovery code
|
||||
if not authenticated and body.password:
|
||||
authenticated = try_recovery_code(body.password, user, db)
|
||||
|
||||
if not authenticated:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
|
||||
|
||||
@@ -96,9 +117,9 @@ class PublicManagerOut(_PydanticBase):
|
||||
|
||||
@router.get("/managers", response_model=list[PublicManagerOut])
|
||||
def public_manager_list(db: Session = Depends(get_db)):
|
||||
"""Public endpoint — returns active manager/sysadmin accounts for login screen."""
|
||||
"""Public endpoint — returns accounts with dashboard access for login screen."""
|
||||
managers = db.query(User).filter(
|
||||
User.role.in_(["manager", "sysadmin"]),
|
||||
User.perm_access_dashboard == True,
|
||||
User.is_active == True,
|
||||
).all()
|
||||
return [PublicManagerOut(id=m.id, username=m.username, full_name=m.full_name) for m in managers]
|
||||
@@ -121,7 +142,7 @@ class PublicWaiterOut(_BaseModel):
|
||||
def public_waiter_list(db: Session = Depends(get_db)):
|
||||
"""Public endpoint — returns active waiters with on-shift flag. No auth required."""
|
||||
from models.shift import WaiterShift
|
||||
waiters = db.query(User).filter(User.role == "waiter", User.is_active == True).all()
|
||||
waiters = db.query(User).filter(User.perm_access_waiter_app == True, User.is_active == True).all()
|
||||
on_shift_ids = {
|
||||
row.waiter_id
|
||||
for row in db.query(WaiterShift).filter(WaiterShift.ended_at == None).all()
|
||||
@@ -172,3 +193,26 @@ def update_me(body: UpdateMeRequest, db: Session = Depends(get_db), user: User =
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
class WaiterSettingsPayload(_PydanticBase):
|
||||
settings: str # raw JSON string
|
||||
|
||||
|
||||
@router.get("/me/settings")
|
||||
def get_my_settings(user: User = Depends(get_current_user)):
|
||||
"""Return the stored waiter settings JSON blob for the current user."""
|
||||
return {"settings": user.waiter_settings or "{}"}
|
||||
|
||||
|
||||
@router.put("/me/settings")
|
||||
def put_my_settings(body: WaiterSettingsPayload, db: Session = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""Replace the stored waiter settings JSON blob."""
|
||||
import json as _json
|
||||
try:
|
||||
_json.loads(body.settings) # validate it's valid JSON
|
||||
except Exception:
|
||||
raise HTTPException(status_code=422, detail="settings must be valid JSON")
|
||||
user.waiter_settings = body.settings
|
||||
db.commit()
|
||||
return {"settings": user.waiter_settings}
|
||||
|
||||
Reference in New Issue
Block a user