93 lines
3.3 KiB
Python
93 lines
3.3 KiB
Python
|
|
"""Admin router: login, sync endpoints, sync log."""
|
||
|
|
from fastapi import APIRouter, 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.cache import cache
|
||
|
|
|
||
|
|
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(
|
||
|
|
user: AdminUser = Depends(get_current_user),
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
) -> Any:
|
||
|
|
"""Trigger a manual equipment sync (admin only)."""
|
||
|
|
sync_service = SyncService(db)
|
||
|
|
result = await sync_service.run_sync()
|
||
|
|
return SyncTriggerResponse(sync_id=result["sync_id"], status=result["status"])
|
||
|
|
|
||
|
|
|
||
|
|
@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"],
|
||
|
|
}
|