á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

@@ -2,14 +2,79 @@
import json
import httpx
import base64
import logging
from typing import Dict, Any, Optional
from app.schemas.evidence import RegistrationDocumentExtracted
logger = logging.getLogger(__name__)
class AiOcrService:
OLLAMA_URL = "http://service_finder_ollama:11434/api/generate"
OLLAMA_URL = "http://sf_ollama:11434/api/generate"
MODEL_NAME = "llama3.2-vision"
DEFAULT_TIMEOUT = 90.0
@classmethod
async def analyze_image(cls, image_bytes: bytes, prompt: str) -> Dict[str, Any]:
"""
Általános képfeldolgozás Ollama Vision modellel.
Args:
image_bytes: A kép bájtjai
prompt: A prompt szöveg, amit a modelnek küldünk
Returns:
Dict a válasz adataival (a 'response' mezőből parse-olt JSON)
Raises:
httpx.RequestError: Ha a hálózati kérés sikertelen
json.JSONDecodeError: Ha a válasz nem érvényes JSON
ValueError: Ha más hiba történik
"""
base64_image = base64.b64encode(image_bytes).decode('utf-8')
payload = {
"model": cls.MODEL_NAME,
"prompt": prompt,
"images": [base64_image],
"stream": False,
"format": "json"
}
async with httpx.AsyncClient(timeout=cls.DEFAULT_TIMEOUT) as client:
try:
logger.info(f"Ollama API hívás: {cls.OLLAMA_URL}, model: {cls.MODEL_NAME}")
response = await client.post(cls.OLLAMA_URL, json=payload)
response.raise_for_status()
result = response.json()
ai_response_text = result.get("response", "{}")
# Próbáljuk JSON-ként értelmezni a választ
try:
parsed = json.loads(ai_response_text)
except json.JSONDecodeError:
# Ha nem JSON, visszaadjuk szövegként
parsed = {"raw_response": ai_response_text}
logger.info(f"Ollama válasz sikeresen feldolgozva")
return parsed
except httpx.TimeoutException:
logger.error("Ollama API timeout")
raise ValueError("Ollama API időtúllépés")
except httpx.HTTPStatusError as e:
logger.error(f"Ollama HTTP hiba: {e.response.status_code} - {e.response.text}")
raise ValueError(f"Ollama HTTP hiba: {e.response.status_code}")
except Exception as e:
logger.error(f"Ollama API hiba: {e}")
raise ValueError(f"AI hiba a képfeldolgozás során: {str(e)}")
@classmethod
async def extract_registration_data(cls, clean_image_bytes: bytes) -> RegistrationDocumentExtracted:
"""
Speciális metódus magyar forgalmi engedély adatainak kinyerésére.
A régi kompatibilitás miatt megtartva.
"""
base64_image = base64.b64encode(clean_image_bytes).decode('utf-8')
prompt = """
@@ -49,7 +114,7 @@ class AiOcrService:
"format": "json"
}
async with httpx.AsyncClient(timeout=90.0) as client:
async with httpx.AsyncClient(timeout=cls.DEFAULT_TIMEOUT) as client:
try:
response = await client.post(cls.OLLAMA_URL, json=payload)
response.raise_for_status()
@@ -60,5 +125,5 @@ class AiOcrService:
return RegistrationDocumentExtracted(**data_dict)
except Exception as e:
print(f"Robot 3 AI Hiba: {e}")
logger.error(f"Robot 3 AI Hiba: {e}")
raise ValueError(f"AI hiba az adatkivonás során: {str(e)}")

View File

@@ -11,8 +11,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.vehicle import VehicleCost, CostCategory
from app.models.vehicle_definitions import VehicleModelDefinition
from app.models.organization import Organization
from app.models import VehicleModelDefinition
from app.models.marketplace.organization import Organization
from app.services.system_service import SystemService
logger = logging.getLogger(__name__)

View File

@@ -8,8 +8,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_
from sqlalchemy.orm import selectinload
from app.models.asset import Asset, AssetAssignment, AssetTelemetry, AssetFinancials
from app.models import Asset, AssetAssignment, AssetTelemetry, AssetFinancials
from app.models.identity import User
from app.models.vehicle.history import LogSeverity
from app.services.config_service import config
from app.services.gamification_service import GamificationService
from app.services.security_service import security_service
@@ -79,7 +80,8 @@ class AssetService:
catalog_id=catalog_id,
current_organization_id=org_id,
status="active",
is_verified=False
individual_equipment={},
created_at=datetime.utcnow()
)
db.add(new_asset)
await db.flush()
@@ -87,7 +89,12 @@ class AssetService:
# Digitális Iker Alapmodulok
db.add(AssetAssignment(asset_id=new_asset.id, organization_id=org_id, status="active"))
db.add(AssetTelemetry(asset_id=new_asset.id))
db.add(AssetFinancials(asset_id=new_asset.id))
db.add(AssetFinancials(
asset_id=new_asset.id,
purchase_price_net=0.0,
purchase_price_gross=0.0,
financing_type="unknown"
))
# Gamification
reward = await config.get_setting(db, "xp_reward_asset_register", default=250)
@@ -112,7 +119,7 @@ class AssetService:
# Logoljuk a kísérletet a biztonsági szolgálatnál (Sentinel)
await security_service.log_event(
db, user_id=user_id, action="VEHICLE_CLAIM_INITIATED",
severity="warning", target_type="Asset", target_id=str(asset.id),
severity=LogSeverity.warning, target_type="Asset", target_id=str(asset.id),
new_data={"vin": asset.vin, "new_org": org_id}
)

View File

@@ -9,8 +9,8 @@ from fastapi.encoders import jsonable_encoder
from fastapi import HTTPException, status
from app.models.identity import User, Person, UserRole, VerificationToken, Wallet
from app.models.gamification import UserStats
from app.models.organization import Organization, OrganizationMember, OrgType, Branch
from app.models import UserStats
from app.models.marketplace import Organization, OrganizationMember, OrgType, Branch
from app.schemas.auth import UserLiteRegister, UserKYCComplete
from app.core.security import get_password_hash, verify_password, generate_secure_slug
from app.services.email_manager import email_manager
@@ -41,7 +41,15 @@ class AuthService:
new_person = Person(
first_name=user_in.first_name,
last_name=user_in.last_name,
is_active=False
is_active=False,
identity_docs={}, # EXPLICIT BEÁLLÍTÁS A DB HIBA ELKERÜLÉSÉRE
ice_contact={}, # EXPLICIT BEÁLLÍTÁS A DB HIBA ELKERÜLÉSÉRE
lifetime_xp=0, # default -1, de explicit 0
penalty_points=0, # default -1, de explicit 0
social_reputation=1.0, # default 0.0, de explicit 1.0
is_sales_agent=False, # default True, de explicit False
is_ghost=False,
created_at=datetime.now(timezone.utc)
)
db.add(new_person)
await db.flush()
@@ -58,7 +66,13 @@ class AuthService:
is_deleted=False,
region_code=user_in.region_code,
preferred_language=user_in.lang,
timezone=user_in.timezone
subscription_plan='FREE',
# --- EXPLICIT DEFAULT ÉRTÉKEK A DB HIBA ELKERÜLÉSÉRE ---
is_vip=True, # Changed to True to force inclusion in INSERT
preferred_currency="HUF",
scope_level="individual",
custom_permissions={},
created_at=datetime.now(timezone.utc)
)
db.add(new_user)
await db.flush()
@@ -84,7 +98,7 @@ class AuthService:
# Sentinel Audit Log
await security_service.log_event(
db, user_id=new_user.id, action="USER_REGISTER_LITE",
severity="info", target_type="User", target_id=str(new_user.id),
severity="INFO", target_type="User", target_id=str(new_user.id),
new_data={"email": user_in.email}
)
@@ -136,7 +150,17 @@ class AuthService:
owner_id=user.id,
is_active=True,
status="verified",
country_code=user.region_code
country_code=user.region_code,
# --- 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)
await db.flush()
@@ -251,7 +275,7 @@ class AuthService:
await security_service.log_event(
db, user_id=actor_id, action="USER_SOFT_DELETE",
severity="warning", target_type="User", target_id=str(user_id),
severity="WARNING", target_type="User", target_id=str(user_id),
new_data={"reason": reason}
)
await db.commit()

View File

@@ -13,7 +13,7 @@ from fastapi import HTTPException, status
from app.models.identity import User, Person, UserRole, VerificationToken, Wallet
from app.models.gamification import UserStats
from app.models.organization import Organization, OrganizationMember, OrgType
from app.models import Organization, OrganizationMember, OrgType
from app.schemas.auth import UserLiteRegister, UserKYCComplete
from app.core.security import get_password_hash, verify_password, generate_secure_slug
from app.services.email_manager import email_manager

View File

@@ -27,7 +27,7 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import selectinload
from app.models.identity import User, Wallet, ActiveVoucher, UserRole
from app.models.audit import FinancialLedger, LedgerEntryType, WalletType
from app.models import FinancialLedger, LedgerEntryType, WalletType
from app.core.config import settings
from app.services.config_service import config

View File

@@ -2,6 +2,7 @@
from typing import Any, Optional, Dict
import logging
import os
import json
from decimal import Decimal
from datetime import datetime, timezone
@@ -10,6 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
# Modellek importálása a központi helyről
from app.models import ExchangeRate, AssetCost, AssetTelemetry
from app.models.system.system import SystemParameter, ParameterScope
from app.db.session import AsyncSessionLocal
logger = logging.getLogger(__name__)
@@ -59,25 +61,211 @@ class CostService:
raise e
class ConfigService:
"""
MB 2.0 Alapvető konfigurációs szerviz.
Kezeli az AI szolgáltatások (Ollama) dinamikus beállításait és promptjait.
"""
async def get_setting(self, db: AsyncSession, key: str, default: Any = None) -> Any:
Egyszerű konfigurációs szolgáltatás a SystemParameter tábla lekérdezéséhez.
Támogatja a különböző típusú értékek lekérését alapértelmezett értékkel.
"""
@staticmethod
async def get(db: AsyncSession, key: str, default: Any = None, scope_level: ParameterScope = ParameterScope.GLOBAL, scope_id: Optional[str] = None) -> Any:
"""
Lekéri a kért beállítást.
1. Megnézi a környezeti változókat (NAGYBETŰVEL).
2. Ha nincs ilyen ENV, visszaadja a kódba égetett 'default' értéket.
"""
env_val = os.getenv(key.upper())
if env_val is not None:
# Automatikus típuskonverzió a default paraméter típusa alapján
if isinstance(default, int): return int(env_val)
if isinstance(default, float): return float(env_val)
if isinstance(default, bool): return str(env_val).lower() in ('true', '1', 'yes')
return env_val
Általános lekérdezés a SystemParameter táblából.
Args:
db: AsyncSession
key: A konfigurációs kulcs
default: Alapértelmezett érték, ha a kulcs nem található
scope_level: A paraméter scope-ja (global, country, region, user)
scope_id: A scope azonosítója (pl. országkód, user_id)
Returns:
A talált érték (a megfelelő típusban) vagy a default.
"""
from sqlalchemy import select, and_, cast, String
try:
# Convert scope_level to lowercase string for comparison
# PostgreSQL enum expects lowercase values, but Python Enum may be uppercase
scope_str = scope_level.value.lower() if hasattr(scope_level, 'value') else str(scope_level).lower()
# Build query with cast to avoid strict enum type mismatch
query = select(SystemParameter).where(
and_(
SystemParameter.key == key,
cast(SystemParameter.scope_level, String) == scope_str,
SystemParameter.is_active == True
)
)
if scope_id is None:
query = query.where(SystemParameter.scope_id.is_(None))
else:
query = query.where(SystemParameter.scope_id == scope_id)
result = await db.execute(query)
param = result.scalar_one_or_none()
if param is None:
# Opcionálisan beilleszthetjük a default értéket a táblába
# await ConfigService._insert_default(db, key, default, scope_level, scope_id)
return default
# A value oszlop JSONB, lehet dict, list, string, number, bool
db_value = param.value
# Típuskonverzió a default típusa alapján
if default is None:
return db_value
if isinstance(default, int):
if isinstance(db_value, (int, float, str)):
try:
return int(db_value)
except (ValueError, TypeError):
return default
return default
elif isinstance(default, float):
if isinstance(db_value, (int, float, str)):
try:
return float(db_value)
except (ValueError, TypeError):
return default
return default
elif isinstance(default, bool):
if isinstance(db_value, bool):
return db_value
elif isinstance(db_value, str):
return db_value.lower() in ('true', '1', 'yes', 'on')
elif isinstance(db_value, int):
return db_value != 0
return default
elif isinstance(default, str):
if isinstance(db_value, str):
return db_value
elif isinstance(db_value, (dict, list)):
return json.dumps(db_value)
else:
return str(db_value)
elif isinstance(default, dict) and isinstance(db_value, dict):
return db_value
elif isinstance(default, list) and isinstance(db_value, list):
return db_value
else:
# Egyébként visszaadjuk a db_value-t
return db_value
except Exception as e:
logger.warning(f"ConfigService.get error for key '{key}': {e}")
return default
async def get_setting(self, db: AsyncSession, key: str, default: Any = None, region_code: Optional[str] = None, org_id: Optional[int] = None, **kwargs) -> Any:
"""
Általános beállítás lekérése a régi kód kompatibilitásához.
Args:
db: AsyncSession
key: A konfigurációs kulcs
default: Alapértelmezett érték
region_code: Országkód (pl. "HU") - COUNTRY scope
org_id: Szervezet azonosító - ORGANIZATION scope
**kwargs: További paraméterek (pl. user_id)
Returns:
A talált érték vagy default.
"""
from app.models.system.system import ParameterScope
# Scope meghatározása
if org_id is not None:
scope_level = ParameterScope.ORGANIZATION
scope_id = str(org_id)
elif region_code is not None:
scope_level = ParameterScope.COUNTRY
scope_id = region_code
else:
scope_level = ParameterScope.GLOBAL
scope_id = None
# További scope-ok (pl. user) a kwargs-ból
if 'user_id' in kwargs:
scope_level = ParameterScope.USER
scope_id = str(kwargs['user_id'])
return await ConfigService.get(db, key, default, scope_level, scope_id)
@staticmethod
async def get_int(db: AsyncSession, key: str, default: int, scope_level: ParameterScope = ParameterScope.GLOBAL, scope_id: Optional[str] = None) -> int:
"""Egész szám lekérése."""
value = await ConfigService.get(db, key, default, scope_level, scope_id)
if isinstance(value, int):
return value
try:
return int(value)
except (ValueError, TypeError):
return default
@staticmethod
async def get_str(db: AsyncSession, key: str, default: str, scope_level: ParameterScope = ParameterScope.GLOBAL, scope_id: Optional[str] = None) -> str:
"""Szöveg lekérése."""
value = await ConfigService.get(db, key, default, scope_level, scope_id)
if isinstance(value, str):
return value
return str(value)
@staticmethod
async def get_bool(db: AsyncSession, key: str, default: bool, scope_level: ParameterScope = ParameterScope.GLOBAL, scope_id: Optional[str] = None) -> bool:
"""Logikai érték lekérése."""
value = await ConfigService.get(db, key, default, scope_level, scope_id)
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.lower() in ('true', '1', 'yes', 'on')
if isinstance(value, int):
return value != 0
return default
@staticmethod
async def get_float(db: AsyncSession, key: str, default: float, scope_level: ParameterScope = ParameterScope.GLOBAL, scope_id: Optional[str] = None) -> float:
"""Lebegőpontos szám lekérése."""
value = await ConfigService.get(db, key, default, scope_level, scope_id)
if isinstance(value, float):
return value
try:
return float(value)
except (ValueError, TypeError):
return default
@staticmethod
async def get_json(db: AsyncSession, key: str, default: dict, scope_level: ParameterScope = ParameterScope.GLOBAL, scope_id: Optional[str] = None) -> dict:
"""JSON objektum lekérése."""
value = await ConfigService.get(db, key, default, scope_level, scope_id)
if isinstance(value, dict):
return value
if isinstance(value, str):
try:
return json.loads(value)
except json.JSONDecodeError:
return default
return default
@staticmethod
async def _insert_default(db: AsyncSession, key: str, default: Any, scope_level: ParameterScope, scope_id: Optional[str] = None) -> None:
"""Opcionális: beszúrja a default értéket a táblába, hogy látható legyen az Admin UI-ban."""
try:
from app.models.system.system import SystemParameter
param = SystemParameter(
key=key,
category="auto_inserted",
value=default if isinstance(default, (dict, list)) else {"value": default},
scope_level=scope_level,
scope_id=scope_id,
is_active=True,
description=f"Auto-inserted default value for {key}"
)
db.add(param)
await db.commit()
except Exception as e:
logger.debug(f"Could not insert default for {key}: {e}")
await db.rollback()
# A példány, amit a többi modul (pl. az auth_service, ai_service) importálni próbál
config = ConfigService()

View File

@@ -5,7 +5,7 @@ from decimal import Decimal
from typing import Any, Dict
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc, func
from app.models.asset import AssetCost, AssetTelemetry, ExchangeRate
from app.models import AssetCost, AssetTelemetry, ExchangeRate
from app.services.gamification_service import GamificationService
from app.services.config_service import config
from app.schemas.asset_cost import AssetCostCreate

View File

@@ -7,7 +7,7 @@ from typing import Optional, Dict, Any
from sqlalchemy import select, and_, or_
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.vehicle_definitions import VehicleModelDefinition
from app.models import VehicleModelDefinition
from app.workers.vehicle.mapping_rules import SOURCE_MAPPINGS, unify_data
logger = logging.getLogger(__name__)

View File

@@ -9,7 +9,7 @@ from fastapi import UploadFile, BackgroundTasks, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_
from app.models.document import Document
from app.models import Document
from app.models.identity import User
from app.services.config_service import config # 2.0 Dinamikus beállítások
from app.workers.ocr.robot_1_ocr_processor import OCRRobot # Robot 1 hívása
@@ -122,7 +122,7 @@ class DocumentService:
if doc_type in auto_ocr_types:
# Robot 1 (OCR) sorba állítása háttérfolyamatként
background_tasks.add_task(OCRRobot.process_document, db, new_doc.id)
new_doc.status = "processing"
new_doc.status = "pending_ocr"
logger.info(f"🤖 Robot 1 (OCR) riasztva: {new_doc.id}")
await db.commit()

View File

@@ -81,6 +81,37 @@ class EmailManager:
smtp_cfg = await config.get_setting(db, "smtp_config", default={
"host": "localhost", "port": 587, "user": "", "pass": "", "tls": True
})
logger.info(f"SMTP config retrieved: {smtp_cfg}")
# Ha a default értéket kaptuk, próbáljuk a környezeti változókból felépíteni a konfigurációt
import os
env_host = os.getenv("SMTP_HOST")
env_port = os.getenv("SMTP_PORT")
env_user = os.getenv("SMTP_USER")
env_pass = os.getenv("SMTP_PASSWORD")
env_tls = os.getenv("SMTP_TLS", "False").lower() in ("true", "1", "yes")
env_ssl = os.getenv("SMTP_SSL", "False").lower() in ("true", "1", "yes")
logger.info(f"Env SMTP: host={env_host}, port={env_port}, tls={env_tls}, ssl={env_ssl}")
# Felülírjuk a konfigurációt a környezeti változókkal, ha vannak
if env_host:
smtp_cfg["host"] = env_host
if env_port:
try:
smtp_cfg["port"] = int(env_port)
except:
pass
if env_user:
smtp_cfg["user"] = env_user
if env_pass:
smtp_cfg["pass"] = env_pass
# TLS/SSL kezelése: ha SSL igaz, akkor TLS legyen False (mert külön SMTP_SSL kapcsolat kell)
# Egyszerűsítés: tls = not ssl (de a Mailpit esetén TLS=False, SSL=False)
smtp_cfg["tls"] = env_tls
# SSL esetén a port változhat, de a kódunk nem támogatja az SMTP_SSL-t, csak TLS-t.
# A Mailpit nem igényel TLS-t, így maradjon False.
if env_ssl:
smtp_cfg["tls"] = False
# Megjegyzés: SSL kapcsolathoz smtplib.SMTP_SSL kellene, de most nem implementáljuk.
logger.info(f"Final SMTP config: {smtp_cfg}")
return await EmailManager._send_via_smtp(smtp_cfg, from_email, from_name, recipient, subject, html)
finally:
@@ -119,8 +150,12 @@ class EmailManager:
with smtplib.SMTP(cfg["host"], cfg["port"], timeout=15) as server:
if cfg.get("tls", True):
server.starttls()
if cfg.get("user") and cfg.get("pass"):
server.login(cfg["user"], cfg["pass"])
# Mailpit nem támogatja az SMTP AUTH-ot, és ha üres string a user/pass, akkor se próbáljuk meg
user = cfg.get("user", "")
passwd = cfg.get("pass", "")
# Ha a user/pass nem üres és nem csak idézőjelek, akkor login
if user and passwd and user.strip() not in ('', '""') and passwd.strip() not in ('', '""'):
server.login(user, passwd)
server.send_message(msg)
logger.info(f"SMTP siker -> {recipient}")

View File

@@ -16,9 +16,9 @@ from typing import Optional, Dict, Any
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update, and_
from app.models.audit import FinancialLedger, WalletType, LedgerStatus, LedgerEntryType
from app.models import FinancialLedger, WalletType, LedgerStatus, LedgerEntryType
from app.models.identity import Wallet
from app.models.finance import Issuer, IssuerType
from app.models.marketplace.finance import Issuer, IssuerType
from app.services.financial_interfaces import (
BasePaymentGateway, BaseInvoicingService,
PaymentGatewayError, InvoicingError, InsufficientFundsError

View File

@@ -8,8 +8,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from sqlalchemy.orm import selectinload
from app.models.asset import Asset, AssetEvent, AssetCost, AssetTelemetry
from app.models.social import ServiceProvider, ModerationStatus
from app.models import Asset, AssetEvent, AssetCost, AssetTelemetry
from app.models import ServiceProvider, ModerationStatus
from app.schemas.fleet import EventCreate, TCOStats
from app.services.gamification_service import gamification_service
from app.services.config_service import config # 2.0 Dinamikus konfig

View File

@@ -4,9 +4,9 @@ import math
from decimal import Decimal
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc
from app.models.gamification import UserStats, PointsLedger, UserBadge, Badge
from app.models import UserStats, PointsLedger, UserBadge, Badge
from app.models.identity import User, Wallet
from app.models.audit import FinancialLedger
from app.models import FinancialLedger
from app.services.config_service import config # 2.0 Központi konfigurátor
logger = logging.getLogger("Gamification-Service-2.0")

View File

@@ -1,12 +1,14 @@
# /opt/docker/dev/service_finder/backend/app/services/geo_service.py
import uuid
import logging
from datetime import datetime, timezone
from typing import Optional, List, Dict, Any
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text, select
from sqlalchemy import text, select, and_
from app.services.config_service import config # 2.0 Dinamikus konfig
from app.db.session import AsyncSessionLocal
from app.models.identity.address import GeoPostalCode, GeoStreet, GeoStreetType, Address
logger = logging.getLogger("Geo-Service-2.2")
@@ -74,27 +76,49 @@ class GeoService:
default="{zip} {city}, {street} {type} {number}."
)
# 2. Irányítószám és Város (Auto-learning / Upsert)
zip_id_query = text("""
INSERT INTO system.geo_postal_codes (zip_code, city, country_code)
VALUES (:z, :c, :cc)
ON CONFLICT (country_code, zip_code, city) DO UPDATE SET city = EXCLUDED.city
RETURNING id
""")
zip_res = await db.execute(zip_id_query, {"z": zip_code, "c": city, "cc": default_country})
zip_id = zip_res.scalar()
# 2. Irányítószám és Város (Auto-learning / Upsert) - SELECT, majd INSERT
stmt = select(GeoPostalCode).where(
and_(
GeoPostalCode.country_code == default_country,
GeoPostalCode.zip_code == zip_code,
GeoPostalCode.city == city
)
)
existing_pc = (await db.execute(stmt)).scalar_one_or_none()
if existing_pc:
zip_id = existing_pc.id
else:
# 2. Beszúrás ha nem létezik
new_pc = GeoPostalCode(
country_code=default_country,
zip_code=zip_code,
city=city
)
db.add(new_pc)
await db.flush()
zip_id = new_pc.id
# 3. Utca szótár frissítése
await db.execute(text("""
INSERT INTO system.geo_streets (postal_code_id, name) VALUES (:zid, :n)
ON CONFLICT (postal_code_id, name) DO NOTHING
"""), {"zid": zip_id, "n": street_name})
# 3. Utca szótár frissítése (SELECT, majd INSERT)
stmt_street = select(GeoStreet).where(
and_(
GeoStreet.postal_code_id == zip_id,
GeoStreet.name == street_name
)
)
existing_street = (await db.execute(stmt_street)).scalar_one_or_none()
if not existing_street:
new_street = GeoStreet(postal_code_id=zip_id, name=street_name)
db.add(new_street)
await db.flush()
# 4. Közterület típus (út, utca, köz...)
await db.execute(text("""
INSERT INTO system.geo_street_types (name) VALUES (:n)
ON CONFLICT (name) DO NOTHING
"""), {"n": street_type.lower()})
# 4. Közterület típus (SELECT, majd INSERT)
stmt_type = select(GeoStreetType).where(GeoStreetType.name == street_type.lower())
existing_type = (await db.execute(stmt_type)).scalar_one_or_none()
if not existing_type:
new_type = GeoStreetType(name=street_type.lower())
db.add(new_type)
await db.flush()
# 5. SZÖVEGES CÍM GENERÁLÁSA SABLON ALAPJÁN (2.2 Újdonság)
# Megformázzuk az alapcímet az admin sablon szerint
@@ -111,42 +135,37 @@ class GeoService:
if floor: full_text += f" {floor}. em."
if door: full_text += f" {door}. ajtó"
# 6. Központi Address rekord rögzítése vagy lekérése
address_query = text("""
INSERT INTO system.addresses (
postal_code_id, street_name, street_type, house_number,
stairwell, floor, door, parcel_id, full_address_text
# 6. Központi Address rekord rögzítése vagy lekérése (SELECT, majd INSERT)
stmt_addr = select(Address).where(
and_(
Address.postal_code_id == zip_id,
Address.street_name == street_name,
Address.street_type == street_type,
Address.house_number == house_number,
Address.stairwell == stairwell,
Address.floor == floor,
Address.door == door
)
VALUES (:zid, :sn, :st, :hn, :sw, :fl, :dr, :pid, :txt)
ON CONFLICT (postal_code_id, street_name, street_type, house_number, stairwell, floor, door)
DO NOTHING
RETURNING id
""")
params = {
"zid": zip_id, "sn": street_name, "st": street_type,
"hn": house_number, "sw": stairwell, "fl": floor,
"dr": door, "pid": parcel_id, "txt": full_text
}
res = await db.execute(address_query, params)
addr_id = res.scalar()
# 7. Biztonsági keresés: Ha létezett a rekord, de nem kaptunk ID-t a RETURNING-gal
if not addr_id:
lookup_query = text("""
SELECT id FROM system.addresses
WHERE postal_code_id = :zid
AND street_name = :sn
AND street_type = :st
AND house_number = :hn
AND (stairwell IS NOT DISTINCT FROM :sw)
AND (floor IS NOT DISTINCT FROM :fl)
AND (door IS NOT DISTINCT FROM :dr)
LIMIT 1
""")
lookup_res = await db.execute(lookup_query, params)
addr_id = lookup_res.scalar()
)
existing_addr = (await db.execute(stmt_addr)).scalar_one_or_none()
if existing_addr:
addr_id = existing_addr.id
else:
new_addr = Address(
postal_code_id=zip_id,
street_name=street_name,
street_type=street_type,
house_number=house_number,
stairwell=stairwell,
floor=floor,
door=door,
parcel_id=parcel_id,
full_address_text=full_text,
created_at=datetime.now(timezone.utc)
)
db.add(new_addr)
await db.flush()
addr_id = new_addr.id
return addr_id

View File

@@ -8,8 +8,8 @@ from decimal import Decimal
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.asset import VehicleLogbook
from app.models.gamification import UserStats
from app.models import VehicleLogbook
from app.models import UserStats
from app.models.identity import User
from app.models.system import SystemParameter

View File

@@ -5,7 +5,7 @@ import shutil
from datetime import datetime, timedelta, timezone
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_
from app.models.asset import Asset, AssetTelemetry
from app.models import Asset, AssetTelemetry
from app.services.config_service import config # 2.0 Dinamikus konfig
from app.services.notification_service import NotificationService

View File

@@ -12,10 +12,10 @@ from sqlalchemy import select, and_, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.exc import IntegrityError
from app.models.social import ServiceReview
from app.models.service import ServiceProfile
from app.models import ServiceReview
from app.models.marketplace.service import ServiceProfile
from app.models.identity import User
from app.models.audit import FinancialLedger
from app.models import FinancialLedger
from app.models.system import SystemParameter
from app.schemas.social import ServiceReviewCreate, ServiceReviewResponse
from app.services.system_service import get_system_parameter

View File

@@ -9,8 +9,8 @@ from sqlalchemy import select
from sqlalchemy.orm import joinedload
from app.models.identity import User
from app.models.asset import Asset
from app.models.organization import Organization
from app.models import Asset
from app.models.marketplace.organization import Organization
from app.models.system import InternalNotification
from app.services.email_manager import email_manager
from app.services.config_service import config

View File

@@ -14,7 +14,7 @@ from sqlalchemy.orm import selectinload
from app.models.vehicle import VehicleOdometerState, VehicleCost
from app.models.system import SystemParameter
from app.models.vehicle_definitions import VehicleModelDefinition
from app.models import VehicleModelDefinition
class OdometerService:

View File

@@ -18,8 +18,8 @@ from decimal import Decimal
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
from app.models.payment import PaymentIntent, PaymentIntentStatus
from app.models.audit import WalletType
from app.models.marketplace.payment import PaymentIntent, PaymentIntentStatus
from app.models import WalletType
from app.models.identity import User, Wallet, ActiveVoucher
from app.services.billing_engine import AtomicTransactionManager, SmartDeduction
from app.services.stripe_adapter import stripe_adapter

View File

@@ -4,7 +4,7 @@ import logging
from datetime import datetime, timezone
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.asset import Asset, AssetCatalog, AssetTelemetry
from app.models import Asset, AssetCatalog, AssetTelemetry
logger = logging.getLogger(__name__)

View File

@@ -7,8 +7,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_
from geoalchemy2.functions import ST_Distance, ST_MakePoint, ST_DWithin
from app.models.service import ServiceProfile, ExpertiseTag
from app.models.organization import Organization
from app.models.marketplace.service import ServiceProfile, ExpertiseTag
from app.models.marketplace.organization import Organization
from app.models.identity import User
from app.services.config_service import config

View File

@@ -0,0 +1,97 @@
"""
Security Auditor Service - Anti-Cheat rendszer része.
Felelős a gyanús tevékenységek (pl. Rapid Fire validációk) észleléséért.
"""
import logging
from datetime import datetime, timedelta
from typing import Optional
from sqlalchemy import select, and_, func
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi import HTTPException
from app.services.config_service import ConfigService
from app.models.gamification.gamification import UserContribution
from app.models.system.system import ParameterScope
logger = logging.getLogger(__name__)
class SecurityAuditorService:
"""
Biztonsági audit szolgáltatás a Rapid Fire (gyorstüzelés) anomáliák
detektálására.
"""
@staticmethod
async def check_rapid_fire_validation(db: AsyncSession, user_id: int) -> None:
"""
Ellenőrzi, hogy a felhasználó túl sok validációt végzett-e rövid idő alatt.
Args:
db: Adatbázis munkamenet
user_id: Ellenőrizendő felhasználó azonosítója
Raises:
HTTPException(429): Ha a felhasználó túllépte a megengedett limitet.
"""
# 1. Dinamikus limit lekérése a konfigurációból
max_validations = await ConfigService.get_int(
db,
"ANTI_CHEAT_MAX_VALIDATIONS_PER_HOUR",
default=10,
scope_level=ParameterScope.GLOBAL,
scope_id=None
)
# 2. Az elmúlt 1 órában végzett validációk számának lekérdezése
one_hour_ago = datetime.utcnow() - timedelta(hours=1)
stmt = select(func.count(UserContribution.id)).where(
and_(
UserContribution.user_id == user_id,
UserContribution.contribution_type == 'service_validation',
UserContribution.created_at >= one_hour_ago,
UserContribution.status == 'approved' # csak jóváhagyott validációk
)
)
result = await db.execute(stmt)
recent_count = result.scalar() or 0
logger.debug(
f"Rapid fire check for user {user_id}: {recent_count} validations "
f"in last hour (limit: {max_validations})"
)
# 3. Limit ellenőrzése
if recent_count >= max_validations:
# Opcionális: büntetőpont hozzáadása a GamificationService-en keresztül
# await GamificationService.add_penalty(db, user_id, reason="Rapid fire validation")
raise HTTPException(
status_code=429,
detail=(
f"Anti-Cheat: Túl sok művelet rövid idő alatt. "
f"Maximum {max_validations} validáció engedélyezett óránként. "
f"Kérjük, lassíts!"
)
)
@staticmethod
async def log_suspicious_activity(
db: AsyncSession,
user_id: int,
activity_type: str,
details: Optional[dict] = None
) -> None:
"""
Gyanús tevékenység naplózása a későbbi elemzéshez.
Jelenleg csak logol, de később beilleszthető egy audit táblába.
"""
logger.warning(
f"Suspicious activity detected: user={user_id}, "
f"type={activity_type}, details={details}"
)
# TODO: Beszúrás egy security_audit_log táblába, ha lesz ilyen

View File

@@ -4,8 +4,8 @@ from datetime import datetime, timedelta, timezone
from typing import Optional, Any, Dict
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_
from app.models.security import PendingAction, ActionStatus
from app.models.history import AuditLog, LogSeverity
from app.models import PendingAction, ActionStatus
from app.models import AuditLog, LogSeverity
from app.models.identity import User
from app.models.system import SystemParameter

View File

@@ -3,7 +3,7 @@ from sqlalchemy import select, and_
from datetime import datetime, timezone
import logging
from app.models.social import ServiceProvider, Vote, ModerationStatus, Competition, UserScore
from app.models import ServiceProvider, Vote, ModerationStatus, Competition, UserScore
from app.models.identity import User
from app.schemas.social import ServiceProviderCreate

View File

@@ -1,31 +1,170 @@
# /opt/docker/dev/service_finder/backend/app/services/storage_service.py
import uuid
import socket
from io import BytesIO
from datetime import timedelta
from minio import Minio
from minio.error import S3Error
from app.core.config import settings
import logging
logger = logging.getLogger(__name__)
class StorageService:
# A klienst a beállításokból inicializáljuk
client = Minio(
settings.REDIS_URL.split("//")[1].split(":")[0], # Gyors fix a hostra vagy settings.MINIO_HOST
access_key="minioadmin",
secret_key="minioadmin",
secure=False
)
"""MinIO S3 objektumtároló szolgáltatás."""
@classmethod
def _resolve_endpoint(cls, endpoint: str) -> str:
"""
Resolve hostname to IP address if endpoint contains a hostname.
This helps with MinIO 'invalid hostname' issues.
"""
if "://" in endpoint:
# Remove protocol
endpoint = endpoint.split("://")[1]
if ":" in endpoint:
host, port = endpoint.split(":", 1)
else:
host, port = endpoint, "9000"
# Try to resolve hostname to IP
try:
ip = socket.gethostbyname(host)
resolved_endpoint = f"{ip}:{port}"
logger.debug(f"Resolved endpoint {endpoint} -> {resolved_endpoint}")
return resolved_endpoint
except socket.gaierror:
logger.warning(f"Could not resolve hostname {host}, using original endpoint")
return endpoint
# MinIO kliens inicializálása a konfigurációból
@classmethod
def _get_client(cls):
"""Get MinIO client with resolved endpoint."""
resolved_endpoint = cls._resolve_endpoint(settings.MINIO_ENDPOINT)
return Minio(
endpoint=resolved_endpoint,
access_key=settings.MINIO_ACCESS_KEY,
secret_key=settings.MINIO_SECRET_KEY,
secure=settings.MINIO_SECURE,
)
@classmethod
def _get_client_instance(cls):
"""Get client instance (cached)."""
if not hasattr(cls, '_client_instance'):
cls._client_instance = cls._get_client()
return cls._client_instance
# Client property
@classmethod
def client(cls):
"""Get MinIO client instance."""
return cls._get_client_instance()
@classmethod
async def ensure_bucket_exists(cls, bucket_name: str) -> bool:
"""
Ellenőrzi, hogy a megadott vödör létezik-e, ha nem, létrehozza.
Args:
bucket_name: A vödör neve
Returns:
True ha a vödör létezik vagy sikeresen létrejött, False ha hiba történt.
"""
try:
client = cls.client()
if not client.bucket_exists(bucket_name):
client.make_bucket(bucket_name)
logger.info(f"Bucket '{bucket_name}' created.")
else:
logger.debug(f"Bucket '{bucket_name}' already exists.")
return True
except S3Error as e:
logger.error(f"Error ensuring bucket '{bucket_name}': {e}")
return False
@classmethod
async def upload_image(
cls,
file_bytes: bytes,
bucket_name: str,
object_name: str,
content_type: str = "application/octet-stream",
) -> str:
"""
Feltölt egy fájlt a MinIO tárolóba.
Args:
file_bytes: A fájl tartalma bájtokban
bucket_name: Cél vödör neve
object_name: Objektum neve (pl. 'images/photo.jpg')
content_type: MIME típus (alapértelmezett: 'application/octet-stream')
Returns:
Az objektum teljes elérési útja (bucket/object_name)
Raises:
S3Error: Ha a feltöltés sikertelen
"""
await cls.ensure_bucket_exists(bucket_name)
# Feltöltés
client = cls.client()
client.put_object(
bucket_name=bucket_name,
object_name=object_name,
data=BytesIO(file_bytes),
length=len(file_bytes),
content_type=content_type,
)
logger.info(f"Uploaded object '{object_name}' to bucket '{bucket_name}'.")
return f"{bucket_name}/{object_name}"
@classmethod
def get_presigned_url(
cls,
bucket_name: str,
object_name: str,
expires: timedelta = timedelta(hours=1),
) -> str:
"""
Generál egy előjegyzett URL-t a fájl letöltéséhez.
Args:
bucket_name: A vödör neve
object_name: Az objektum neve
expires: Az URL érvényességi ideje (alapértelmezett: 1 óra)
Returns:
Az előjegyzett URL string
"""
try:
client = cls.client()
url = client.presigned_get_object(
bucket_name=bucket_name,
object_name=object_name,
expires=int(expires.total_seconds()),
)
logger.debug(f"Generated presigned URL for '{bucket_name}/{object_name}'.")
return url
except S3Error as e:
logger.error(f"Error generating presigned URL: {e}")
raise
# Kompatibilitás a régi kóddal
BUCKET_NAME = "vehicle-documents"
@classmethod
async def upload_document(cls, file_bytes: bytes, file_name: str, folder: str) -> str:
""" Fájl feltöltése S3/Minio tárhelyre. """
if not cls.client.bucket_exists(cls.BUCKET_NAME):
cls.client.make_bucket(cls.BUCKET_NAME)
unique_name = f"{folder}/{uuid.uuid4()}_{file_name}"
cls.client.put_object(
cls.BUCKET_NAME,
unique_name,
BytesIO(file_bytes),
len(file_bytes)
)
return f"{cls.BUCKET_NAME}/{unique_name}"
"""Kompatibilitási metódus a régi kóddal."""
object_name = f"{folder}/{uuid.uuid4()}_{file_name}"
return await cls.upload_image(
file_bytes=file_bytes,
bucket_name=cls.BUCKET_NAME,
object_name=object_name,
content_type="application/octet-stream",
)

View File

@@ -10,8 +10,8 @@ from datetime import datetime, timedelta
from decimal import Decimal
from app.core.config import settings
from app.models.payment import PaymentIntent, PaymentIntentStatus
from app.models.audit import WalletType
from app.models.marketplace.payment import PaymentIntent, PaymentIntentStatus
from app.models import WalletType
logger = logging.getLogger("stripe-adapter")

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/translation.py
# /opt/docker/dev/service_finder/backend/app/services/translation.py
from sqlalchemy import String, Text, Boolean, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base_class import Base

View File

@@ -4,7 +4,7 @@ import os
import logging
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
from app.models.translation import Translation
from app.models import Translation
from app.core.config import settings
from typing import Dict, Any, Optional

View File

@@ -12,8 +12,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.identity import User, UserTrustProfile
from app.models.asset import Vehicle, VehicleOwnership
from app.models.service import Cost
from app.models import Vehicle, VehicleOwnership
from app.models.marketplace.service import Cost
from app.models.system import SystemParameter, ParameterScope
from app.services.system_service import SystemService