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:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
169
backend/app/services/security_service.py
Normal file
169
backend/app/services/security_service.py
Normal 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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user