teljes backend_mentés

This commit is contained in:
Roo
2026-03-22 18:59:27 +00:00
parent 5d44339f21
commit 5d96b00f81
34 changed files with 2575 additions and 977 deletions

View File

@@ -4,7 +4,7 @@ from app.api.v1.endpoints import (
auth, catalog, assets, organizations, documents,
services, admin, expenses, evidence, social, security,
billing, finance_admin, analytics, vehicles, system_parameters,
gamification
gamification, translations
)
api_router = APIRouter()
@@ -25,4 +25,5 @@ api_router.include_router(finance_admin.router, prefix="/finance/issuers", tags=
api_router.include_router(analytics.router, prefix="/analytics", tags=["Analytics"])
api_router.include_router(vehicles.router, prefix="/vehicles", tags=["Vehicles"])
api_router.include_router(system_parameters.router, prefix="/system/parameters", tags=["System Parameters"])
api_router.include_router(gamification.router, prefix="/gamification", tags=["Gamification"])
api_router.include_router(gamification.router, prefix="/gamification", tags=["Gamification"])
api_router.include_router(translations.router, prefix="/translations", tags=["i18n"])

View File

@@ -1,5 +1,5 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/admin.py
from fastapi import APIRouter, Depends, HTTPException, status, Body
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
@@ -358,4 +358,197 @@ async def approve_staged_service(
"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
}

View File

@@ -2,33 +2,56 @@ from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.services.asset_service import AssetService
from app.api import deps
from typing import List
router = APIRouter()
# Secured endpoint: Closed premium ecosystem
@router.get("/makes", response_model=List[str])
async def list_makes(db: AsyncSession = Depends(get_db)):
async def list_makes(
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_user)
):
"""1. Szint: Márkák listázása."""
return await AssetService.get_makes(db)
# Secured endpoint: Closed premium ecosystem
@router.get("/models", response_model=List[str])
async def list_models(make: str, db: AsyncSession = Depends(get_db)):
async def list_models(
make: str,
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_user)
):
"""2. Szint: Típusok listázása egy adott márkához."""
models = await AssetService.get_models(db, make)
if not models:
raise HTTPException(status_code=404, detail="Márka nem található vagy nincsenek típusok.")
return models
# Secured endpoint: Closed premium ecosystem
@router.get("/generations", response_model=List[str])
async def list_generations(make: str, model: str, db: AsyncSession = Depends(get_db)):
async def list_generations(
make: str,
model: str,
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_user)
):
"""3. Szint: Generációk/Évjáratok listázása."""
generations = await AssetService.get_generations(db, make, model)
if not generations:
raise HTTPException(status_code=404, detail="Nincs generációs adat ehhez a típushoz.")
return generations
# Secured endpoint: Closed premium ecosystem
@router.get("/engines")
async def list_engines(make: str, model: str, gen: str, db: AsyncSession = Depends(get_db)):
async def list_engines(
make: str,
model: str,
gen: str,
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_user)
):
"""4. Szint: Motorváltozatok és technikai specifikációk."""
engines = await AssetService.get_engines(db, make, model, gen)
if not engines:

View File

@@ -3,10 +3,16 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.schemas.social import ServiceProviderCreate, ServiceProviderResponse
from app.services.social_service import create_service_provider
from app.api import deps
router = APIRouter()
# Secured endpoint: Closed premium ecosystem
@router.post("/", response_model=ServiceProviderResponse)
async def add_provider(provider_data: ServiceProviderCreate, db: AsyncSession = Depends(get_db)):
user_id = 2
async def add_provider(
provider_data: ServiceProviderCreate,
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_user)
):
user_id = current_user.id
return await create_service_provider(db, provider_data, user_id)

View File

