feat: SuperAdmin bootstrap, i18n sync fix and AssetAssignment ORM fix

- Fixed AttributeError in User model (added region_code, preferred_language)
- Fixed InvalidRequestError in AssetAssignment (added organization relationship)
- Configured STATIC_DIR for translation sync
- Applied Alembic migrations for user schema updates
This commit is contained in:
2026-02-10 21:01:58 +00:00
parent e255fea3a5
commit 425f598fa3
51 changed files with 1753 additions and 204 deletions

View File

@@ -15,10 +15,6 @@ reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def get_current_token_payload(
token: str = Depends(reusable_oauth2)
) -> Dict[str, Any]:
"""
Kinyeri a token payload-ot DB hívás nélkül.
Ez teszi lehetővé a gyors jogosultság-ellenőrzést.
"""
if token == "dev_bypass_active":
return {
"sub": "1",
@@ -40,9 +36,6 @@ async def get_current_user(
db: AsyncSession = Depends(get_db),
payload: Dict[str, Any] = Depends(get_current_token_payload),
) -> User:
"""
Visszaadja a teljes User modellt. Akkor használjuk, ha módosítani kell az usert.
"""
user_id = payload.get("sub")
if not user_id:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token azonosítási hiba.")
@@ -55,11 +48,18 @@ async def get_current_user(
return user
async def get_current_active_user(
current_user: User = Depends(get_current_user),
) -> User:
"""Ellenőrzi, hogy a felhasználó aktív-e."""
if not current_user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="A felhasználói fiók zárolva van vagy inaktív."
)
return current_user
def check_min_rank(required_rank: int):
"""
Függőség-gyár: Ellenőrzi, hogy a felhasználó rangja eléri-e a minimumot.
Használat: Depends(check_min_rank(60)) -> RegionAdmin+
"""
def rank_checker(payload: Dict[str, Any] = Depends(get_current_token_payload)):
user_rank = payload.get("rank", 0)
if user_rank < required_rank:

View File

@@ -1,22 +1,26 @@
from fastapi import APIRouter
from app.api.v1.endpoints import auth, catalog, assets, organizations, documents, services
from app.api.v1.endpoints import auth, catalog, assets, organizations, documents, services, admin
api_router = APIRouter()
# Hitelesítés
# Hitelesítés (Authentication)
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
# Szolgáltatások és Vadászat (Ez az új rész!)
# Szolgáltatások és Vadászat (Service Hunt & Discovery)
api_router.include_router(services.router, prefix="/services", tags=["Service Hunt & Discovery"])
# Katalógus
# Katalógus (Vehicle Catalog)
api_router.include_router(catalog.router, prefix="/catalog", tags=["Vehicle Catalog"])
# Eszközök (Járművek)
# Eszközök / Járművek (Assets)
api_router.include_router(assets.router, prefix="/assets", tags=["Assets"])
# Szervezetek
# Szervezetek (Organizations)
api_router.include_router(organizations.router, prefix="/organizations", tags=["Organizations"])
# Dokumentumok
api_router.include_router(documents.router, prefix="/documents", tags=["Documents"])
# Dokumentumok (Documents)
api_router.include_router(documents.router, prefix="/documents", tags=["Documents"])
# --- 🛡️ SENTINEL ADMIN KONTROLL PANEL ---
# Ez a rész tette láthatóvá az Admin API-t a felületen
api_router.include_router(admin.router, prefix="/admin", tags=["Admin Control Center (Sentinel)"])

View File

@@ -1,79 +1,115 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import List
from sqlalchemy import select, func
from typing import List, Any, Dict
from datetime import datetime, timedelta
from app.db.session import get_db
from app.api import deps
from app.models.user import User, UserRole
from app.models.system_settings import SystemSetting # ÚJ import
from app.models.gamification import PointRule, LevelConfig, RegionalSetting
from app.models.translation import Translation
from app.services.translation_service import TranslationService
from app.models.identity import User, UserRole
from app.models.system_config import SystemParameter
from app.models.security import PendingAction, ActionStatus
from app.models.history import AuditLog, LogSeverity
from app.schemas.admin_security import PendingActionResponse, SecurityStatusResponse
from app.services.security_service import security_service
# Feltételezve, hogy a JSON-alapú TranslationService-ed már készen van
from app.services.translation_service import TranslationService
router = APIRouter()
def check_admin_access(current_user: User, required_roles: List[UserRole]):
if current_user.role not in required_roles:
# --- 🛡️ ADMIN JOGOSULTSÁG ELLENŐRZŐ ---
async def check_admin_access(current_user: User = Depends(deps.get_current_active_user)):
if current_user.role not in [UserRole.admin, UserRole.superadmin]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Nincs jogosultságod ehhez a művelethez."
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin jogosultság szükséges!"
)
return current_user
# --- ⚙️ ÚJ: DINAMIKUS RENDSZERBEÁLLÍTÁSOK (Pl. Jármű limit) ---
# --- 1. SENTINEL: NÉGY SZEM ELV (Approval System) ---
@router.get("/settings", response_model=List[dict])
async def get_all_system_settings(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_user)
@router.get("/pending-actions", response_model=List[PendingActionResponse])
async def list_pending_actions(
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
"""Az összes globális rendszerbeállítás listázása."""
check_admin_access(current_user, [UserRole.SUPERUSER])
result = await db.execute(select(SystemSetting))
settings = result.scalars().all()
return [{"key": s.key, "value": s.value, "description": s.description} for s in settings]
"""Jóváhagyásra váró kritikus kérések listázása."""
stmt = select(PendingAction).where(PendingAction.status == ActionStatus.pending)
result = await db.execute(stmt)
return result.scalars().all()
@router.post("/approve/{action_id}")
async def approve_action(
action_id: int,
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
"""Művelet véglegesítése (második admin által)."""
try:
await security_service.approve_action(db, admin.id, action_id)
return {"status": "success", "message": "Művelet végrehajtva."}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
# --- 2. SENTINEL: BIZTONSÁGI ÖSSZEGZÉS ---
@router.get("/security-status", response_model=SecurityStatusResponse)
async def get_security_status(
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
"""Rendszerállapot: Zárolt júzerek és kritikus események."""
day_ago = datetime.now() - timedelta(days=1)
crit_count = (await db.execute(select(func.count(AuditLog.id)).where(
AuditLog.severity.in_([LogSeverity.critical, LogSeverity.emergency]),
AuditLog.timestamp >= day_ago
))).scalar() or 0
locked_count = (await db.execute(select(func.count(User.id)).where(
User.is_active == False, User.is_deleted == False
))).scalar() or 0
return {
"total_pending": (await db.execute(select(func.count(PendingAction.id)).where(PendingAction.status == ActionStatus.pending))).scalar() or 0,
"critical_logs_last_24h": crit_count,
"emergency_locks_active": locked_count
}
# --- 3. RENDSZERBEÁLLÍTÁSOK (Dynamic Config) ---
@router.get("/settings")
async def get_settings(db: AsyncSession = Depends(deps.get_db), admin: User = Depends(check_admin_access)):
"""Minden globális paraméter (Gamification, Limitek stb.) lekérése."""
result = await db.execute(select(SystemParameter))
return result.scalars().all()
@router.put("/settings/{key}")
async def update_system_setting(
key: str,
new_value: int, # Később lehet JSON is, ha komplexebb a beállítás
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_user)
):
"""Egy adott beállítás (pl. FREE_VEHICLE_LIMIT) módosítása."""
check_admin_access(current_user, [UserRole.SUPERUSER])
async def update_setting(key: str, value: Any, db: AsyncSession = Depends(deps.get_db), admin: User = Depends(check_admin_access)):
"""Paraméter módosítása és Audit Log generálása."""
stmt = select(SystemParameter).where(SystemParameter.key == key)
param = (await db.execute(stmt)).scalar_one_or_none()
if not param:
raise HTTPException(status_code=404, detail="Nincs ilyen beállítás.")
result = await db.execute(select(SystemSetting).where(SystemSetting.key == key))
setting = result.scalar_one_or_none()
old_val = param.value
param.value = value
if not setting:
raise HTTPException(status_code=404, detail="Beállítás nem található")
setting.value = new_value
await security_service.log_event(
db, admin.id, action="SETTING_CHANGE", severity=LogSeverity.warning,
old_data={key: old_val}, new_data={key: value}
)
await db.commit()
return {"status": "success", "key": key, "new_value": new_value}
return {"status": "success", "key": key, "new_value": value}
# --- 🌍 JSON FORDÍTÁSOK KEZELÉSE ---
# --- 🌍 FORDÍTÁSOK KEZELÉSE (Meglévő kódod) ---
@router.post("/translations", status_code=status.HTTP_201_CREATED)
async def add_translation_draft(
key: str, lang: str, value: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_user)
@router.post("/translations/sync")
async def sync_translations_to_json(
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
check_admin_access(current_user, [UserRole.SUPERUSER, UserRole.REGIONAL_ADMIN])
new_t = Translation(key=key, lang_code=lang, value=value, is_published=False)
db.add(new_t)
await db.commit()
return {"message": "Fordítás piszkozatként mentve. Ne felejtsd el publikálni!"}
@router.post("/translations/publish")
async def publish_translations(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_user)
):
check_admin_access(current_user, [UserRole.SUPERUSER, UserRole.REGIONAL_ADMIN])
await TranslationService.publish_all(db)
return {"message": "Sikeres publikálás! A változások minden szerveren élesedtek."}
"""Szinkronizálja az adatbázisban tárolt fordításokat a JSON fájlokba."""
# A TranslationService-ben kell megírni a fájlbaíró logikát
await TranslationService.export_to_json(db)
return {"message": "JSON nyelvi fájlok frissítve."}

View File

@@ -1,10 +1,16 @@
import os
from pathlib import Path
from typing import Any, Optional
from pydantic_settings import BaseSettings, SettingsConfigDict
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
class Settings(BaseSettings):
# --- Paths (ÚJ SZEKCIÓ) ---
# Meghatározzuk a projekt gyökérmappáját és a statikus fájlok helyét
BASE_DIR: Path = Path(__file__).resolve().parent.parent.parent
STATIC_DIR: str = os.path.join(str(BASE_DIR), "static")
# --- General ---
PROJECT_NAME: str = "Traffic Ecosystem SuperApp"
VERSION: str = "1.0.0"
@@ -16,8 +22,7 @@ class Settings(BaseSettings):
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 nap
# --- Initial Admin (ÚJ SZEKCIÓ) ---
# Ezeket a .env-ből fogja venni
# --- Initial Admin ---
INITIAL_ADMIN_EMAIL: str = "admin@servicefinder.hu"
INITIAL_ADMIN_PASSWORD: str = "Admin123!"

View File

@@ -15,6 +15,8 @@ from app.models.gamification import ( # noqa
from app.models.system_config import SystemParameter # noqa
from app.models.history import AuditLog, VehicleOwnership # noqa
from app.models.document import Document # noqa
from app.models.translation import Translation # noqa <--- HOZZÁADVA
from app.models.core_logic import ( # noqa
SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
)
)
from app.models.security import PendingAction # noqa <--- CSAK A BIZTONSÁG KEDVÉÉRT, HA EZ IS HIÁNYZOTT VOLNA

View File

@@ -11,8 +11,10 @@ from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType
from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, Rating, PointsLedger
from .system_config import SystemParameter
from .document import Document
from .translation import Translation # <--- HOZZÁADVA
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
from .history import AuditLog, VehicleOwnership
from .security import PendingAction # <--- HOZZÁADVA
# Aliasok
Vehicle = Asset
@@ -26,7 +28,8 @@ __all__ = [
"AssetEvent", "AssetFinancials", "AssetTelemetry", "AssetReview", "ExchangeRate",
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "PointRule",
"LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
"SystemParameter", "Document", "SubscriptionTier", "OrganizationSubscription",
"SystemParameter", "Document", "Translation", "PendingAction", # <--- BŐVÍTVE
"SubscriptionTier", "OrganizationSubscription",
"CreditTransaction", "ServiceSpecialty", "AuditLog", "VehicleOwnership",
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord"
]

View File

@@ -75,7 +75,9 @@ class AssetReview(Base):
criteria_scores = Column(JSON, server_default=text("'{}'::jsonb"))
comment = Column(Text)
created_at = Column(DateTime(timezone=True), server_default=func.now())
asset = relationship("Asset", back_populates="reviews")
user = relationship("User") # <--- JAVÍTÁS: Hozzáadva
class AssetAssignment(Base):
__tablename__ = "asset_assignments"
@@ -86,7 +88,9 @@ class AssetAssignment(Base):
assigned_at = Column(DateTime(timezone=True), server_default=func.now())
released_at = Column(DateTime(timezone=True), nullable=True)
status = Column(String(30), default="active")
asset = relationship("Asset", back_populates="assignments")
organization = relationship("Organization") # <--- KRITIKUS JAVÍTÁS: Ez okozta a login hibát
class AssetEvent(Base):
__tablename__ = "asset_events"
@@ -115,7 +119,10 @@ class AssetCost(Base):
date = Column(DateTime(timezone=True), server_default=func.now())
mileage_at_cost = Column(Integer)
data = Column(JSON, server_default=text("'{}'::jsonb"))
asset = relationship("Asset", back_populates="costs")
organization = relationship("Organization") # <--- JAVÍTÁS: Hozzáadva
driver = relationship("User") # <--- JAVÍTÁS: Hozzáadva
class ExchangeRate(Base):
__tablename__ = "exchange_rates"

View File

@@ -1,16 +1,15 @@
import uuid
from datetime import datetime
from typing import Optional, TYPE_CHECKING
from sqlalchemy import ForeignKey, String, Integer, DateTime, func, Boolean
from sqlalchemy import ForeignKey, String, Integer, DateTime, func, Boolean, Text, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from app.db.base_class import Base
# Típusvizsgálathoz a körkörös import elkerülése érdekében
if TYPE_CHECKING:
from app.models.identity import User
# Közös beállítás az összes táblához ebben a fájlban
SCHEMA_ARGS = {"schema": "data"}
class PointRule(Base):
@@ -30,39 +29,36 @@ class LevelConfig(Base):
min_points: Mapped[int] = mapped_column(Integer)
rank_name: Mapped[str] = mapped_column(String)
class RegionalSetting(Base):
__tablename__ = "regional_settings"
__table_args__ = SCHEMA_ARGS
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
country_code: Mapped[str] = mapped_column(String, unique=True)
currency: Mapped[str] = mapped_column(String, default="HUF")
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
class PointsLedger(Base):
__tablename__ = "points_ledger"
__table_args__ = SCHEMA_ARGS
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"))
points: Mapped[int] = mapped_column(Integer)
points: Mapped[int] = mapped_column(Integer, default=0)
# JAVÍTÁS: Itt is server_default-ot használunk
penalty_change: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
reason: Mapped[str] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(DateTime, default=func.now())
# Kapcsolat a felhasználóhoz
user: Mapped["User"] = relationship("User")
class UserStats(Base):
__tablename__ = "user_stats"
__table_args__ = SCHEMA_ARGS
# user_id a PK, mert 1:1 kapcsolat a User-rel
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"), primary_key=True)
total_xp: Mapped[int] = mapped_column(Integer, default=0)
social_points: Mapped[int] = mapped_column(Integer, default=0)
current_level: Mapped[int] = mapped_column(Integer, default=1)
# --- BÜNTETŐ RENDSZER (Strike System) ---
# JAVÍTÁS: server_default hozzáadva, hogy a meglévő sorok is 0-t kapjanak
penalty_points: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
restriction_level: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=func.now(), onupdate=func.now())
# EZ A JAVÍTÁS: A visszamutató kapcsolat definiálása
user: Mapped["User"] = relationship("User", back_populates="stats")
class Badge(Base):
__tablename__ = "badges"
__table_args__ = SCHEMA_ARGS
@@ -81,7 +77,7 @@ class UserBadge(Base):
user: Mapped["User"] = relationship("User")
class Rating(Base): # <--- Az új értékelési modell
class Rating(Base):
__tablename__ = "ratings"
__table_args__ = SCHEMA_ARGS
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)

