# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/admin.py from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, text, delete from typing import List, Any, Dict, Optional from datetime import datetime, timedelta from app.api import deps from app.models.identity import User, UserRole # JAVÍTVA: Központi import from app.models.system import SystemParameter, ParameterScope from app.services.system_service import system_service # JAVÍTVA: Security audit modellek from app.models.audit import SecurityAuditLog, OperationalLog # JAVÍTVA: Ezek a modellek a security.py-ból jönnek (ha ott vannak) from app.models.security import PendingAction, ActionStatus from app.services.security_service import security_service from app.services.translation_service import TranslationService from app.services.odometer_service import OdometerService from pydantic import BaseModel, Field from typing import Optional as Opt class ConfigUpdate(BaseModel): key: str value: Any scope_level: ParameterScope = ParameterScope.GLOBAL scope_id: Optional[str] = None category: str = "general" router = APIRouter() async def check_admin_access(current_user: User = Depends(deps.get_current_active_user)): """ Csak Admin vagy Superadmin. """ if current_user.role not in [UserRole.admin, UserRole.superadmin]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Sentinel jogosultság szükséges!" ) return current_user @router.get("/health-monitor", tags=["Sentinel Monitoring"]) async def get_system_health( db: AsyncSession = Depends(deps.get_db), admin: User = Depends(check_admin_access) ): stats = {} # Adatbázis statisztikák (Nyers SQL marad, mert hatékony) user_stats = await db.execute(text("SELECT subscription_plan, count(*) FROM identity.users GROUP BY subscription_plan")) stats["user_distribution"] = {row[0]: row[1] for row in user_stats} asset_count = await db.execute(text("SELECT count(*) FROM vehicle.assets")) stats["total_assets"] = asset_count.scalar() org_count = await db.execute(text("SELECT count(*) FROM fleet.organizations")) stats["total_organizations"] = org_count.scalar() # JAVÍTVA: Biztonsági státusz az új SecurityAuditLog alapján day_ago = datetime.now() - timedelta(days=1) crit_logs = await db.execute( select(func.count(SecurityAuditLog.id)) .where( SecurityAuditLog.is_critical == True, SecurityAuditLog.created_at >= day_ago ) ) stats["critical_alerts_24h"] = crit_logs.scalar() or 0 return stats @router.get("/pending-actions", response_model=List[Any], tags=["Sentinel Security"]) async def list_pending_actions( db: AsyncSession = Depends(deps.get_db), admin: User = Depends(check_admin_access) ): stmt = select(PendingAction).where(PendingAction.status == ActionStatus.pending) result = await db.execute(stmt) return result.scalars().all() @router.post("/approve/{action_id}", tags=["Sentinel Security"]) async def approve_action( action_id: int, db: AsyncSession = Depends(deps.get_db), admin: User = Depends(check_admin_access) ): try: await security_service.approve_action(db, admin.id, action_id) return {"status": "success", "message": "Művelet végrehajtva."} except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @router.get("/parameters", tags=["Dynamic Configuration"]) async def list_all_parameters( db: AsyncSession = Depends(deps.get_db), admin: User = Depends(check_admin_access) ): result = await db.execute(select(SystemParameter)) return result.scalars().all() @router.post("/parameters", tags=["Dynamic Configuration"]) async def set_parameter( config: ConfigUpdate, db: AsyncSession = Depends(deps.get_db), admin: User = Depends(check_admin_access) ): query = text(""" INSERT INTO system.system_parameters (key, value, scope_level, scope_id, category, last_modified_by) VALUES (:key, :val, :sl, :sid, :cat, :user) ON CONFLICT (key, scope_level, scope_id) DO UPDATE SET value = EXCLUDED.value, category = EXCLUDED.category, last_modified_by = EXCLUDED.last_modified_by, updated_at = now() """) await db.execute(query, { "key": config.key, "val": config.value, "sl": config.scope_level, "sid": config.scope_id, "cat": config.category, "user": admin.email }) await db.commit() return {"status": "success", "message": f"'{config.key}' frissítve."} @router.get("/parameters/scoped", tags=["Dynamic Configuration"]) async def get_scoped_parameter( key: str, user_id: Optional[str] = None, region_id: Optional[str] = None, country_code: Optional[str] = None, db: AsyncSession = Depends(deps.get_db), admin: User = Depends(check_admin_access) ): """ Hierarchikus paraméterlekérdezés a következő prioritással: User > Region > Country > Global. """ value = await system_service.get_scoped_parameter( db, key, user_id, region_id, country_code, default=None ) if value is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Paraméter '{key}' nem található a megadott scope-okban." ) return {"key": key, "value": value} @router.post("/translations/sync", tags=["System Utilities"]) async def sync_translations_to_json( db: AsyncSession = Depends(deps.get_db), admin: User = Depends(check_admin_access) ): await TranslationService.export_to_json(db) return {"message": "JSON fájlok frissítve."} # ==================== SMART ODOMETER ADMIN API ==================== class OdometerStatsResponse(BaseModel): vehicle_id: int last_recorded_odometer: int last_recorded_date: datetime daily_avg_distance: float estimated_current_odometer: float confidence_score: float manual_override_avg: Opt[float] is_confidence_high: bool = Field(..., description="True ha confidence_score >= threshold") class ManualOverrideRequest(BaseModel): daily_avg: Opt[float] = Field(None, description="Napi átlagos kilométer (km/nap). Ha null, törli a manuális beállítást.") @router.get("/odometer/{vehicle_id}", tags=["Smart Odometer"]) async def get_odometer_stats( vehicle_id: int, db: AsyncSession = Depends(deps.get_db), admin: User = Depends(check_admin_access) ): """ Jármű kilométeróra statisztikáinak lekérése. A rendszer automatikusan frissíti a statisztikákat, ha szükséges. """ # Frissítjük a statisztikákat odometer_state = await OdometerService.update_vehicle_stats(db, vehicle_id) if not odometer_state: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Jármű nem található ID: {vehicle_id}" ) # Confidence threshold lekérése confidence_threshold = await OdometerService.get_system_param( db, 'ODOMETER_CONFIDENCE_THRESHOLD', 0.5 ) return OdometerStatsResponse( vehicle_id=odometer_state.vehicle_id, last_recorded_odometer=odometer_state.last_recorded_odometer, last_recorded_date=odometer_state.last_recorded_date, daily_avg_distance=float(odometer_state.daily_avg_distance), estimated_current_odometer=float(odometer_state.estimated_current_odometer), confidence_score=odometer_state.confidence_score, manual_override_avg=float(odometer_state.manual_override_avg) if odometer_state.manual_override_avg else None, is_confidence_high=odometer_state.confidence_score >= confidence_threshold ) @router.patch("/odometer/{vehicle_id}", tags=["Smart Odometer"]) async def set_odometer_manual_override( vehicle_id: int, request: ManualOverrideRequest, db: AsyncSession = Depends(deps.get_db), admin: User = Depends(check_admin_access) ): """ Adminisztrátori manuális átlag beállítása a kilométeróra becsléshez. Ha a user csal vagy hibás az adat, az admin ezzel felülírhatja az automatikus számítást. """ odometer_state = await OdometerService.set_manual_override( db, vehicle_id, request.daily_avg ) if not odometer_state: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Jármű nem található ID: {vehicle_id}" ) action = "beállítva" if request.daily_avg is not None else "törölve" return { "status": "success", "message": f"Manuális átlag {action}: {request.daily_avg} km/nap", "vehicle_id": vehicle_id, "manual_override_avg": odometer_state.manual_override_avg }