Files
service-finder/backend/app/api/v1/endpoints/admin.py
2026-03-22 11:02:05 +00:00

361 lines
12 KiB
Python
Executable File

# /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
}