View File

@@ -1,9 +1,16 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON, Date, Text
import enum
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON, Date, Text, Enum
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from app.db.base_class import Base
class LogSeverity(str, enum.Enum):
info = "info" # Általános művelet (pl. profil megtekintés)
warning = "warning" # Gyanús, de nem biztosan káros (pl. 3 elrontott jelszó)
critical = "critical" # Súlyos művelet (pl. jelszóváltoztatás, export)
emergency = "emergency" # Azonnali beavatkozást igényel (pl. SuperAdmin módosítás)
class VehicleOwnership(Base):
__tablename__ = "vehicle_ownerships"
__table_args__ = {"schema": "data"}
@@ -20,11 +27,25 @@ class VehicleOwnership(Base):
class AuditLog(Base):
__tablename__ = "audit_logs"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
target_type = Column(String, index=True)
target_id = Column(String, index=True)
action = Column(String, nullable=False)
changes = Column(JSON, nullable=True)
timestamp = Column(DateTime(timezone=True), server_default=func.now())
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
severity = Column(Enum(LogSeverity), default=LogSeverity.info, nullable=False)
# Mi történt és min?
action = Column(String(100), nullable=False, index=True)
target_type = Column(String(50), index=True) # pl. "User", "Wallet", "Asset"
target_id = Column(String(50), index=True) # A cél rekord ID-ja
# Részletes adatok (JSONB formátum a rugalmasságért)
# A 'changes' helyett explicit old/new párost használunk a könnyebb visszaállításhoz
old_data = Column(JSON, nullable=True)
new_data = Column(JSON, nullable=True)
# Biztonsági nyomkövetés
ip_address = Column(String(45), index=True) # IPv6-ot is támogat
user_agent = Column(Text, nullable=True) # Böngésző/Eszköz információ
timestamp = Column(DateTime(timezone=True), server_default=func.now(), index=True)
user = relationship("User")

