# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/admin.py from fastapi import APIRouter, Depends, HTTPException, status, Body 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 import SecurityAuditLog, OperationalLog # JAVÍTVA: Ezek a modellek a security.py-ból jönnek (ha ott vannak) from app.models 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 } @router.get("/ping", tags=["Admin Test"]) async def admin_ping( current_user: User = Depends(deps.get_current_admin) ): """ Egyszerű ping végpont admin jogosultság ellenőrzéséhez. """ return { "message": "Admin felület aktív", "role": current_user.role.value if hasattr(current_user.role, "value") else current_user.role } @router.post("/users/{user_id}/ban", tags=["Admin Security"]) async def ban_user( user_id: int, reason: str = Body(..., embed=True), current_admin: User = Depends(deps.get_current_admin), db: AsyncSession = Depends(deps.get_db) ): """ Felhasználó tiltása (Ban Hammer). - Megkeresi a usert (identity.users táblában). - Ha nincs -> 404 - Ha a user.role == superadmin -> 403 (Saját magát/másik admint ne tiltson le). - Állítja be a tiltást (is_active = False). - Audit logba rögzíti a reason-t. """ from sqlalchemy import select # 1. Keresd meg a usert stmt = select(User).where(User.id == user_id) result = await db.execute(stmt) user = result.scalar_one_or_none() if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"User not found with ID: {user_id}" ) # 2. Ellenőrizd, hogy nem superadmin-e if user.role == UserRole.superadmin: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Cannot ban a superadmin user" ) # 3. Tiltás beállítása user.is_active = False # Opcionálisan: banned_until mező kitöltése, ha létezik a modellben # user.banned_until = datetime.now() + timedelta(days=30) # 4. Audit log létrehozása audit_log = SecurityAuditLog( user_id=current_admin.id, action="ban_user", target_user_id=user_id, details=f"User banned. Reason: {reason}", is_critical=True, ip_address="admin_api" ) db.add(audit_log) await db.commit() return { "status": "success", "message": f"User {user_id} banned successfully.", "reason": reason } @router.post("/marketplace/services/{staging_id}/approve", tags=["Marketplace Moderation"]) async def approve_staged_service( staging_id: int, current_admin: User = Depends(deps.get_current_admin), db: AsyncSession = Depends(deps.get_db) ): """ Szerviz jóváhagyása a Piactéren (Kék Pipa). - Megkeresi a marketplace.service_staging rekordot. - Ha nincs -> 404 - Állítja a validation_level-t 100-ra, a status-t 'approved'-ra. """ from sqlalchemy import select from app.models.staged_data import ServiceStaging stmt = select(ServiceStaging).where(ServiceStaging.id == staging_id) result = await db.execute(stmt) staging = result.scalar_one_or_none() if not staging: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Service staging record not found with ID: {staging_id}" ) # Jóváhagyás staging.validation_level = 100 staging.status = "approved" # Audit log audit_log = SecurityAuditLog( user_id=current_admin.id, action="approve_service", target_staging_id=staging_id, details=f"Service staging approved: {staging.service_name}", is_critical=False, ip_address="admin_api" ) db.add(audit_log) await db.commit() return { "status": "success", "message": f"Service staging {staging_id} approved.", "service_name": staging.service_name }