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)