View File

@@ -7,12 +7,12 @@ from sqlalchemy.sql import func
from app.db.base_class import Base
class UserRole(str, enum.Enum):
superadmin = "superadmin"
admin = "admin"
user = "user"
service = "service"
fleet_manager = "fleet_manager"
driver = "driver"
superadmin = "superadmin" # Hozzáadva a biztonság kedvéért
class Person(Base):
__tablename__ = "persons"
@@ -24,16 +24,9 @@ class Person(Base):
last_name = Column(String, nullable=False)
first_name = Column(String, nullable=False)
mothers_last_name = Column(String, nullable=True)
mothers_first_name = Column(String, nullable=True)
birth_place = Column(String, nullable=True)
birth_date = Column(DateTime, nullable=True)
phone = Column(String, nullable=True)
identity_docs = Column(JSON, server_default=text("'{}'::jsonb"))
medical_emergency = Column(JSON, server_default=text("'{}'::jsonb"))
ice_contact = Column(JSON, server_default=text("'{}'::jsonb"))
is_active = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
@@ -49,27 +42,27 @@ class User(Base):
hashed_password = Column(String, nullable=True)
role = Column(Enum(UserRole), default=UserRole.user)
is_active = Column(Boolean, default=False)
region_code = Column(String, default="HU")
is_deleted = Column(Boolean, default=False)
person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True)
preferred_language = Column(String(5), default="hu")
preferred_currency = Column(String(3), default="HUF")
timezone = Column(String(50), default="Europe/Budapest")
# RBAC & SCOPE mezők (Visszaállítva a DB sémához)
# ÚJ MEZŐK HOZZÁADVA:
preferred_language = Column(String(5), server_default="hu")
region_code = Column(String(5), server_default="HU")
# RBAC & SCOPE
scope_level = Column(String(30), server_default="individual")
scope_id = Column(String(50))
custom_permissions = Column(JSON, server_default=text("'{}'::jsonb"))
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Kapcsolatok
person = relationship("Person", back_populates="users")
wallet = relationship("Wallet", back_populates="user", uselist=False)
stats = relationship("UserStats", back_populates="user", uselist=False)
ownership_history = relationship("VehicleOwnership", back_populates="user")
owned_organizations = relationship("Organization", back_populates="owner")
# A Wallet és VerificationToken osztályok maradnak változatlanok...
class Wallet(Base):
__tablename__ = "wallets"
__table_args__ = {"schema": "data"}

