Files
gh-christianlouis-dmarq/backend/app/api/api_v1/endpoints/setup.py
T
2026-05-18 19:17:36 +02:00

196 lines
5.4 KiB
Python

from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request, Security, status
from fastapi.security import HTTPAuthorizationCredentials
from pydantic import BaseModel, EmailStr
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.security import api_key_header, require_admin_auth, security_bearer
from app.models.setting import Setting
router = APIRouter()
# Simple in-memory storage for setup status (for Milestone 1)
setup_status = {
"is_setup_complete": False,
"admin_email": None,
"app_name": "DMARQ",
}
SETUP_COMPLETE_KEY = "setup.is_complete"
SETUP_ADMIN_EMAIL_KEY = "setup.admin_email"
GENERAL_APP_NAME_KEY = "general.app_name"
GENERAL_BASE_URL_KEY = "general.base_url"
def _setting_value(db: Session, key: str) -> Optional[str]:
row = db.query(Setting).filter(Setting.key == key).first()
return row.value if row else None
def _is_true(value: Optional[str]) -> bool:
return str(value).strip().lower() in {"1", "true", "yes", "on"}
def _upsert_setting(
db: Session,
key: str,
value: Optional[str],
*,
description: str,
value_type: str = "string",
category: str = "setup",
) -> None:
row = db.query(Setting).filter(Setting.key == key).first()
if row is None:
db.add(
Setting(
key=key,
value=value,
description=description,
value_type=value_type,
category=category,
)
)
return
row.value = value
row.description = row.description or description
row.value_type = row.value_type or value_type
row.category = row.category or category
def _refresh_setup_status_from_db(db: Session) -> dict:
"""Merge persisted setup state into the legacy in-memory setup status."""
persisted_complete = _is_true(_setting_value(db, SETUP_COMPLETE_KEY))
if persisted_complete:
setup_status["is_setup_complete"] = True
persisted_admin_email = _setting_value(db, SETUP_ADMIN_EMAIL_KEY)
if persisted_admin_email:
setup_status["admin_email"] = persisted_admin_email
persisted_app_name = _setting_value(db, GENERAL_APP_NAME_KEY)
if persisted_app_name:
setup_status["app_name"] = persisted_app_name
return setup_status
def _setup_is_complete(db: Session) -> bool:
return bool(setup_status["is_setup_complete"]) or _is_true(
_setting_value(db, SETUP_COMPLETE_KEY)
)
class SetupStatusResponse(BaseModel):
"""Setup status response"""
is_setup_complete: bool
app_name: str
class AdminSetupRequest(BaseModel):
"""Admin user setup request body"""
email: EmailStr
username: str
password: str
class SystemConfigRequest(BaseModel):
"""System configuration setup request body"""
app_name: str
base_url: str
async def require_setup_write_auth(
request: Request,
db: Session = Depends(get_db),
api_key: Optional[str] = Security(api_key_header),
bearer: Optional[HTTPAuthorizationCredentials] = Security(security_bearer),
) -> dict:
"""Allow unauthenticated first-time setup writes, then require admin auth."""
if not _setup_is_complete(db):
return {"auth_type": "initial_setup"}
return await require_admin_auth(request=request, api_key=api_key, bearer=bearer)
@router.get("/status", response_model=SetupStatusResponse)
async def get_setup_status(db: Session = Depends(get_db)):
"""Get the current setup status"""
current_status = _refresh_setup_status_from_db(db)
return SetupStatusResponse(
is_setup_complete=current_status["is_setup_complete"],
app_name=current_status["app_name"],
)
@router.post("/admin", status_code=201)
async def setup_admin(
request: AdminSetupRequest,
db: Session = Depends(get_db),
_auth: dict = Depends(require_setup_write_auth),
):
"""
Setup admin user during initial system configuration.
For Milestone 1, this simply stores the admin email in memory.
"""
if _setup_is_complete(db):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Setup already completed"
)
# Store admin email
setup_status["admin_email"] = request.email
_upsert_setting(
db,
SETUP_ADMIN_EMAIL_KEY,
request.email,
description="Email address configured during initial setup",
)
db.commit()
return {"message": "Admin user setup completed"}
@router.post("/system", status_code=200)
async def setup_system(
request: SystemConfigRequest,
db: Session = Depends(get_db),
_auth: dict = Depends(require_setup_write_auth),
):
"""
Setup system configuration.
For Milestone 1, this simply stores the app name in memory.
"""
# Store app name
setup_status["app_name"] = request.app_name
setup_status["is_setup_complete"] = True
_upsert_setting(
db,
GENERAL_APP_NAME_KEY,
request.app_name,
description="Application display name shown in the UI",
category="general",
)
_upsert_setting(
db,
GENERAL_BASE_URL_KEY,
request.base_url,
description="Public base URL for this DMARQ instance",
category="general",
)
_upsert_setting(
db,
SETUP_COMPLETE_KEY,
"true",
description="Whether initial setup has been completed",
value_type="boolean",
)
db.commit()
return {"message": "System settings saved successfully"}