@@ -1,24 +1,90 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/search.py
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
from sqlalchemy import select, func, or_
from sqlalchemy.orm import selectinload
from app.db.session import get_db
from app.api.deps import get_current_user
from app.models.marketplace.organization import Organization # JAVÍTVA
from app.models.marketplace.organization import Organization, Branch
from geoalchemy2 import WKTElement
from typing import Optional
router = APIRouter()
@router.get("/match")
async def match_service(lat: float, lng: float, radius: int = 20, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
# PostGIS alapú keresés a fleet.branches táblában (a régi locations helyett)
query = text("""
SELECT o.id, o.name, b.city,
ST_Distance(b.location, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography) / 1000 as distance
FROM fleet.organizations o
JOIN fleet.branches b ON o.id = b.organization_id
WHERE o.is_active = True AND b.is_active = True
AND ST_DWithin(b.location, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography, :r * 1000)
ORDER BY distance ASC
""")
result = await db.execute(query, {"lat": lat, "lng": lng, "r": radius})
return {"results": [dict(row._mapping) for row in result.fetchall()]}
async def match_service(
lat: Optional[float] = None,
lng: Optional[float] = None,
radius_km: float = 20.0,
sort_by: str = "distance",
db: AsyncSession = Depends(get_db),
current_user = Depends(get_current_user)
):
"""
Geofencing keresőmotor PostGIS segítségével.
Ha nincs megadva lat/lng, akkor nem alkalmazunk távolságszűrést.
"""
# Alap lekérdezés: aktív szervezetek és telephelyek
query = select(
Organization.id,
Organization.name,
Branch.city,
Branch.branch_rating,
Branch.location
).join(
Branch, Organization.id == Branch.organization_id
).where(
Organization.is_active == True,
Branch.is_deleted == False
)
# Távolság számítás és szűrés, ha van koordináta
if lat is not None and lng is not None:
# WKT pont létrehozása a felhasználó helyéhez
user_location = WKTElement(f'POINT({lng} {lat})', srid=4326)
# Távolság kiszámítása méterben (ST_DistanceSphere)
distance_col = func.ST_DistanceSphere(Branch.location, user_location).label("distance_meters")
query = query.add_columns(distance_col)
# Szűrés a sugárra (ST_DWithin) - a távolság méterben, radius_km * 1000
query = query.where(
func.ST_DWithin(Branch.location, user_location, radius_km * 1000)
)
else:
# Ha nincs koordináta, ne legyen distance oszlop
distance_col = None
# Rendezés a sort_by paraméter alapján
if sort_by == "distance" and lat is not None and lng is not None:
query = query.order_by(distance_col.asc())
elif sort_by == "rating":
query = query.order_by(Branch.branch_rating.desc())
elif sort_by == "price":
# Jelenleg nincs ár információ, ezért rendezés alapértelmezettként (pl. név)
query = query.order_by(Organization.name.asc())
else:
# Alapértelmezett rendezés: távolság, ha van, különben név
if distance_col is not None:
query = query.order_by(distance_col.asc())
else:
query = query.order_by(Organization.name.asc())
# Lekérdezés végrehajtása
result = await db.execute(query)
rows = result.fetchall()
# Eredmények formázása
results = []
for row in rows:
row_dict = {
"id": row.id,
"name": row.name,
"city": row.city,
"rating": row.branch_rating,
}
if lat is not None and lng is not None:
row_dict["distance_km"] = round(row.distance_meters / 1000, 2) if row.distance_meters else None
results.append(row_dict)
return {"results": results}

View File

@@ -1,16 +1,28 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.api import deps
# ITT A JAVÍTÁS: A példányt importáljuk, nem a régi függvényeket
from app.services.social_service import social_service
router = APIRouter()
# Secured endpoint: Closed premium ecosystem
@router.get("/leaderboard")
async def read_leaderboard(limit: int = 10, db: AsyncSession = Depends(get_db)):
async def read_leaderboard(
limit: int = 10,
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_user)
):
return await social_service.get_leaderboard(db, limit)
# Secured endpoint: Closed premium ecosystem
@router.post("/vote/{provider_id}")
async def provider_vote(provider_id: int, vote_value: int, db: AsyncSession = Depends(get_db)):
user_id = 2
async def provider_vote(
provider_id: int,
vote_value: int,
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_user)
):
user_id = current_user.id
return await social_service.vote_for_provider(db, user_id, provider_id, vote_value)

View File

@@ -0,0 +1,71 @@
"""
Nyilvános i18n API végpont a frontend számára.
Autentikációt NEM igényel, mivel a fordítások nyilvánosak.
"""
import json
import os
from fastapi import APIRouter, HTTPException, Path
from fastapi.responses import JSONResponse
from typing import Dict, Any
router = APIRouter()
# A statikus JSON fájlok elérési útja
LOCALES_DIR = os.path.join(os.path.dirname(__file__), "../../../static/locales")
def load_locale(lang: str) -> Dict[str, Any]:
"""Betölti a nyelvi JSON fájlt, ha nem létezik, fallback angol."""
file_path = os.path.join(LOCALES_DIR, f"{lang}.json")
fallback_path = os.path.join(LOCALES_DIR, "en.json")
if not os.path.exists(file_path):
# Ha a kért nyelv nem létezik, próbáljuk meg az angolt
if lang != "en" and os.path.exists(fallback_path):
file_path = fallback_path
else:
raise HTTPException(status_code=404, detail=f"Language '{lang}' not found")
try:
with open(file_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error loading translation file: {str(e)}")
@router.get("/{lang}", response_model=Dict[str, Any])
async def get_translations(
lang: str = Path(..., description="Nyelvkód, pl. 'hu', 'en', 'de'", min_length=2, max_length=5)
):
"""
Visszaadja a teljes fordításcsomagot a kért nyelvhez.
- Ha a nyelv nem létezik, 404 hibát dob.
- Ha a fájl sérült, 500 hibát dob.
- A válasz egy JSON objektum, amelyben a kulcsok hierarchikusak.
"""
translations = load_locale(lang)
return translations
@router.get("/{lang}/{key:path}")
async def get_translation_by_key(
lang: str = Path(..., description="Nyelvkód"),
key: str = Path(..., description="Pontokkal elválasztott kulcs, pl. 'AUTH.LOGIN.TITLE'")
):
"""
Visszaadja a fordításcsomag egy adott kulcsához tartozó értéket.
- Ha a kulcs nem található, 404 hibát dob.
- Támogatja a hierarchikus kulcsokat (pl. 'AUTH.LOGIN.TITLE').
"""
translations = load_locale(lang)
# Kulcs felbontása
parts = key.split('.')
current = translations
for part in parts:
if isinstance(current, dict) and part in current:
current = current[part]
else:
raise HTTPException(status_code=404, detail=f"Translation key '{key}' not found for language '{lang}'")
# Ha a current egy szótár, akkor azt adjuk vissza (részleges fa)
# Ha sztring, akkor azt
return {key: current}