feat: initial commit — local services (backend + manager dashboard + waiter PWA)
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>
This commit is contained in:
116
local_backend/routers/setup.py
Normal file
116
local_backend/routers/setup.py
Normal file
@@ -0,0 +1,116 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user