Includes all work to date: - local_backend: FastAPI backend with products, orders, tables, shifts, cloud sync - manager_dashboard: React manager UI with product/category management, reports, settings - waiter_pwa: React PWA for waiter devices - Category reparent endpoint and UI - Waiter domain: local_ip sent on heartbeat, waiter_domain persisted from cloud response - QR code modal in AppInfoTab for waiter domain - Product form: number input spinners removed, category pre-selected on new product - Category row: count badge moved to far right Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
117 lines
4.0 KiB
Python
117 lines
4.0 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.orm import Session
|
|
from pydantic import BaseModel, EmailStr
|
|
from typing import Optional
|
|
import bcrypt
|
|
|
|
from database import get_db
|
|
from models.user import User
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class SetupStatusResponse(BaseModel):
|
|
needs_setup: bool
|
|
|
|
|
|
class SetupInitRequest(BaseModel):
|
|
username: str
|
|
password: str
|
|
email: Optional[str] = None
|
|
full_name: Optional[str] = None
|
|
venue_type: Optional[str] = None
|
|
venue_name: Optional[str] = None
|
|
pin: Optional[str] = None
|
|
|
|
|
|
class SetupInitResponse(BaseModel):
|
|
ok: bool
|
|
|
|
|
|
class SecurityConfigResponse(BaseModel):
|
|
login_method: str
|
|
autofill_username: bool
|
|
|
|
|
|
@router.get("/security-config", response_model=SecurityConfigResponse)
|
|
def security_config(db: Session = Depends(get_db)):
|
|
"""Public endpoint — returns only the security settings needed by the login page."""
|
|
from models.settings import PosSettings
|
|
rows = {r.key: r.value for r in db.query(PosSettings).filter(
|
|
PosSettings.key.in_(["security.login_method", "security.autofill_username"])
|
|
).all()}
|
|
return SecurityConfigResponse(
|
|
login_method=rows.get("security.login_method", "password"),
|
|
autofill_username=rows.get("security.autofill_username", "true") == "true",
|
|
)
|
|
|
|
|
|
@router.get("/status", response_model=SetupStatusResponse)
|
|
def setup_status(db: Session = Depends(get_db)):
|
|
has_manager = db.query(User).filter(
|
|
User.role.in_(["manager", "sysadmin"]),
|
|
User.is_active == True,
|
|
).first()
|
|
return SetupStatusResponse(needs_setup=has_manager is None)
|
|
|
|
|
|
@router.post("/init", response_model=SetupInitResponse)
|
|
def setup_init(body: SetupInitRequest, db: Session = Depends(get_db)):
|
|
has_manager = db.query(User).filter(
|
|
User.role.in_(["manager", "sysadmin"]),
|
|
User.is_active == True,
|
|
).first()
|
|
if has_manager:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail="Setup already completed — a manager account already exists.",
|
|
)
|
|
|
|
if not body.username.strip():
|
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Username is required.")
|
|
if len(body.password) < 6:
|
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Password must be at least 6 characters.")
|
|
|
|
existing = db.query(User).filter(User.username == body.username.strip()).first()
|
|
if existing:
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Username already taken.")
|
|
|
|
password_hash = bcrypt.hashpw(body.password.encode(), bcrypt.gensalt()).decode()
|
|
|
|
raw_pin = body.pin if (body.pin and body.pin.isdigit() and 4 <= len(body.pin) <= 6) else "0000"
|
|
pin_hash = bcrypt.hashpw(raw_pin.encode(), bcrypt.gensalt()).decode()
|
|
|
|
user = User(
|
|
username=body.username.strip(),
|
|
pin_hash=pin_hash,
|
|
password_hash=password_hash,
|
|
email=body.email,
|
|
full_name=body.full_name,
|
|
role="manager",
|
|
is_active=True,
|
|
)
|
|
db.add(user)
|
|
|
|
# Persist venue settings if provided
|
|
if body.venue_name or body.venue_type:
|
|
from models.settings import PosSettings
|
|
from datetime import datetime, timezone
|
|
now = datetime.now(timezone.utc)
|
|
if body.venue_name:
|
|
setting = db.query(PosSettings).filter(PosSettings.key == "venue.name").first()
|
|
if setting:
|
|
setting.value = body.venue_name
|
|
setting.updated_at = now
|
|
else:
|
|
db.add(PosSettings(key="venue.name", value=body.venue_name, updated_at=now))
|
|
if body.venue_type:
|
|
setting = db.query(PosSettings).filter(PosSettings.key == "venue.type").first()
|
|
if setting:
|
|
setting.value = body.venue_type
|
|
setting.updated_at = now
|
|
else:
|
|
db.add(PosSettings(key="venue.type", value=body.venue_type, updated_at=now))
|
|
|
|
db.commit()
|
|
return SetupInitResponse(ok=True)
|