átlagos kiegészítséek jó sok

This commit is contained in:
Roo
2026-03-22 11:02:05 +00:00
parent f53e0b53df
commit 5d44339f21
249 changed files with 20922 additions and 2253 deletions

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

View File

@@ -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__)

View File

@@ -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")

View File

@@ -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)

View File

@@ -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

View File

@@ -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)}"
)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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:

View File

@@ -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(

View 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

View File

@@ -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