Files
service-finder/backend/app/api/v1/endpoints/admin.py
2026-03-22 18:59:27 +00:00

554 lines
19 KiB
Python
Executable File

# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/admin.py
from fastapi import APIRouter, Depends, HTTPException, status, Body, BackgroundTasks
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
}
# ==================== EPIC 10: ADMIN FRONTEND API ENDPOINTS ====================
from app.workers.service.validation_pipeline import ValidationPipeline
from app.models.marketplace.service import ServiceProfile
from app.models.gamification.gamification import GamificationProfile
class LocationUpdate(BaseModel):
latitude: float = Field(..., ge=-90, le=90)
longitude: float = Field(..., ge=-180, le=180)
class PenaltyRequest(BaseModel):
penalty_level: int = Field(..., ge=-10, le=-1, description="Negatív szint (-1 a legkisebb, -10 a legnagyobb büntetés)")
reason: str = Field(..., min_length=5, max_length=500)
@router.post("/services/{service_id}/trigger-ai", tags=["AI Pipeline"])
async def trigger_ai_pipeline(
service_id: int,
background_tasks: BackgroundTasks,
current_admin: User = Depends(deps.get_current_admin),
db: AsyncSession = Depends(deps.get_db)
):
"""
AI Pipeline manuális indítása egy adott szerviz profilra.
A végpont azonnal visszatér, és a validációt háttérfeladatként futtatja.
"""
# Ellenőrizzük, hogy létezik-e a szerviz profil
stmt = select(ServiceProfile).where(ServiceProfile.id == service_id)
result = await db.execute(stmt)
profile = result.scalar_one_or_none()
if not profile:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Service profile not found with ID: {service_id}"
)
# Háttérfeladat hozzáadása
background_tasks.add_task(run_validation_pipeline, service_id)
# Audit log
audit_log = SecurityAuditLog(
user_id=current_admin.id,
action="trigger_ai_pipeline",
target_service_id=service_id,
details=f"AI pipeline manually triggered for service {service_id}",
is_critical=False,
ip_address="admin_api"
)
db.add(audit_log)
await db.commit()
return {
"status": "success",
"message": f"AI pipeline started for service {service_id}",
"service_name": profile.service_name,
"note": "Validation runs in background, check logs for results."
}
async def run_validation_pipeline(profile_id: int):
"""Háttérfeladat a ValidationPipeline futtatásához."""
try:
pipeline = ValidationPipeline()
success = await pipeline.run(profile_id)
logger = logging.getLogger("Service-AI-Pipeline")
if success:
logger.info(f"Pipeline successful for profile {profile_id}")
else:
logger.warning(f"Pipeline failed for profile {profile_id}")
except Exception as e:
logger.error(f"Pipeline error for profile {profile_id}: {e}")
@router.patch("/services/{service_id}/location", tags=["Service Management"])
async def update_service_location(
service_id: int,
location: LocationUpdate,
current_admin: User = Depends(deps.get_current_admin),
db: AsyncSession = Depends(deps.get_db)
):
"""
Szerviz térképes mozgatása (Koordináta frissítés).
A Nuxt Leaflet térkép drag-and-drop funkciójához használható.
"""
stmt = select(ServiceProfile).where(ServiceProfile.id == service_id)
result = await db.execute(stmt)
profile = result.scalar_one_or_none()
if not profile:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Service profile not found with ID: {service_id}"
)
# Frissítjük a koordinátákat
profile.latitude = location.latitude
profile.longitude = location.longitude
profile.updated_at = datetime.now()
# Audit log
audit_log = SecurityAuditLog(
user_id=current_admin.id,
action="update_service_location",
target_service_id=service_id,
details=f"Service location updated to lat={location.latitude}, lon={location.longitude}",
is_critical=False,
ip_address="admin_api"
)
db.add(audit_log)
await db.commit()
return {
"status": "success",
"message": f"Service location updated for {service_id}",
"latitude": location.latitude,
"longitude": location.longitude
}
@router.patch("/users/{user_id}/penalty", tags=["Gamification Admin"])
async def apply_gamification_penalty(
user_id: int,
penalty: PenaltyRequest,
current_admin: User = Depends(deps.get_current_admin),
db: AsyncSession = Depends(deps.get_db)
):
"""
Gamification büntetés kiosztása egy felhasználónak.
Negatív szintek alkalmazása a frissen létrehozott Gamification rendszerben.
"""
# Ellenőrizzük, hogy létezik-e a felhasználó
user_stmt = select(User).where(User.id == user_id)
user_result = await db.execute(user_stmt)
user = 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}"
)
# Megkeressük a felhasználó gamification profilját (vagy létrehozzuk)
gamification_stmt = select(GamificationProfile).where(GamificationProfile.user_id == user_id)
gamification_result = await db.execute(gamification_stmt)
gamification = gamification_result.scalar_one_or_none()
if not gamification:
# Ha nincs profil, létrehozzuk alapértelmezett értékekkel
gamification = GamificationProfile(
user_id=user_id,
level=0,
xp=0,
reputation_score=100,
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(gamification)
await db.flush()
# Alkalmazzuk a büntetést (negatív szint módosítása)
# A level mező lehet negatív is a büntetések miatt
new_level = gamification.level + penalty.penalty_level
gamification.level = new_level
gamification.updated_at = datetime.now()
# Audit log
audit_log = SecurityAuditLog(
user_id=current_admin.id,
action="apply_gamification_penalty",
target_user_id=user_id,
details=f"Gamification penalty applied: level change {penalty.penalty_level}, reason: {penalty.reason}",
is_critical=False,
ip_address="admin_api"
)
db.add(audit_log)
await db.commit()
return {
"status": "success",
"message": f"Gamification penalty applied to user {user_id}",
"user_id": user_id,
"penalty_level": penalty.penalty_level,
"new_level": new_level,
"reason": penalty.reason
}