View File

@@ -0,0 +1,44 @@
import enum
import uuid
from datetime import datetime, timedelta
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON, Enum, text
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.base_class import Base
class ActionStatus(str, enum.Enum):
pending = "pending" # Jóváhagyásra vár
approved = "approved" # Végrehajtva
rejected = "rejected" # Elutasítva
expired = "expired" # Lejárt (biztonsági okokból)
class PendingAction(Base):
"""Négy szem elv: Műveletek, amik jóváhagyásra várnak."""
__tablename__ = "pending_actions"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
# Ki akarja csinálni?
requester_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
# Ki hagyta jóvá/utasította el?
approver_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
status = Column(Enum(ActionStatus), default=ActionStatus.pending, nullable=False)
# Milyen típusú művelet? (pl. "CHANGE_ROLE", "WALLET_ADJUST", "DELETE_LOGS")
action_type = Column(String(50), nullable=False)
# A művelet adatai JSON-ben (pl. {"user_id": 5, "new_role": "admin"})
payload = Column(JSON, nullable=False)
# Miért kell ez a művelet? (Indoklás kötelező az audit miatt)
reason = Column(String(255), nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
expires_at = Column(DateTime(timezone=True), default=lambda: datetime.now() + timedelta(hours=24))
processed_at = Column(DateTime(timezone=True), nullable=True)
requester = relationship("User", foreign_keys=[requester_id])
approver = relationship("User", foreign_keys=[approver_id])

View File

@@ -1,5 +1,6 @@
from sqlalchemy import Column, Integer, String, Text, Boolean, UniqueConstraint
from app.db.base import Base
# JAVÍTÁS: Közvetlenül a base_class-ból importálunk, hogy elkerüljük a körkörös importot
from app.db.base_class import Base
class Translation(Base):
__tablename__ = "translations"
@@ -12,4 +13,4 @@ class Translation(Base):
key = Column(String(100), nullable=False, index=True)
lang_code = Column(String(5), nullable=False, index=True)
value = Column(Text, nullable=False)
is_published = Column(Boolean, default=False) # Publikálási állapot
is_published = Column(Boolean, default=False)

View File

@@ -0,0 +1,26 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Optional, Any, Dict, List
from app.models.security import ActionStatus
class PendingActionResponse(BaseModel):
id: int
requester_id: int
action_type: str
payload: Dict[str, Any]
reason: str
status: ActionStatus
created_at: datetime
expires_at: datetime
class Config:
from_attributes = True
class ActionApproveRequest(BaseModel):
# Itt akár extra jelszót vagy MFA tokent is kérhetnénk a jövőben
comment: Optional[str] = None
class SecurityStatusResponse(BaseModel):
total_pending: int
critical_logs_last_24h: int
emergency_locks_active: int

View File

@@ -1,6 +1,7 @@
import os
import logging
import uuid
import json
from datetime import datetime, timedelta, timezone
from typing import Optional
@@ -18,18 +19,15 @@ from app.services.email_manager import email_manager
from app.core.config import settings
from app.services.config_service import config
from app.services.geo_service import GeoService
from app.services.security_service import security_service # Sentinel integráció
logger = logging.getLogger(__name__)
class AuthService:
@staticmethod
async def register_lite(db: AsyncSession, user_in: UserLiteRegister):
"""
Step 1: Lite Regisztráció (Master Book 1.1)
Új User és ideiglenes Person rekord létrehozása nyelvi és időzóna adatokkal.
"""
"""Step 1: Lite Regisztráció."""
try:
# Ideiglenes Person rekord a KYC-ig
new_person = Person(
first_name=user_in.first_name,
last_name=user_in.last_name,
@@ -46,14 +44,12 @@ class AuthService:
is_active=False,
is_deleted=False,
region_code=user_in.region_code,
# --- NYELVI ÉS ADMIN BEÁLLÍTÁSOK MENTÉSE ---
preferred_language=user_in.lang,
timezone=user_in.timezone
)
db.add(new_user)
await db.flush()
# Regisztrációs token generálása
reg_hours = await config.get_setting("auth_registration_hours", region_code=user_in.region_code, default=48)
token_val = uuid.uuid4()
db.add(VerificationToken(
@@ -63,14 +59,12 @@ class AuthService:
expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reg_hours))
))
# --- EMAIL KÜLDÉSE A VÁLASZTOTT NYELVEN ---
# Master Book 3.2: Nincs manuális subject, a nyelvi kulcs alapján töltődik be
verification_link = f"{settings.FRONTEND_BASE_URL}/verify?token={token_val}"
await email_manager.send_email(
recipient=user_in.email,
template_key="reg", # hu.json: email.reg_subject, reg_greeting stb.
template_key="reg",
variables={"first_name": user_in.first_name, "link": verification_link},
lang=user_in.lang # Dinamikus nyelvválasztás
lang=user_in.lang
)
await db.commit()
@@ -83,23 +77,16 @@ class AuthService:
@staticmethod
async def complete_kyc(db: AsyncSession, user_id: int, kyc_in: UserKYCComplete):
"""
1.3. Fázis: Atomi Tranzakció & Shadow Identity
Felismeri a visszatérő Person-t, de új User-ként, izolált flottával indít.
Frissíti a nyelvi és pénzügyi beállításokat.
"""
"""1.3. Fázis: Atomi Tranzakció & Shadow Identity."""
try:
# 1. Aktuális technikai User lekérése
stmt = select(User).options(joinedload(User.person)).where(User.id == user_id)
res = await db.execute(stmt)
user = res.scalar_one_or_none()
if not user: return None
# --- PÉNZNEM PREFERENCIA FRISSÍTÉSE ---
if hasattr(kyc_in, 'preferred_currency') and kyc_in.preferred_currency:
user.preferred_currency = kyc_in.preferred_currency
# 2. Shadow Identity Ellenőrzése
identity_stmt = select(Person).where(and_(
Person.mothers_last_name == kyc_in.mothers_last_name,
Person.mothers_first_name == kyc_in.mothers_first_name,
@@ -115,7 +102,6 @@ class AuthService:
else:
active_person = user.person
# 3. Címkezelés
addr_id = await GeoService.get_or_create_full_address(
db,
zip_code=kyc_in.address_zip,
@@ -126,7 +112,6 @@ class AuthService:
parcel_id=kyc_in.address_hrsz
)
# 4. Person adatok frissítése
active_person.mothers_last_name = kyc_in.mothers_last_name
active_person.mothers_first_name = kyc_in.mothers_first_name
active_person.birth_place = kyc_in.birth_place
@@ -137,7 +122,6 @@ class AuthService:
active_person.ice_contact = jsonable_encoder(kyc_in.ice_contact)
active_person.is_active = True
# 5. Új, izolált INDIVIDUAL szervezet (4.2.3) i18n beállításokkal
new_org = Organization(
full_name=f"{active_person.last_name} {active_person.first_name} Egyéni Flotta",
name=f"{active_person.last_name} Flotta",
@@ -146,7 +130,6 @@ class AuthService:
is_transferable=False,
is_active=True,
status="verified",
# Megörökölt adminisztrációs adatok
language=user.preferred_language,
default_currency=user.preferred_currency,
country_code=user.region_code
@@ -154,7 +137,6 @@ class AuthService:
db.add(new_org)
await db.flush()
# 6. Tagság és Jogosultságok
db.add(OrganizationMember(
organization_id=new_org.id,
user_id=user.id,
@@ -162,7 +144,6 @@ class AuthService:
permissions={"can_add_asset": True, "can_view_costs": True, "is_admin": True}
))
# 7. Wallet & Stats
db.add(Wallet(
user_id=user.id,
coin_balance=0,
@@ -171,7 +152,6 @@ class AuthService:
))
db.add(UserStats(user_id=user.id, total_xp=0, current_level=1))
# 8. Aktiválás
user.is_active = True
await db.commit()
@@ -182,6 +162,39 @@ class AuthService:
logger.error(f"KYC Atomi Tranzakció Hiba: {str(e)}")
raise e
@staticmethod
async def soft_delete_user(db: AsyncSession, user_id: int, reason: str, actor_id: int):
"""
Step 2 utáni Soft-Delete: Email felszabadítás és izoláció.
Az email átnevezésre kerül, így az eredeti cím újra regisztrálható 'tiszta lappal'.
"""
stmt = select(User).where(User.id == user_id)
user = (await db.execute(stmt)).scalar_one_or_none()
if not user or user.is_deleted:
return False
old_email = user.email
# Email felszabadítása: deleted_ID_TIMESTAMP_EMAIL formátumban
user.email = f"deleted_{user.id}_{datetime.now().strftime('%Y%m%d')}_{old_email}"
user.is_deleted = True
user.is_active = False
# Sentinel AuditLog bejegyzés
await security_service.log_event(
db,
user_id=actor_id,
action="USER_SOFT_DELETE",
severity="warning",
target_type="User",
target_id=str(user_id),
old_data={"email": old_email},
new_data={"is_deleted": True, "reason": reason}
)
await db.commit()
return True
@staticmethod
async def verify_email(db: AsyncSession, token_str: str):
try:
@@ -227,13 +240,12 @@ class AuthService:
expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reset_hours))
))
# --- EMAIL KÜLDÉSE A FELHASZNÁLÓ SAJÁT NYELVÉN ---
reset_link = f"{settings.FRONTEND_BASE_URL}/reset-password?token={token_val}"
await email_manager.send_email(
recipient=email,
template_key="pwd_reset", # hu.json: email.pwd_reset_subject stb.
template_key="pwd_reset",
variables={"link": reset_link},
lang=user.preferred_language # Adatbázisból kinyert nyelv
lang=user.preferred_language
)
await db.commit()
return "success"

