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>
124 lines
4.3 KiB
Python
124 lines
4.3 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
|
|
from services.chat_service import add_user_to_system_group
|
|
|
|
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.perm_access_dashboard == True,
|
|
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.perm_access_dashboard == True,
|
|
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="store_manager",
|
|
is_active=True,
|
|
)
|
|
# Apply store_manager default permissions
|
|
from roles import get_default_permissions
|
|
for field, value in get_default_permissions("store_manager").items():
|
|
setattr(user, field, value)
|
|
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()
|
|
db.refresh(user)
|
|
add_user_to_system_group(db, user.id)
|
|
return SetupInitResponse(ok=True)
|