teljes backend_mentés
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -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}
|
||||
@@ -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)
|
||||
71
backend/app/api/v1/endpoints/translations.py
Normal file
71
backend/app/api/v1/endpoints/translations.py
Normal 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}
|
||||
Reference in New Issue
Block a user