View File

@@ -1,47 +1,106 @@
import logging
import math
from decimal import Decimal
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.gamification import UserStats, PointsLedger
import math
from app.models.identity import User, Wallet
from app.models.core_logic import CreditTransaction
from app.models.system_config import SystemParameter
logger = logging.getLogger(__name__)
class GamificationService:
@staticmethod
async def process_activity(db: AsyncSession, user_id: int, xp_amount: int, social_amount: int, reason: str):
"""
XP növelés, Szintlépés csekk és Automata Kredit váltás.
"""
# 1. User statisztika lekérése
stmt = select(UserStats).where(UserStats.user_id == user_id)
stats = (await db.execute(stmt)).scalar_one_or_none()
async def get_config(db: AsyncSession):
"""Kiolvassa a GAMIFICATION_MASTER_CONFIG-ot a rendszerparaméterekből."""
stmt = select(SystemParameter).where(SystemParameter.key == "GAMIFICATION_MASTER_CONFIG")
res = await db.execute(stmt)
param = res.scalar_one_or_none()
return param.value if param else {
"xp_logic": {"base_xp": 500, "exponent": 1.5},
"penalty_logic": {
"thresholds": {"level_1": 100, "level_2": 500, "level_3": 1000},
"multipliers": {"level_0": 1.0, "level_1": 0.5, "level_2": 0.1, "level_3": 0.0},
"recovery_rate": 0.5
},
"conversion_logic": {"social_to_credit_rate": 100},
"level_rewards": {"credits_per_10_levels": 50},
"blocked_roles": ["superadmin", "service_bot"]
}
async def process_activity(self, db: AsyncSession, user_id: int, xp_amount: int, social_amount: int, reason: str, is_penalty: bool = False):
"""A 'Bíró' logika: Ellenőriz, büntet, jutalmaz és szintez."""
config = await self.get_config(db)
# 1. Jogosultság ellenőrzése
user_stmt = select(User).where(User.id == user_id)
user = (await db.execute(user_stmt)).scalar_one_or_none()
if not user or user.is_deleted or user.role.value in config.get("blocked_roles", []):
return None
# 2. Stats lekérése
stats_stmt = select(UserStats).where(UserStats.user_id == user_id)
stats = (await db.execute(stats_stmt)).scalar_one_or_none()
if not stats:
stats = UserStats(user_id=user_id, total_xp=0, social_points=0, current_level=1, credits=0)
stats = UserStats(user_id=user_id)
db.add(stats)
# 2. Részletes Logolás (PointsLedger) - A visszakövethetőség miatt
db.add(PointsLedger(
user_id=user_id,
xp_gain=xp_amount,
social_gain=social_amount,
reason=reason
))
# 3. XP és Szintlépés (Nehezedő görbe)
stats.total_xp += xp_amount
# Képlet: Level = (XP / 500)^(1/1.5)
new_level = int((stats.total_xp / 500) ** (1/1.5)) + 1
if new_level > stats.current_level:
stats.current_level = new_level
# 4. Automata Kredit váltás
# Példa: Minden 100 Social pont automatikusan 1 Kredit lesz
stats.social_points += social_amount
if stats.social_points >= 100:
new_credits = stats.social_points // 100
stats.credits += new_credits
stats.social_points %= 100 # A maradék megmarad a következő váltáshoz
# 3. Büntető logika (Penalty)
if is_penalty:
stats.penalty_points += xp_amount
th = config["penalty_logic"]["thresholds"]
if stats.penalty_points >= th["level_3"]: stats.restriction_level = 3
elif stats.penalty_points >= th["level_2"]: stats.restriction_level = 2
elif stats.penalty_points >= th["level_1"]: stats.restriction_level = 1
# Külön log a váltásról
db.add(PointsLedger(user_id=user_id, reason=f"Auto-conversion: {new_credits} Credits", credits_change=new_credits))
db.add(PointsLedger(user_id=user_id, points=0, penalty_change=xp_amount, reason=f"PENALTY: {reason}"))
await db.commit()
return stats
# 4. Dinamikus szorzó alkalmazása
multipliers = config["penalty_logic"]["multipliers"]
multiplier = multipliers.get(f"level_{stats.restriction_level}", 1.0)
if multiplier <= 0:
logger.warning(f"User {user_id} activity blocked (Level {stats.restriction_level})")
return stats
# 5. XP, Ledolgozás és Szintlépés
final_xp = int(xp_amount * multiplier)
if final_xp > 0:
stats.total_xp += final_xp
if stats.penalty_points > 0:
rec_rate = config["penalty_logic"]["recovery_rate"]
stats.penalty_points = max(0, stats.penalty_points - int(final_xp * rec_rate))
xp_cfg = config["xp_logic"]
new_level = int((stats.total_xp / xp_cfg["base_xp"]) ** (1/xp_cfg["exponent"])) + 1
if new_level > stats.current_level:
if new_level % 10 == 0:
reward = config["level_rewards"]["credits_per_10_levels"]
await self._add_credits(db, user_id, reward, f"Level {new_level} Achievement Bonus")
stats.current_level = new_level
# 6. Social pont és váltás
final_social = int(social_amount * multiplier)
if final_social > 0:
stats.social_points += final_social
rate = config["conversion_logic"]["social_to_credit_rate"]
if stats.social_points >= rate:
new_credits = stats.social_points // rate
stats.social_points %= rate
await self._add_credits(db, user_id, new_credits, "Social conversion")
db.add(PointsLedger(user_id=user_id, points=final_xp, reason=reason))
await db.commit()
return stats
return stats
async def _add_credits(self, db: AsyncSession, user_id: int, amount: int, reason: str):
wallet_stmt = select(Wallet).where(Wallet.user_id == user_id)
wallet = (await db.execute(wallet_stmt)).scalar_one_or_none()
if wallet:
wallet.credit_balance += Decimal(amount)
db.add(CreditTransaction(org_id=None, amount=Decimal(amount), description=reason))
gamification_service = GamificationService()

