Files
hms-licht-ton/backend/app/routers/admin.py
T
Agent Zero e66bcc7058 fix: seed admin user on startup and run sync as background task
- Seed/update admin user from ADMIN_USERNAME/ADMIN_PASSWORD settings on
  startup (admin_users table was empty, login was impossible)
- POST /api/admin/sync now returns immediately and runs the equipment
  sync via BackgroundTasks (full sync took minutes and hit nginx 504)
- run_sync accepts optional sync_log_id to reuse a pre-created log entry
- Adapt admin router test to background sync behavior
2026-09-24 21:41:35 +02:00

119 lines
4.4 KiB
Python

"""Admin router: login, sync endpoints, sync log."""
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Response, Query, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Any
from app.database import get_db
from app.models.admin_user import AdminUser
from app.models.sync_log import SyncLog
from app.schemas.auth import LoginRequest, TokenResponse, AdminInfo
from app.schemas.sync import SyncStatus, SyncLogEntry, SyncTriggerResponse
from app.auth import verify_password, create_access_token, get_current_user
from app.services.sync_service import SyncService
from app.database import async_session
from app.cache import cache
from datetime import datetime
router = APIRouter(prefix="/api/admin", tags=["admin"])
@router.post("/login", response_model=TokenResponse)
async def login(
creds: LoginRequest,
response: Response,
db: AsyncSession = Depends(get_db),
) -> Any:
"""Admin login: verify credentials, set JWT in HttpOnly cookie."""
# Rate limiting
rate_key = f"rate:login:{creds.username}"
count = await cache.incr_rate(rate_key, window=60)
if count > 5:
raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Too many login attempts")
result = await db.execute(select(AdminUser).where(AdminUser.username == creds.username))
user = result.scalar_one_or_none()
if not user or not verify_password(creds.password, user.password_hash):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
token = create_access_token({"sub": user.username})
response.set_cookie(
key="hms_admin_token",
value=token,
httponly=True,
secure=True,
samesite="strict",
max_age=86400,
path="/",
)
return TokenResponse(access_token=token, token_type="bearer")
@router.get("/me", response_model=AdminInfo)
async def get_me(user: AdminUser = Depends(get_current_user)) -> Any:
"""Return current authenticated admin user."""
return AdminInfo(username=user.username)
@router.post("/sync", response_model=SyncTriggerResponse)
async def trigger_sync(
background_tasks: BackgroundTasks,
user: AdminUser = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any:
"""Trigger a manual equipment sync in the background (admin only).
Runs the sync via BackgroundTasks so the HTTP request returns immediately
(a full sync of 700+ items incl. image downloads takes minutes and
would otherwise hit proxy timeouts).
Poll GET /api/admin/sync-status for progress.
"""
log_entry = SyncLog(sync_type="equipment", status="running", started_at=datetime.utcnow())
db.add(log_entry)
await db.commit()
await db.refresh(log_entry)
async def _run_sync(sync_id: int) -> None:
async with async_session() as sync_db:
sync_service = SyncService(sync_db)
try:
await sync_service.run_sync(sync_log_id=sync_id)
except Exception as exc: # noqa: BLE001
log = await sync_db.get(SyncLog, sync_id)
if log:
log.status = "failed"
log.error_message = str(exc)
log.completed_at = datetime.utcnow()
await sync_db.commit()
background_tasks.add_task(_run_sync, log_entry.id)
return SyncTriggerResponse(sync_id=log_entry.id, status="running")
@router.get("/sync-status", response_model=SyncStatus)
async def sync_status(
user: AdminUser = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any:
"""Return the latest sync status."""
sync_service = SyncService(db)
return await sync_service.get_last_sync()
@router.get("/sync-log")
async def sync_log(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
user: AdminUser = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any:
"""Return paginated sync log entries (admin only)."""
sync_service = SyncService(db)
result = await sync_service.get_sync_log_paginated(page=page, page_size=page_size)
return {
"items": [SyncLogEntry.model_validate(log) for log in result["items"]],
"total": result["total"],
"page": result["page"],
"page_size": result["page_size"],
"total_pages": result["total_pages"],
}