Files
bonamin 8ba8c95ecd 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>
2026-05-20 14:04:38 +03:00

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)