View File

@@ -0,0 +1,169 @@
import logging
from datetime import datetime, timedelta
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.identity import User
from app.models.system_config import SystemParameter
logger = logging.getLogger(__name__)
class SecurityService:
@staticmethod
async def get_sec_config(db: AsyncSession) -> Dict[str, Any]:
"""Lekéri a biztonsági korlátokat a központi rendszerparaméterekből."""
keys = ["SECURITY_MAX_RECORDS_PER_HOUR", "SECURITY_DUAL_CONTROL_ENABLED"]
stmt = select(SystemParameter).where(SystemParameter.key.in_(keys))
res = await db.execute(stmt)
params = {p.key: p.value for p in res.scalars().all()}
return {
"max_records": int(params.get("SECURITY_MAX_RECORDS_PER_HOUR", 500)),
"dual_control": str(params.get("SECURITY_DUAL_CONTROL_ENABLED", "true")).lower() == "true"
}
# --- 1. SZINT: AUDIT & LOGGING (A Mindenlátó Szem) ---
async def log_event(
self,
db: AsyncSession,
user_id: Optional[int],
action: str,
severity: LogSeverity,
old_data: Optional[Dict] = None,
new_data: Optional[Dict] = None,
ip: Optional[str] = None,
ua: Optional[str] = None,
target_type: Optional[str] = None,
target_id: Optional[str] = None,
reason: Optional[str] = None
):
"""Minden rendszerművelet rögzítése és azonnali biztonsági elemzése."""
new_log = AuditLog(
user_id=user_id,
severity=severity,
action=action,
target_type=target_type,
target_id=target_id,
old_data=old_data,
new_data=new_data,
ip_address=ip,
user_agent=ua
)
db.add(new_log)
# Ha a szint EMERGENCY, azonnal lőjük le a júzert
if severity == LogSeverity.emergency:
await self._execute_emergency_lock(db, user_id, f"Auto-lock triggered by: {action}")
await db.commit()
# --- 2. SZINT: PENDING ACTIONS (Négy szem elv) ---
async def request_action(
self,
db: AsyncSession,
requester_id: int,
action_type: str,
payload: Dict,
reason: str
):
"""Kritikus művelet kezdeményezése jóváhagyásra (nem hajtódik végre azonnal)."""
new_action = PendingAction(
requester_id=requester_id,
action_type=action_type,
payload=payload,
reason=reason,
status=ActionStatus.pending
)
db.add(new_action)
await self.log_event(
db, requester_id,
action=f"REQUEST_{action_type}",
severity=LogSeverity.critical,
new_data=payload,
reason=f"Approval requested: {reason}"
)
await db.commit()
return new_action
async def approve_action(self, db: AsyncSession, approver_id: int, action_id: int):
"""Művelet végrehajtása egy második admin által."""
stmt = select(PendingAction).where(PendingAction.id == action_id)
action = (await db.execute(stmt)).scalar_one_or_none()
if not action or action.status != ActionStatus.pending:
raise Exception("A művelet nem található vagy már feldolgozták.")
if action.requester_id == approver_id:
raise Exception("Önmagad kérését nem hagyhatod jóvá! (Négy szem elv)")
# ITT TÖRTÉNIK A TÉNYLEGES ÜZLETI LOGIKA (Példa: Rangmódosítás)
if action.action_type == "CHANGE_ROLE":
user_id = action.payload.get("user_id")
new_role = action.payload.get("new_role")
user_stmt = select(User).where(User.id == user_id)
user = (await db.execute(user_stmt)).scalar_one_or_none()
if user:
user.role = new_role
logger.info(f"Role for user {user_id} changed to {new_role} via approved action {action_id}")
action.status = ActionStatus.approved
action.approver_id = approver_id
action.processed_at = func.now()
await self.log_event(
db, approver_id,
action=f"APPROVE_{action.action_type}",
severity=LogSeverity.info,
target_id=str(action.id),
reason=f"Approved action requested by {action.requester_id}"
)
await db.commit()
return True
# --- 3. SZINT: DATA THROTTLING & EMERGENCY LOCK ---
async def check_data_access_limit(self, db: AsyncSession, user_id: int):
"""Figyeli a tömeges adatlekérést (Adatlopás elleni védelem)."""
config = await self.get_sec_config(db)
one_hour_ago = datetime.now() - timedelta(hours=1)
# Megszámoljuk az utolsó egy óra GET (lekérési) logjait
stmt = select(func.count(AuditLog.id)).where(
and_(
AuditLog.user_id == user_id,
AuditLog.timestamp >= one_hour_ago,
AuditLog.action.like("GET_%")
)
)
count = (await db.execute(stmt)).scalar() or 0
if count > config["max_records"]:
await self.log_event(
db, user_id,
action="MASS_DATA_ACCESS_DETECTED",
severity=LogSeverity.emergency,
reason=f"Access count: {count} (Limit: {config['max_records']})"
)
# A log_event automatikusan hívja a _execute_emergency_lock-ot
return False
return True
async def _execute_emergency_lock(self, db: AsyncSession, user_id: int, reason: str):
"""Azonnali fiókfelfüggesztés vészhelyzet esetén."""
if not user_id: return
stmt = select(User).where(User.id == user_id)
user = (await db.execute(stmt)).scalar_one_or_none()
if user:
user.is_active = False
logger.critical(f"🚨 SECURITY EMERGENCY LOCK: User {user_id} suspended. Reason: {reason}")
# Itt lehetne bekötni egy külső SMS/Slack/Email riasztást
security_service = SecurityService()

