554 lines
19 KiB
Python
Executable File
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
|
|
} |