átlagos kiegészítséek jó sok
This commit is contained in:
@@ -138,4 +138,25 @@ def check_min_rank(role_key: str):
|
||||
detail=f"Alacsony jogosultsági szint. (Elvárt: {required_rank})"
|
||||
)
|
||||
return True
|
||||
return rank_checker
|
||||
return rank_checker
|
||||
|
||||
async def get_current_admin(
|
||||
current_user: User = Depends(get_current_user)
|
||||
) -> User:
|
||||
"""
|
||||
Csak admin/moderátor/superadmin szerepkörrel rendelkező felhasználók számára.
|
||||
"""
|
||||
# A UserRole Enum értékeit használjuk
|
||||
allowed_roles = {
|
||||
UserRole.superadmin,
|
||||
UserRole.admin,
|
||||
UserRole.region_admin,
|
||||
UserRole.country_admin,
|
||||
UserRole.moderator,
|
||||
}
|
||||
if current_user.role not in allowed_roles:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Nincs megfelelő jogosultságod (Admin/Moderátor)!"
|
||||
)
|
||||
return current_user
|
||||
@@ -3,7 +3,8 @@ from fastapi import APIRouter
|
||||
from app.api.v1.endpoints import (
|
||||
auth, catalog, assets, organizations, documents,
|
||||
services, admin, expenses, evidence, social, security,
|
||||
billing, finance_admin, analytics, vehicles
|
||||
billing, finance_admin, analytics, vehicles, system_parameters,
|
||||
gamification
|
||||
)
|
||||
|
||||
api_router = APIRouter()
|
||||
@@ -22,4 +23,6 @@ api_router.include_router(social.router, prefix="/social", tags=["Social & Leade
|
||||
api_router.include_router(security.router, prefix="/security", tags=["Dual Control (Security)"])
|
||||
api_router.include_router(finance_admin.router, prefix="/finance/issuers", tags=["finance-admin"])
|
||||
api_router.include_router(analytics.router, prefix="/analytics", tags=["Analytics"])
|
||||
api_router.include_router(vehicles.router, prefix="/vehicles", tags=["Vehicles"])
|
||||
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"])
|
||||
@@ -1,5 +1,5 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/admin.py
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
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
|
||||
@@ -10,9 +10,9 @@ 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.audit import SecurityAuditLog, OperationalLog
|
||||
from app.models import SecurityAuditLog, OperationalLog
|
||||
# JAVÍTVA: Ezek a modellek a security.py-ból jönnek (ha ott vannak)
|
||||
from app.models.security import PendingAction, ActionStatus
|
||||
from app.models import PendingAction, ActionStatus
|
||||
|
||||
from app.services.security_service import security_service
|
||||
from app.services.translation_service import TranslationService
|
||||
@@ -235,4 +235,127 @@ async def set_odometer_manual_override(
|
||||
"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
|
||||
}
|
||||
@@ -12,7 +12,7 @@ from app.api import deps
|
||||
from app.schemas.analytics import TCOSummaryResponse, TCOErrorResponse
|
||||
from app.services.analytics_service import TCOAnalytics
|
||||
from app.models import Vehicle
|
||||
from app.models.organization import OrganizationMember
|
||||
from app.models.marketplace.organization import OrganizationMember
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/assets.py
|
||||
import uuid
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -8,11 +9,12 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.asset import Asset, AssetCost
|
||||
from app.models import Asset, AssetCost
|
||||
from app.models.identity import User
|
||||
from app.services.cost_service import cost_service
|
||||
from app.services.asset_service import AssetService
|
||||
from app.schemas.asset_cost import AssetCostCreate, AssetCostResponse
|
||||
from app.schemas.asset import AssetResponse
|
||||
from app.schemas.asset import AssetResponse, AssetCreate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -51,4 +53,39 @@ async def list_asset_costs(
|
||||
.limit(limit)
|
||||
)
|
||||
res = await db.execute(stmt)
|
||||
return res.scalars().all()
|
||||
return res.scalars().all()
|
||||
|
||||
|
||||
@router.post("/vehicles", response_model=AssetResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_or_claim_vehicle(
|
||||
payload: AssetCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Új jármű hozzáadása vagy meglévő jármű igénylése a flottához.
|
||||
|
||||
A végpont a következőket végzi:
|
||||
- Ellenőrzi a felhasználó járműlimitjét
|
||||
- Ha a VIN már létezik, tulajdonjog-átvitelt kezdeményez
|
||||
- Ha új, létrehozza a járművet és a kapcsolódó digitális ikreket
|
||||
- XP jutalom adása a felhasználónak
|
||||
"""
|
||||
try:
|
||||
asset = await AssetService.create_or_claim_vehicle(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
org_id=payload.organization_id,
|
||||
vin=payload.vin,
|
||||
license_plate=payload.license_plate,
|
||||
catalog_id=payload.catalog_id
|
||||
)
|
||||
return asset
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f"Vehicle creation error: {e}")
|
||||
raise HTTPException(status_code=500, detail="Belső szerverhiba a jármű létrehozásakor")
|
||||
@@ -1,4 +1,4 @@
|
||||
# backend/app/api/v1/endpoints/auth.py
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/auth.py
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -10,9 +10,23 @@ from app.core.config import settings
|
||||
from app.schemas.auth import UserLiteRegister, Token, UserKYCComplete
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.identity import User # JAVÍTVA: Új központi modell
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/register", status_code=status.HTTP_201_CREATED)
|
||||
async def register(user_in: UserLiteRegister, db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
Regisztráció (Lite fázis) - új felhasználó létrehozása.
|
||||
"""
|
||||
user = await AuthService.register_lite(db, user_in)
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Regisztráció sikeres. Aktivációs e-mail elküldve.",
|
||||
"user_id": user.id,
|
||||
"email": user.email
|
||||
}
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
user = await AuthService.authenticate(db, form_data.username, form_data.password)
|
||||
@@ -34,6 +48,19 @@ async def login(db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordReq
|
||||
access, refresh = create_tokens(data=token_data)
|
||||
return {"access_token": access, "refresh_token": refresh, "token_type": "bearer", "is_active": user.is_active}
|
||||
|
||||
class VerifyEmailRequest(BaseModel):
|
||||
token: str = Field(..., description="Email verification token (UUID)")
|
||||
|
||||
@router.post("/verify-email")
|
||||
async def verify_email(request: VerifyEmailRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
Email megerősítés token alapján.
|
||||
"""
|
||||
success = await AuthService.verify_email(db, request.token)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="Érvénytelen vagy lejárt token.")
|
||||
return {"status": "success", "message": "Email sikeresen megerősítve."}
|
||||
|
||||
@router.post("/complete-kyc")
|
||||
async def complete_kyc(kyc_in: UserKYCComplete, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
user = await AuthService.complete_kyc(db, current_user.id, kyc_in)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# backend/app/api/v1/endpoints/billing.py
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/billing.py
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request, Header
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
@@ -7,8 +7,8 @@ import logging
|
||||
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.models.identity import User, Wallet, UserRole
|
||||
from app.models.audit import FinancialLedger, WalletType
|
||||
from app.models.payment import PaymentIntent, PaymentIntentStatus
|
||||
from app.models import FinancialLedger, WalletType
|
||||
from app.models.marketplace.payment import PaymentIntent, PaymentIntentStatus
|
||||
from app.services.config_service import config
|
||||
from app.services.payment_router import PaymentRouter
|
||||
from app.services.stripe_adapter import stripe_adapter
|
||||
|
||||
@@ -84,4 +84,147 @@ async def get_document_status(
|
||||
):
|
||||
"""Lekérdezhető, hogy a robot végzett-e már a feldolgozással."""
|
||||
# (Itt egy egyszerű lekérdezés a Document táblából a státuszra)
|
||||
pass
|
||||
pass
|
||||
|
||||
|
||||
# RBAC helper function
|
||||
def _check_premium_or_admin(user: User) -> bool:
|
||||
"""Check if user has premium subscription or admin role."""
|
||||
premium_plans = ['PREMIUM', 'PREMIUM_PLUS', 'VIP', 'VIP_PLUS']
|
||||
if user.role == 'admin':
|
||||
return True
|
||||
if hasattr(user, 'subscription_plan') and user.subscription_plan in premium_plans:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@router.post("/scan-instant")
|
||||
async def scan_instant(
|
||||
file: UploadFile = File(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Szinkron végpont (Villámszkenner) - forgalmi/ID dokumentumokhoz.
|
||||
Azonnali OCR feldolgozás és válasz.
|
||||
RBAC: Csak prémium előfizetés vagy admin.
|
||||
"""
|
||||
# RBAC ellenőrzés
|
||||
if not _check_premium_or_admin(current_user):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Prémium előfizetés szükséges a funkcióhoz"
|
||||
)
|
||||
|
||||
try:
|
||||
# 1. Fájl feltöltése MinIO-ba (StorageService segítségével)
|
||||
# Jelenleg mock: feltételezzük, hogy a StorageService.upload_file létezik
|
||||
from app.services.storage_service import StorageService
|
||||
file_url = await StorageService.upload_file(file, prefix="instant_scan")
|
||||
|
||||
# 2. Mock OCR hívás (valós implementációban AiOcrService-t hívnánk)
|
||||
mock_ocr_result = {
|
||||
"plate": "TEST-123",
|
||||
"vin": "TRX12345",
|
||||
"make": "Toyota",
|
||||
"model": "Corolla",
|
||||
"year": 2022,
|
||||
"fuel_type": "petrol",
|
||||
"engine_capacity": 1600
|
||||
}
|
||||
|
||||
# 3. Dokumentum rekord létrehozása system.documents táblában
|
||||
from app.models import Document
|
||||
from datetime import datetime, timezone
|
||||
import uuid
|
||||
|
||||
doc = Document(
|
||||
id=uuid.uuid4(),
|
||||
user_id=current_user.id,
|
||||
original_name=file.filename,
|
||||
file_path=file_url,
|
||||
file_size=file.size,
|
||||
mime_type=file.content_type,
|
||||
status='processed',
|
||||
ocr_data=mock_ocr_result,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
db.add(doc)
|
||||
await db.commit()
|
||||
await db.refresh(doc)
|
||||
|
||||
# 4. Válasz
|
||||
return {
|
||||
"document_id": str(doc.id),
|
||||
"status": "processed",
|
||||
"ocr_result": mock_ocr_result,
|
||||
"file_url": file_url,
|
||||
"message": "Dokumentum sikeresen feldolgozva"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Hiba a dokumentum feldolgozása során: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/upload-async")
|
||||
async def upload_async(
|
||||
file: UploadFile = File(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Aszinkron végpont (Költség/Számla nyelő) - háttérben futó OCR-nek.
|
||||
Azonnali 202 Accepted válasz, pending_ocr státusszal.
|
||||
RBAC: Csak prémium előfizetés vagy admin.
|
||||
"""
|
||||
# RBAC ellenőrzés
|
||||
if not _check_premium_or_admin(current_user):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Prémium előfizetés szükséges a funkcióhoz"
|
||||
)
|
||||
|
||||
try:
|
||||
# 1. Fájl feltöltése MinIO-ba
|
||||
from app.services.storage_service import StorageService
|
||||
file_url = await StorageService.upload_file(file, prefix="async_upload")
|
||||
|
||||
# 2. Dokumentum rekord létrehozása pending_ocr státusszal
|
||||
from app.models import Document
|
||||
from datetime import datetime, timezone
|
||||
import uuid
|
||||
|
||||
doc = Document(
|
||||
id=uuid.uuid4(),
|
||||
user_id=current_user.id,
|
||||
original_name=file.filename,
|
||||
file_path=file_url,
|
||||
file_size=file.size,
|
||||
mime_type=file.content_type,
|
||||
status='pending_ocr', # Fontos: a háttérrobot ezt fogja felvenni
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
db.add(doc)
|
||||
await db.commit()
|
||||
await db.refresh(doc)
|
||||
|
||||
# 3. 202 Accepted válasz
|
||||
return {
|
||||
"document_id": str(doc.id),
|
||||
"status": "pending_ocr",
|
||||
"message": "A dokumentum feltöltve, háttérben történő elemzése megkezdődött.",
|
||||
"file_url": file_url
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Hiba a dokumentum feltöltése során: {str(e)}"
|
||||
)
|
||||
@@ -1,10 +1,10 @@
|
||||
# backend/app/api/v1/endpoints/evidence.py
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/evidence.py
|
||||
from fastapi import APIRouter, UploadFile, File, HTTPException, status, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, text
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.models.identity import User
|
||||
from app.models.asset import Asset # JAVÍTVA: Asset modell
|
||||
from app.models import Asset # JAVÍTVA: Asset modell
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# backend/app/api/v1/endpoints/expenses.py
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/expenses.py
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.models.asset import Asset, AssetCost # JAVÍTVA
|
||||
from app.models import Asset, AssetCost # JAVÍTVA
|
||||
from pydantic import BaseModel
|
||||
from datetime import date
|
||||
|
||||
@@ -18,15 +18,23 @@ class ExpenseCreate(BaseModel):
|
||||
@router.post("/add")
|
||||
async def add_expense(expense: ExpenseCreate, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
stmt = select(Asset).where(Asset.id == expense.asset_id)
|
||||
if not (await db.execute(stmt)).scalar_one_or_none():
|
||||
result = await db.execute(stmt)
|
||||
asset = result.scalar_one_or_none()
|
||||
if not asset:
|
||||
raise HTTPException(status_code=404, detail="Jármű nem található.")
|
||||
|
||||
|
||||
# Determine organization_id from asset
|
||||
organization_id = asset.current_organization_id or asset.owner_org_id
|
||||
if not organization_id:
|
||||
raise HTTPException(status_code=400, detail="Az eszközhez nincs társított szervezet.")
|
||||
|
||||
new_cost = AssetCost(
|
||||
asset_id=expense.asset_id,
|
||||
cost_type=expense.category,
|
||||
amount_local=expense.amount,
|
||||
cost_category=expense.category,
|
||||
amount_net=expense.amount,
|
||||
currency="HUF",
|
||||
date=expense.date,
|
||||
currency_local="HUF"
|
||||
organization_id=organization_id
|
||||
)
|
||||
db.add(new_cost)
|
||||
await db.commit()
|
||||
|
||||
@@ -11,7 +11,7 @@ from typing import List
|
||||
|
||||
from app.api import deps
|
||||
from app.models.identity import User, UserRole
|
||||
from app.models.finance import Issuer
|
||||
from app.models.marketplace.finance import Issuer
|
||||
from app.schemas.finance import IssuerResponse, IssuerUpdate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -1,40 +1,475 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/gamification.py
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Body, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, desc
|
||||
from typing import List
|
||||
from sqlalchemy import select, desc, func, and_
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.identity import User
|
||||
from app.models.gamification import UserStats, PointsLedger
|
||||
from app.services.config_service import config
|
||||
from app.models import UserStats, PointsLedger, LevelConfig, UserContribution, Badge, UserBadge, Season
|
||||
from app.models.system import SystemParameter, ParameterScope
|
||||
from app.models.marketplace.service import ServiceStaging
|
||||
from app.schemas.gamification import SeasonResponse, UserStatResponse, LeaderboardEntry
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# -- SEGÉDFÜGGVÉNY A RENDSZERBEÁLLÍTÁSOKHOZ --
|
||||
async def get_system_param(db: AsyncSession, key: str, default_value):
|
||||
stmt = select(SystemParameter).where(SystemParameter.key == key)
|
||||
res = (await db.execute(stmt)).scalar_one_or_none()
|
||||
return res.value if res else default_value
|
||||
|
||||
@router.get("/my-stats")
|
||||
async def get_my_stats(db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""A bejelentkezett felhasználó aktuális XP-je, szintje és büntetőpontjai."""
|
||||
stmt = select(UserStats).where(UserStats.user_id == current_user.id)
|
||||
stats = (await db.execute(stmt)).scalar_one_or_none()
|
||||
if not stats:
|
||||
return {"total_xp": 0, "current_level": 1, "penalty_points": 0, "services_submitted": 0}
|
||||
return stats
|
||||
|
||||
@router.get("/leaderboard")
|
||||
async def get_leaderboard(
|
||||
limit: int = 10,
|
||||
season_id: Optional[int] = None,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Vezetőlista - globális vagy szezonális"""
|
||||
if season_id:
|
||||
# Szezonális vezetőlista
|
||||
stmt = (
|
||||
select(
|
||||
User.email,
|
||||
func.sum(UserContribution.points_awarded).label("total_points"),
|
||||
func.sum(UserContribution.xp_awarded).label("total_xp")
|
||||
)
|
||||
.join(UserContribution, User.id == UserContribution.user_id)
|
||||
.where(UserContribution.season_id == season_id)
|
||||
.group_by(User.id)
|
||||
.order_by(desc("total_points"))
|
||||
.limit(limit)
|
||||
)
|
||||
else:
|
||||
# Globális vezetőlista
|
||||
stmt = (
|
||||
select(User.email, UserStats.total_xp, UserStats.current_level)
|
||||
.join(UserStats, User.id == UserStats.user_id)
|
||||
.order_by(desc(UserStats.total_xp))
|
||||
.limit(limit)
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
|
||||
if season_id:
|
||||
return [
|
||||
{"user": f"{r[0][:2]}***@{r[0].split('@')[1]}", "points": r[1], "xp": r[2]}
|
||||
for r in result.all()
|
||||
]
|
||||
else:
|
||||
return [
|
||||
{"user": f"{r[0][:2]}***@{r[0].split('@')[1]}", "xp": r[1], "level": r[2]}
|
||||
for r in result.all()
|
||||
]
|
||||
|
||||
@router.get("/seasons")
|
||||
async def get_seasons(
|
||||
active_only: bool = True,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Szezonok listázása"""
|
||||
stmt = select(Season)
|
||||
if active_only:
|
||||
stmt = stmt.where(Season.is_active == True)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
seasons = result.scalars().all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": s.id,
|
||||
"name": s.name,
|
||||
"start_date": s.start_date,
|
||||
"end_date": s.end_date,
|
||||
"is_active": s.is_active
|
||||
}
|
||||
for s in seasons
|
||||
]
|
||||
|
||||
@router.get("/my-contributions")
|
||||
async def get_my_contributions(
|
||||
season_id: Optional[int] = None,
|
||||
limit: int = 50,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Felhasználó hozzájárulásainak listázása"""
|
||||
stmt = select(UserContribution).where(UserContribution.user_id == current_user.id)
|
||||
|
||||
if season_id:
|
||||
stmt = stmt.where(UserContribution.season_id == season_id)
|
||||
|
||||
stmt = stmt.order_by(desc(UserContribution.created_at)).limit(limit)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
contributions = result.scalars().all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": c.id,
|
||||
"contribution_type": c.contribution_type,
|
||||
"entity_type": c.entity_type,
|
||||
"entity_id": c.entity_id,
|
||||
"points_awarded": c.points_awarded,
|
||||
"xp_awarded": c.xp_awarded,
|
||||
"status": c.status,
|
||||
"created_at": c.created_at
|
||||
}
|
||||
for c in contributions
|
||||
]
|
||||
|
||||
@router.get("/season-standings/{season_id}")
|
||||
async def get_season_standings(
|
||||
season_id: int,
|
||||
limit: int = 20,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Szezon állása - top hozzájárulók"""
|
||||
# Aktuális szezon ellenőrzése
|
||||
season_stmt = select(Season).where(Season.id == season_id)
|
||||
season = (await db.execute(season_stmt)).scalar_one_or_none()
|
||||
|
||||
if not season:
|
||||
raise HTTPException(status_code=404, detail="Season not found")
|
||||
|
||||
# Top hozzájárulók lekérdezése
|
||||
stmt = (
|
||||
select(
|
||||
User.email,
|
||||
func.sum(UserContribution.points_awarded).label("total_points"),
|
||||
func.sum(UserContribution.xp_awarded).label("total_xp"),
|
||||
func.count(UserContribution.id).label("contribution_count")
|
||||
)
|
||||
.join(UserContribution, User.id == UserContribution.user_id)
|
||||
.where(
|
||||
and_(
|
||||
UserContribution.season_id == season_id,
|
||||
UserContribution.status == "approved"
|
||||
)
|
||||
)
|
||||
.group_by(User.id)
|
||||
.order_by(desc("total_points"))
|
||||
.limit(limit)
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
standings = result.all()
|
||||
|
||||
# Szezonális jutalmak konfigurációja
|
||||
season_config = await get_system_param(
|
||||
db, "seasonal_competition_config",
|
||||
{
|
||||
"top_contributors_count": 10,
|
||||
"rewards": {
|
||||
"first_place": {"credits": 1000, "badge": "season_champion"},
|
||||
"second_place": {"credits": 500, "badge": "season_runner_up"},
|
||||
"third_place": {"credits": 250, "badge": "season_bronze"},
|
||||
"top_10": {"credits": 100, "badge": "season_elite"}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"season": {
|
||||
"id": season.id,
|
||||
"name": season.name,
|
||||
"start_date": season.start_date,
|
||||
"end_date": season.end_date
|
||||
},
|
||||
"standings": [
|
||||
{
|
||||
"rank": idx + 1,
|
||||
"user": f"{r[0][:2]}***@{r[0].split('@')[1]}",
|
||||
"points": r[1],
|
||||
"xp": r[2],
|
||||
"contributions": r[3],
|
||||
"reward": get_season_reward(idx + 1, season_config)
|
||||
}
|
||||
for idx, r in enumerate(standings)
|
||||
],
|
||||
"config": season_config
|
||||
}
|
||||
|
||||
def get_season_reward(rank: int, config: dict) -> dict:
|
||||
"""Szezonális jutalom meghatározása a rang alapján"""
|
||||
rewards = config.get("rewards", {})
|
||||
|
||||
if rank == 1:
|
||||
return rewards.get("first_place", {})
|
||||
elif rank == 2:
|
||||
return rewards.get("second_place", {})
|
||||
elif rank == 3:
|
||||
return rewards.get("third_place", {})
|
||||
elif rank <= config.get("top_contributors_count", 10):
|
||||
return rewards.get("top_10", {})
|
||||
else:
|
||||
return {}
|
||||
|
||||
@router.get("/self-defense-status")
|
||||
async def get_self_defense_status(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Önvédelmi rendszer státusz lekérdezése"""
|
||||
stmt = select(UserStats).where(UserStats.user_id == current_user.id)
|
||||
stats = (await db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
if not stats:
|
||||
return {"total_xp": 0, "current_level": 1, "penalty_points": 0}
|
||||
return {
|
||||
"penalty_level": 0,
|
||||
"restrictions": [],
|
||||
"recovery_progress": 0,
|
||||
"can_submit_services": True
|
||||
}
|
||||
|
||||
return stats
|
||||
# Önvédelmi büntetések konfigurációja
|
||||
penalty_config = await get_system_param(
|
||||
db, "self_defense_penalties",
|
||||
{
|
||||
"level_minus_1": {"restrictions": ["no_service_submissions"], "duration_days": 7},
|
||||
"level_minus_2": {"restrictions": ["no_service_submissions", "no_reviews"], "duration_days": 30},
|
||||
"level_minus_3": {"restrictions": ["no_service_submissions", "no_reviews", "no_messaging"], "duration_days": 365}
|
||||
}
|
||||
)
|
||||
|
||||
# Büntetési szint meghatározása (egyszerűsített logika)
|
||||
penalty_level = 0
|
||||
if stats.penalty_points >= 1000:
|
||||
penalty_level = -3
|
||||
elif stats.penalty_points >= 500:
|
||||
penalty_level = -2
|
||||
elif stats.penalty_points >= 100:
|
||||
penalty_level = -1
|
||||
|
||||
restrictions = []
|
||||
if penalty_level < 0:
|
||||
level_key = f"level_minus_{abs(penalty_level)}"
|
||||
restrictions = penalty_config.get(level_key, {}).get("restrictions", [])
|
||||
|
||||
return {
|
||||
"penalty_level": penalty_level,
|
||||
"penalty_points": stats.penalty_points,
|
||||
"restrictions": restrictions,
|
||||
"recovery_progress": min(stats.total_xp / 10000 * 100, 100) if penalty_level < 0 else 100,
|
||||
"can_submit_services": "no_service_submissions" not in restrictions
|
||||
}
|
||||
|
||||
@router.get("/leaderboard")
|
||||
async def get_leaderboard(limit: int = 10, db: AsyncSession = Depends(get_db)):
|
||||
"""A 10 legtöbb XP-vel rendelkező felhasználó listája."""
|
||||
# --- AZ ÚJ, DINAMIKUS BEKÜLDŐ VÉGPONT (Gamification 2.0 kompatibilis) ---
|
||||
@router.post("/submit-service")
|
||||
async def submit_new_service(
|
||||
name: str = Body(...),
|
||||
city: str = Body(...),
|
||||
address: str = Body(...),
|
||||
contact_phone: Optional[str] = Body(None),
|
||||
website: Optional[str] = Body(None),
|
||||
description: Optional[str] = Body(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
# 1. Önvédelmi státusz ellenőrzése
|
||||
defense_status = await get_self_defense_status(db, current_user)
|
||||
if not defense_status["can_submit_services"]:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Önvédelmi korlátozás miatt nem küldhetsz be új szerviz adatokat."
|
||||
)
|
||||
|
||||
# 2. Beállítások lekérése az Admin által vezérelt táblából
|
||||
submission_rewards = await get_system_param(
|
||||
db, "service_submission_rewards",
|
||||
{"points": 50, "xp": 100, "social_credits": 10}
|
||||
)
|
||||
|
||||
contribution_config = await get_system_param(
|
||||
db, "contribution_types_config",
|
||||
{
|
||||
"service_submission": {"points": 50, "xp": 100, "weight": 1.0}
|
||||
}
|
||||
)
|
||||
|
||||
# 3. Aktuális szezon lekérdezése
|
||||
season_stmt = select(Season).where(
|
||||
and_(
|
||||
Season.is_active == True,
|
||||
Season.start_date <= datetime.utcnow().date(),
|
||||
Season.end_date >= datetime.utcnow().date()
|
||||
)
|
||||
).limit(1)
|
||||
|
||||
season_result = await db.execute(season_stmt)
|
||||
current_season = season_result.scalar_one_or_none()
|
||||
|
||||
# 4. Felhasználó statisztikák
|
||||
stmt = select(UserStats).where(UserStats.user_id == current_user.id)
|
||||
stats = (await db.execute(stmt)).scalar_one_or_none()
|
||||
user_lvl = stats.current_level if stats else 1
|
||||
|
||||
# 5. Trust score számítás a szint alapján
|
||||
trust_weight = min(20 + (user_lvl * 6), 90)
|
||||
|
||||
# 6. Nyers adat beküldése a Robotoknak (Staging)
|
||||
import hashlib
|
||||
f_print = hashlib.md5(f"{name.lower()}{city.lower()}{address.lower()}".encode()).hexdigest()
|
||||
|
||||
new_staging = ServiceStaging(
|
||||
name=name,
|
||||
city=city,
|
||||
address_line1=address,
|
||||
contact_phone=contact_phone,
|
||||
website=website,
|
||||
description=description,
|
||||
fingerprint=f_print,
|
||||
status="pending",
|
||||
trust_score=trust_weight,
|
||||
submitted_by=current_user.id,
|
||||
raw_data={
|
||||
"submitted_by_user": current_user.id,
|
||||
"user_level": user_lvl,
|
||||
"submitted_at": datetime.utcnow().isoformat()
|
||||
}
|
||||
)
|
||||
db.add(new_staging)
|
||||
await db.flush() # Get the ID
|
||||
|
||||
# 7. UserContribution létrehozása
|
||||
contribution = UserContribution(
|
||||
user_id=current_user.id,
|
||||
season_id=current_season.id if current_season else None,
|
||||
contribution_type="service_submission",
|
||||
entity_type="service_staging",
|
||||
entity_id=new_staging.id,
|
||||
points_awarded=submission_rewards.get("points", 50),
|
||||
xp_awarded=submission_rewards.get("xp", 100),
|
||||
status="pending", # Robot 5 jóváhagyására vár
|
||||
metadata={
|
||||
"service_name": name,
|
||||
"city": city,
|
||||
"staging_id": new_staging.id
|
||||
},
|
||||
created_at=datetime.utcnow()
|
||||
)
|
||||
db.add(contribution)
|
||||
|
||||
# 8. PointsLedger bejegyzés
|
||||
ledger = PointsLedger(
|
||||
user_id=current_user.id,
|
||||
points=submission_rewards.get("points", 50),
|
||||
xp=submission_rewards.get("xp", 100),
|
||||
source_type="service_submission",
|
||||
source_id=new_staging.id,
|
||||
description=f"Szerviz beküldés: {name}",
|
||||
created_at=datetime.utcnow()
|
||||
)
|
||||
db.add(ledger)
|
||||
|
||||
# 9. UserStats frissítése
|
||||
if stats:
|
||||
stats.total_points += submission_rewards.get("points", 50)
|
||||
stats.total_xp += submission_rewards.get("xp", 100)
|
||||
stats.services_submitted += 1
|
||||
stats.updated_at = datetime.utcnow()
|
||||
else:
|
||||
# Ha nincs még UserStats, létrehozzuk
|
||||
stats = UserStats(
|
||||
user_id=current_user.id,
|
||||
total_points=submission_rewards.get("points", 50),
|
||||
total_xp=submission_rewards.get("xp", 100),
|
||||
services_submitted=1,
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow()
|
||||
)
|
||||
db.add(stats)
|
||||
|
||||
try:
|
||||
await db.commit()
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Szerviz beküldve a rendszerbe elemzésre!",
|
||||
"xp_earned": submission_rewards.get("xp", 100),
|
||||
"points_earned": submission_rewards.get("points", 50),
|
||||
"staging_id": new_staging.id,
|
||||
"season_id": current_season.id if current_season else None
|
||||
}
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
raise HTTPException(status_code=400, detail=f"Hiba a beküldés során: {str(e)}")
|
||||
|
||||
|
||||
# --- Gamification 2.0 API végpontok (Frontend/Mobil) ---
|
||||
|
||||
@router.get("/me", response_model=UserStatResponse)
|
||||
async def get_my_gamification_stats(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Visszaadja a bejelentkezett felhasználó aktuális statisztikáit.
|
||||
Ha nincs rekord, alapértelmezett értékekkel tér vissza.
|
||||
"""
|
||||
stmt = select(UserStats).where(UserStats.user_id == current_user.id)
|
||||
stats = (await db.execute(stmt)).scalar_one_or_none()
|
||||
if not stats:
|
||||
# Alapértelmezett statisztika
|
||||
return UserStatResponse(
|
||||
user_id=current_user.id,
|
||||
total_xp=0,
|
||||
current_level=1,
|
||||
restriction_level=0,
|
||||
penalty_quota_remaining=0,
|
||||
banned_until=None
|
||||
)
|
||||
return UserStatResponse.from_orm(stats)
|
||||
|
||||
|
||||
@router.get("/seasons/active", response_model=SeasonResponse)
|
||||
async def get_active_season(
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Visszaadja az éppen aktív szezont.
|
||||
"""
|
||||
stmt = select(Season).where(Season.is_active == True)
|
||||
season = (await db.execute(stmt)).scalar_one_or_none()
|
||||
if not season:
|
||||
raise HTTPException(status_code=404, detail="No active season found")
|
||||
return SeasonResponse.from_orm(season)
|
||||
|
||||
|
||||
@router.get("/leaderboard", response_model=List[LeaderboardEntry])
|
||||
async def get_leaderboard_top10(
|
||||
limit: int = Query(10, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Visszaadja a top felhasználókat total_xp alapján csökkenő sorrendben.
|
||||
"""
|
||||
stmt = (
|
||||
select(User.email, UserStats.total_xp, UserStats.current_level)
|
||||
.join(UserStats, User.id == UserStats.user_id)
|
||||
select(UserStats, User.email)
|
||||
.join(User, UserStats.user_id == User.id)
|
||||
.order_by(desc(UserStats.total_xp))
|
||||
.limit(limit)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
# Az email-eket maszkoljuk a GDPR miatt (pl. k***s@p***.hu)
|
||||
return [
|
||||
{"user": f"{r[0][:2]}***@{r[0].split('@')[1]}", "xp": r[1], "level": r[2]}
|
||||
for r in result.all()
|
||||
]
|
||||
rows = result.all()
|
||||
|
||||
leaderboard = []
|
||||
for stats, email in rows:
|
||||
leaderboard.append(
|
||||
LeaderboardEntry(
|
||||
user_id=stats.user_id,
|
||||
username=email, # email használata username helyett
|
||||
total_xp=stats.total_xp,
|
||||
current_level=stats.current_level
|
||||
)
|
||||
)
|
||||
return leaderboard
|
||||
@@ -5,6 +5,7 @@ import uuid
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import List
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
@@ -12,7 +13,7 @@ from sqlalchemy import select
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.schemas.organization import CorpOnboardIn, CorpOnboardResponse
|
||||
from app.models.organization import Organization, OrgType, OrganizationMember
|
||||
from app.models.marketplace.organization import Organization, OrgType, OrganizationMember
|
||||
from app.models.identity import User # JAVÍTVA: Központi Identity modell
|
||||
from app.core.config import settings
|
||||
|
||||
@@ -65,12 +66,19 @@ async def onboard_organization(
|
||||
address_street_type=org_in.address_street_type,
|
||||
address_house_number=org_in.address_house_number,
|
||||
address_hrsz=org_in.address_hrsz,
|
||||
address_stairwell=org_in.address_stairwell,
|
||||
address_floor=org_in.address_floor,
|
||||
address_door=org_in.address_door,
|
||||
country_code=org_in.country_code,
|
||||
org_type=OrgType.business,
|
||||
status="pending_verification"
|
||||
status="pending_verification",
|
||||
# --- EXPLICIT IDŐBÉLYEGEK A DB HIBA ELKERÜLÉSÉRE ---
|
||||
first_registered_at=datetime.now(timezone.utc),
|
||||
current_lifecycle_started_at=datetime.now(timezone.utc),
|
||||
created_at=datetime.now(timezone.utc),
|
||||
subscription_plan="FREE",
|
||||
base_asset_limit=1,
|
||||
purchased_extra_slots=0,
|
||||
notification_settings={},
|
||||
external_integration_config={},
|
||||
is_ownership_transferable=True
|
||||
)
|
||||
|
||||
db.add(new_org)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# backend/app/api/v1/endpoints/search.py
|
||||
# /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 app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.organization import Organization # JAVÍTVA
|
||||
from app.models.marketplace.organization import Organization # JAVÍTVA
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ async def approve_action(
|
||||
await security_service.approve_action(db, approver_id=current_user.id, action_id=action_id)
|
||||
# Frissített művelet lekérdezése
|
||||
from sqlalchemy import select
|
||||
from app.models.security import PendingAction
|
||||
from app.models import PendingAction
|
||||
stmt = select(PendingAction).where(PendingAction.id == action_id)
|
||||
action = (await db.execute(stmt)).scalar_one()
|
||||
return PendingActionResponse.from_orm(action)
|
||||
@@ -135,7 +135,7 @@ async def reject_action(
|
||||
)
|
||||
# Frissített művelet lekérdezése
|
||||
from sqlalchemy import select
|
||||
from app.models.security import PendingAction
|
||||
from app.models import PendingAction
|
||||
stmt = select(PendingAction).where(PendingAction.id == action_id)
|
||||
action = (await db.execute(stmt)).scalar_one()
|
||||
return PendingActionResponse.from_orm(action)
|
||||
@@ -158,7 +158,7 @@ async def get_action(
|
||||
Csak a művelet létrehozója vagy admin/superadmin érheti el.
|
||||
"""
|
||||
from sqlalchemy import select
|
||||
from app.models.security import PendingAction
|
||||
from app.models import PendingAction
|
||||
stmt = select(PendingAction).where(PendingAction.id == action_id)
|
||||
action = (await db.execute(stmt)).scalar_one_or_none()
|
||||
if not action:
|
||||
|
||||
@@ -4,7 +4,9 @@ from sqlalchemy import select, and_, text
|
||||
from typing import List, Optional
|
||||
from app.db.session import get_db
|
||||
from app.services.gamification_service import GamificationService
|
||||
from app.models.service import ServiceProfile, ExpertiseTag, ServiceExpertise
|
||||
from app.services.config_service import ConfigService
|
||||
from app.services.security_auditor import SecurityAuditorService
|
||||
from app.models.marketplace.service import ServiceProfile, ExpertiseTag, ServiceExpertise
|
||||
from app.services.marketplace_service import (
|
||||
create_verified_review,
|
||||
get_service_reviews,
|
||||
@@ -19,24 +21,92 @@ router = APIRouter()
|
||||
# --- 🎯 SZERVIZ VADÁSZAT (Service Hunt) ---
|
||||
@router.post("/hunt")
|
||||
async def register_service_hunt(
|
||||
name: str = Form(...),
|
||||
lat: float = Form(...),
|
||||
lng: float = Form(...),
|
||||
name: str = Form(...),
|
||||
lat: float = Form(...),
|
||||
lng: float = Form(...),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
""" Új szerviz-jelölt rögzítése a staging táblába jutalompontért. """
|
||||
# Új szerviz-jelölt rögzítése
|
||||
await db.execute(text("""
|
||||
INSERT INTO marketplace.service_staging (name, fingerprint, status, raw_data)
|
||||
VALUES (:n, :f, 'pending', jsonb_build_object('lat', :lat, 'lng', :lng))
|
||||
"""), {"n": name, "f": f"{name}-{lat}-{lng}", "lat": lat, "lng": lng})
|
||||
INSERT INTO marketplace.service_staging (name, fingerprint, status, city, submitted_by, raw_data)
|
||||
VALUES (:n, :f, 'pending', 'Unknown', :user_id, jsonb_build_object('lat', CAST(:lat AS double precision), 'lng', CAST(:lng AS double precision)))
|
||||
"""), {"n": name, "f": f"{name}-{lat}-{lng}", "lat": lat, "lng": lng, "user_id": current_user.id})
|
||||
|
||||
# MB 2.0 Gamification: 50 pont a felfedezésért
|
||||
# TODO: A 1-es ID helyett a bejelentkezett felhasználót kell használni (current_user.id)
|
||||
await GamificationService.award_points(db, 1, 50, f"Service Hunt: {name}")
|
||||
# MB 2.0 Gamification: Dinamikus pontszám a felfedezésért
|
||||
reward_points = await ConfigService.get_int(db, "GAMIFICATION_HUNT_REWARD", 50)
|
||||
await GamificationService.award_points(db, current_user.id, reward_points, f"Service Hunt: {name}")
|
||||
await db.commit()
|
||||
return {"status": "success", "message": "Discovery registered and points awarded."}
|
||||
|
||||
# --- ✅ SZERVIZ VALIDÁLÁS (Service Validation) ---
|
||||
@router.post("/hunt/{staging_id}/validate")
|
||||
async def validate_staged_service(
|
||||
staging_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Validálja egy másik felhasználó által beküldött szerviz-jelöltet.
|
||||
Növeli a validation_level-t 10-zel (max 80), adományoz 10 XP-t,
|
||||
és növeli a places_validated számlálót a felhasználó statisztikáiban.
|
||||
"""
|
||||
# Anti-Cheat: Rapid Fire ellenőrzés
|
||||
await SecurityAuditorService.check_rapid_fire_validation(db, current_user.id)
|
||||
|
||||
# 1. Keresd meg a staging rekordot
|
||||
result = await db.execute(
|
||||
text("SELECT id, submitted_by, validation_level FROM marketplace.service_staging WHERE id = :id"),
|
||||
{"id": staging_id}
|
||||
)
|
||||
staging = result.fetchone()
|
||||
if not staging:
|
||||
raise HTTPException(status_code=404, detail="Staging record not found")
|
||||
|
||||
# 2. Ha a saját beküldését validálná, hiba
|
||||
if staging.submitted_by == current_user.id:
|
||||
raise HTTPException(status_code=400, detail="Cannot validate your own submission")
|
||||
|
||||
# 3. Növeld a validation_level-t 10-zel (max 80)
|
||||
new_level = staging.validation_level + 10
|
||||
if new_level > 80:
|
||||
new_level = 80
|
||||
|
||||
# 4. UPDATE a validation_level és a status (ha elérte a 80-at, akkor "verified"?)
|
||||
# Jelenleg csak a validation_level frissítése
|
||||
await db.execute(
|
||||
text("""
|
||||
UPDATE marketplace.service_staging
|
||||
SET validation_level = :new_level
|
||||
WHERE id = :id
|
||||
"""),
|
||||
{"new_level": new_level, "id": staging_id}
|
||||
)
|
||||
|
||||
# 5. Adományozz dinamikus XP-t a current_user-nek a GamificationService-en keresztül
|
||||
validation_reward = await ConfigService.get_int(db, "GAMIFICATION_VALIDATE_REWARD", 10)
|
||||
await GamificationService.award_points(db, current_user.id, validation_reward, f"Service Validation: staging #{staging_id}")
|
||||
|
||||
# 6. Növeld a current_user places_validated értékét a UserStats-ban
|
||||
await db.execute(
|
||||
text("""
|
||||
UPDATE gamification.user_stats
|
||||
SET places_validated = places_validated + 1
|
||||
WHERE user_id = :user_id
|
||||
"""),
|
||||
{"user_id": current_user.id}
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Validation successful",
|
||||
"validation_level": new_level,
|
||||
"places_validated_incremented": True
|
||||
}
|
||||
|
||||
# --- 🔍 SZERVIZ KERESŐ (Service Search) ---
|
||||
@router.get("/search")
|
||||
async def search_services(
|
||||
|
||||
132
backend/app/api/v1/endpoints/system_parameters.py
Normal file
132
backend/app/api/v1/endpoints/system_parameters.py
Normal file
@@ -0,0 +1,132 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/system_parameters.py
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update
|
||||
from typing import List, Optional
|
||||
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.schemas.system import (
|
||||
SystemParameterResponse,
|
||||
SystemParameterUpdate,
|
||||
SystemParameterCreate,
|
||||
)
|
||||
from app.models.system import SystemParameter, ParameterScope
|
||||
from app.models.identity import UserRole
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[SystemParameterResponse])
|
||||
async def list_system_parameters(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
scope_level: Optional[ParameterScope] = Query(None, description="Scope szint (global, country, region, user)"),
|
||||
scope_id: Optional[str] = Query(None, description="Scope azonosító (pl. 'HU', 'budapest', user_id)"),
|
||||
is_active: Optional[bool] = Query(True, description="Csak aktív paraméterek"),
|
||||
):
|
||||
"""
|
||||
Listázza az összes aktív (vagy opcionálisan inaktív) rendszerparamétert.
|
||||
Szűrhető scope_level és scope_id alapján.
|
||||
"""
|
||||
query = select(SystemParameter)
|
||||
|
||||
if scope_level is not None:
|
||||
query = query.where(SystemParameter.scope_level == scope_level)
|
||||
if scope_id is not None:
|
||||
query = query.where(SystemParameter.scope_id == scope_id)
|
||||
if is_active is not None:
|
||||
query = query.where(SystemParameter.is_active == is_active)
|
||||
|
||||
result = await db.execute(query)
|
||||
parameters = result.scalars().all()
|
||||
return parameters
|
||||
|
||||
|
||||
@router.get("/{key}", response_model=SystemParameterResponse)
|
||||
async def get_system_parameter(
|
||||
key: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
scope_level: ParameterScope = Query("global", description="Scope szint (alapértelmezett: global)"),
|
||||
scope_id: Optional[str] = Query(None, description="Scope azonosító"),
|
||||
):
|
||||
"""
|
||||
Visszaad egy konkrét paramétert a key és scope_level (és opcionálisan scope_id) alapján.
|
||||
"""
|
||||
query = select(SystemParameter).where(
|
||||
SystemParameter.key == key,
|
||||
SystemParameter.scope_level == scope_level,
|
||||
)
|
||||
if scope_id is not None:
|
||||
query = query.where(SystemParameter.scope_id == scope_id)
|
||||
else:
|
||||
query = query.where(SystemParameter.scope_id.is_(None))
|
||||
|
||||
result = await db.execute(query)
|
||||
parameter = result.scalar_one_or_none()
|
||||
|
||||
if not parameter:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"System parameter not found with key='{key}', scope_level='{scope_level}', scope_id='{scope_id}'"
|
||||
)
|
||||
return parameter
|
||||
|
||||
|
||||
@router.put("/{key}", response_model=SystemParameterResponse)
|
||||
async def update_system_parameter(
|
||||
key: str,
|
||||
param_in: SystemParameterUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user=Depends(get_current_user),
|
||||
scope_level: ParameterScope = Query("global", description="Scope szint (alapértelmezett: global)"),
|
||||
scope_id: Optional[str] = Query(None, description="Scope azonosító"),
|
||||
):
|
||||
"""
|
||||
Módosítja egy létező paraméter value (JSONB) vagy is_active mezőjét (Admin funkció).
|
||||
Csak superadmin vagy admin jogosultságú felhasználók használhatják.
|
||||
"""
|
||||
# Jogosultság ellenőrzése
|
||||
if current_user.role not in (UserRole.superadmin, UserRole.admin):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Insufficient permissions. Only superadmin or admin can update system parameters."
|
||||
)
|
||||
|
||||
# Paraméter keresése
|
||||
query = select(SystemParameter).where(
|
||||
SystemParameter.key == key,
|
||||
SystemParameter.scope_level == scope_level,
|
||||
)
|
||||
if scope_id is not None:
|
||||
query = query.where(SystemParameter.scope_id == scope_id)
|
||||
else:
|
||||
query = query.where(SystemParameter.scope_id.is_(None))
|
||||
|
||||
result = await db.execute(query)
|
||||
parameter = result.scalar_one_or_none()
|
||||
|
||||
if not parameter:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"System parameter not found with key='{key}', scope_level='{scope_level}', scope_id='{scope_id}'"
|
||||
)
|
||||
|
||||
# Frissítés
|
||||
update_data = {}
|
||||
if param_in.description is not None:
|
||||
update_data["description"] = param_in.description
|
||||
if param_in.value is not None:
|
||||
update_data["value"] = param_in.value
|
||||
if param_in.is_active is not None:
|
||||
update_data["is_active"] = param_in.is_active
|
||||
|
||||
if update_data:
|
||||
stmt = (
|
||||
update(SystemParameter)
|
||||
.where(SystemParameter.id == parameter.id)
|
||||
.values(**update_data)
|
||||
)
|
||||
await db.execute(stmt)
|
||||
await db.commit()
|
||||
await db.refresh(parameter)
|
||||
|
||||
return parameter
|
||||
@@ -11,7 +11,7 @@ from sqlalchemy.orm import selectinload
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.vehicle import VehicleUserRating
|
||||
from app.models.vehicle_definitions import VehicleModelDefinition
|
||||
from app.models import VehicleModelDefinition
|
||||
from app.models.identity import User
|
||||
from app.schemas.vehicle import VehicleRatingCreate, VehicleRatingResponse
|
||||
|
||||
|
||||
Reference in New Issue
Block a user