View File

@@ -1,15 +1,21 @@
import json
import os
import logging
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
from app.models.translation import Translation
from typing import Dict
from app.core.config import settings
from typing import Dict, Any, Optional
logger = logging.getLogger(__name__)
class TranslationService:
# Ez a memória-cache tárolja az élesített szövegeket
# Memória-cache a szerveroldali hibaüzenetekhez és emailekhez
_published_cache: Dict[str, Dict[str, str]] = {}
@classmethod
async def load_cache(cls, db: AsyncSession):
"""Betölti az összes PUBLIKÁLT fordítást az adatbázisból a memóriába."""
"""Betölti a publikált szövegeket a memóriába az adatbázisból."""
result = await db.execute(
select(Translation).where(Translation.is_published == True)
)
@@ -20,27 +26,80 @@ class TranslationService:
if t.lang_code not in cls._published_cache:
cls._published_cache[t.lang_code] = {}
cls._published_cache[t.lang_code][t.key] = t.value
print(f"🌍 i18n Cache: {len(translations)} szöveg élesítve.")
logger.info(f"🌍 i18n Cache: {len(translations)} szöveg betöltve.")
@classmethod
def get_text(cls, key: str, lang: str = "en") -> str:
"""Villámgyors lekérés a memóriából Fallback logikával."""
# 1. Kért nyelv
def get_text(cls, key: str, lang: str = "hu", variables: Optional[Dict[str, Any]] = None) -> str:
"""
Szerveroldali lekérés Fallback (EN) logikával és változó behelyettesítéssel.
Példa: get_text("AUTH.WELCOME", "hu", {"name": "Péter"})
"""
# 1. Kért nyelv lekérése
text = cls._published_cache.get(lang, {}).get(key)
if text: return text
# 2. Fallback: Angol
if lang != "en":
text = cls._published_cache.get("en", {}).get(key)
if text: return text
return f"[{key}]"
# 2. Fallback angolra, ha nincs meg a kért nyelven
if not text and lang != "en":
text = cls._published_cache.get("en", {}).get(key)
# 3. Ha sehol nincs meg, adjuk vissza a kulcsot
if not text:
return f"[{key}]"
# 4. Változók behelyettesítése (pl. {{name}})
if variables:
for k, v in variables.items():
text = text.replace(f"{{{{{k}}}}}", str(v))
return text
@classmethod
async def publish_all(cls, db: AsyncSession):
"""Élesíti a piszkozatokat és frissíti a szerver memóriáját."""
"""Minden piszkozatot élesít, frissíti a memóriát és legenerálja a JSON-öket."""
await db.execute(
update(Translation).where(Translation.is_published == False).values(is_published=True)
)
await db.commit()
await cls.load_cache(db)
await cls.load_cache(db)
await cls.export_to_json(db)
@staticmethod
async def export_to_json(db: AsyncSession):
"""
Adatbázis -> Hierarchikus JSON export.
'AUTH.LOGIN.TITLE' -> { "AUTH": { "LOGIN": { "TITLE": "..." } } }
"""
stmt = select(Translation).where(Translation.is_published == True)
result = await db.execute(stmt)
translations = result.scalars().all()
languages: Dict[str, Any] = {}
for t in translations:
if t.lang_code not in languages:
languages[t.lang_code] = {}
# Hierarchikus struktúra felépítése
parts = t.key.split('.')
current_level = languages[t.lang_code]
for part in parts[:-1]:
if part not in current_level:
current_level[part] = {}
current_level = current_level[part]
current_level[parts[-1]] = t.value
# Fájlok mentése
locales_path = os.path.join(settings.STATIC_DIR, "locales")
os.makedirs(locales_path, exist_ok=True)
for lang, content in languages.items():
file_path = os.path.join(locales_path, f"{lang}.json")
try:
with open(file_path, "w", encoding="utf-8") as f:
json.dump(content, f, ensure_ascii=False, indent=2)
logger.info(f"🚀 JSON legenerálva: {file_path}")
except Exception as e:
logger.error(f"Fájl hiba ({lang}): {str(e)}")
return True
translation_service = TranslationService()