átlagos kiegészítséek jó sok
This commit is contained in:
@@ -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)}")
|
||||
@@ -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__)
|
||||
|
||||
@@ -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}
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
97
backend/app/services/security_auditor.py
Normal file
97
backend/app/services/security_auditor.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user