"""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"], }