diff --git a/backend/app/api/__pycache__/deps.cpython-312.pyc b/backend/app/api/__pycache__/deps.cpython-312.pyc index b571516..8e23a62 100644 Binary files a/backend/app/api/__pycache__/deps.cpython-312.pyc and b/backend/app/api/__pycache__/deps.cpython-312.pyc differ diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py index 2550451..9cab3b9 100755 --- a/backend/app/api/deps.py +++ b/backend/app/api/deps.py @@ -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: diff --git a/backend/app/api/v1/__pycache__/api.cpython-312.pyc b/backend/app/api/v1/__pycache__/api.cpython-312.pyc index 87e4886..d7138ee 100644 Binary files a/backend/app/api/v1/__pycache__/api.cpython-312.pyc and b/backend/app/api/v1/__pycache__/api.cpython-312.pyc differ diff --git a/backend/app/api/v1/api.py b/backend/app/api/v1/api.py index 656ab36..fedd04f 100755 --- a/backend/app/api/v1/api.py +++ b/backend/app/api/v1/api.py @@ -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"]) \ No newline at end of file +# 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)"]) \ No newline at end of file diff --git a/backend/app/api/v1/endpoints/__pycache__/admin.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/admin.cpython-312.pyc new file mode 100644 index 0000000..cdb564f Binary files /dev/null and b/backend/app/api/v1/endpoints/__pycache__/admin.cpython-312.pyc differ diff --git a/backend/app/api/v1/endpoints/admin.py b/backend/app/api/v1/endpoints/admin.py index 987241f..6f555c1 100755 --- a/backend/app/api/v1/endpoints/admin.py +++ b/backend/app/api/v1/endpoints/admin.py @@ -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."} - \ No newline at end of file + """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."} \ No newline at end of file diff --git a/backend/app/core/__pycache__/config.cpython-312.pyc b/backend/app/core/__pycache__/config.cpython-312.pyc index 277aaa3..0da2b50 100644 Binary files a/backend/app/core/__pycache__/config.cpython-312.pyc and b/backend/app/core/__pycache__/config.cpython-312.pyc differ diff --git a/backend/app/core/config.py b/backend/app/core/config.py index cb6770c..e19274e 100755 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -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!" diff --git a/backend/app/db/__pycache__/base.cpython-312.pyc b/backend/app/db/__pycache__/base.cpython-312.pyc index ac879e9..e14e879 100644 Binary files a/backend/app/db/__pycache__/base.cpython-312.pyc and b/backend/app/db/__pycache__/base.cpython-312.pyc differ diff --git a/backend/app/db/base.py b/backend/app/db/base.py index 219f02c..b3b2d5f 100755 --- a/backend/app/db/base.py +++ b/backend/app/db/base.py @@ -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 -) \ No newline at end of file +) +from app.models.security import PendingAction # noqa <--- CSAK A BIZTONSÁG KEDVÉÉRT, HA EZ IS HIÁNYZOTT VOLNA \ No newline at end of file diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 3b19ade..aa41faa 100755 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -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" ] \ No newline at end of file diff --git a/backend/app/models/__pycache__/__init__.cpython-312.pyc b/backend/app/models/__pycache__/__init__.cpython-312.pyc index b41676a..3ce077e 100644 Binary files a/backend/app/models/__pycache__/__init__.cpython-312.pyc and b/backend/app/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/asset.cpython-312.pyc b/backend/app/models/__pycache__/asset.cpython-312.pyc index e7435c1..5e69cee 100644 Binary files a/backend/app/models/__pycache__/asset.cpython-312.pyc and b/backend/app/models/__pycache__/asset.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/gamification.cpython-312.pyc b/backend/app/models/__pycache__/gamification.cpython-312.pyc index 0ded220..ac42ebd 100644 Binary files a/backend/app/models/__pycache__/gamification.cpython-312.pyc and b/backend/app/models/__pycache__/gamification.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/history.cpython-312.pyc b/backend/app/models/__pycache__/history.cpython-312.pyc index db83aa0..4767261 100644 Binary files a/backend/app/models/__pycache__/history.cpython-312.pyc and b/backend/app/models/__pycache__/history.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/identity.cpython-312.pyc b/backend/app/models/__pycache__/identity.cpython-312.pyc index c791879..4e6e0e8 100644 Binary files a/backend/app/models/__pycache__/identity.cpython-312.pyc and b/backend/app/models/__pycache__/identity.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/security.cpython-312.pyc b/backend/app/models/__pycache__/security.cpython-312.pyc new file mode 100644 index 0000000..b8e14e3 Binary files /dev/null and b/backend/app/models/__pycache__/security.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/translation.cpython-312.pyc b/backend/app/models/__pycache__/translation.cpython-312.pyc new file mode 100644 index 0000000..22ff00a Binary files /dev/null and b/backend/app/models/__pycache__/translation.cpython-312.pyc differ diff --git a/backend/app/models/asset.py b/backend/app/models/asset.py index 783dd76..93b7dea 100644 --- a/backend/app/models/asset.py +++ b/backend/app/models/asset.py @@ -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" diff --git a/backend/app/models/gamification.py b/backend/app/models/gamification.py index 4c92c49..2a5c6de 100755 --- a/backend/app/models/gamification.py +++ b/backend/app/models/gamification.py @@ -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) diff --git a/backend/app/models/history.py b/backend/app/models/history.py index 8d4d21d..0c74bc7 100755 --- a/backend/app/models/history.py +++ b/backend/app/models/history.py @@ -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") \ No newline at end of file diff --git a/backend/app/models/identity.py b/backend/app/models/identity.py index 9d53ffc..a6f2ac4 100644 --- a/backend/app/models/identity.py +++ b/backend/app/models/identity.py @@ -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"} diff --git a/backend/app/models/security.py b/backend/app/models/security.py new file mode 100644 index 0000000..94c493b --- /dev/null +++ b/backend/app/models/security.py @@ -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]) \ No newline at end of file diff --git a/backend/app/models/translation.py b/backend/app/models/translation.py index a1f1b5a..7f164c0 100755 --- a/backend/app/models/translation.py +++ b/backend/app/models/translation.py @@ -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) \ No newline at end of file diff --git a/backend/app/schemas/__pycache__/admin_security.cpython-312.pyc b/backend/app/schemas/__pycache__/admin_security.cpython-312.pyc new file mode 100644 index 0000000..cd1b5b2 Binary files /dev/null and b/backend/app/schemas/__pycache__/admin_security.cpython-312.pyc differ diff --git a/backend/app/schemas/admin_security.py b/backend/app/schemas/admin_security.py new file mode 100644 index 0000000..99e7ba1 --- /dev/null +++ b/backend/app/schemas/admin_security.py @@ -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 \ No newline at end of file diff --git a/backend/app/services/__pycache__/auth_service.cpython-312.pyc b/backend/app/services/__pycache__/auth_service.cpython-312.pyc index 091f5b5..2ed4e66 100644 Binary files a/backend/app/services/__pycache__/auth_service.cpython-312.pyc and b/backend/app/services/__pycache__/auth_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/gamification_service.cpython-312.pyc b/backend/app/services/__pycache__/gamification_service.cpython-312.pyc index 04264e8..923b6b5 100644 Binary files a/backend/app/services/__pycache__/gamification_service.cpython-312.pyc and b/backend/app/services/__pycache__/gamification_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/security_service.cpython-312.pyc b/backend/app/services/__pycache__/security_service.cpython-312.pyc new file mode 100644 index 0000000..8c6b61f Binary files /dev/null and b/backend/app/services/__pycache__/security_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/translation_service.cpython-312.pyc b/backend/app/services/__pycache__/translation_service.cpython-312.pyc new file mode 100644 index 0000000..c869625 Binary files /dev/null and b/backend/app/services/__pycache__/translation_service.cpython-312.pyc differ diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py index 78717fc..f67a5a4 100644 --- a/backend/app/services/auth_service.py +++ b/backend/app/services/auth_service.py @@ -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" diff --git a/backend/app/services/gamification_service.py b/backend/app/services/gamification_service.py index 1789d59..33d3000 100755 --- a/backend/app/services/gamification_service.py +++ b/backend/app/services/gamification_service.py @@ -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 \ No newline at end of file + 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() \ No newline at end of file diff --git a/backend/app/services/security_service.py b/backend/app/services/security_service.py new file mode 100644 index 0000000..a9737f1 --- /dev/null +++ b/backend/app/services/security_service.py @@ -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() \ No newline at end of file diff --git a/backend/app/services/translation_service.py b/backend/app/services/translation_service.py index 4efbb18..abbe1e9 100755 --- a/backend/app/services/translation_service.py +++ b/backend/app/services/translation_service.py @@ -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) \ No newline at end of file + 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() \ No newline at end of file diff --git a/backend/migrations/versions/134d92edd430_create_translation_and_security_tables.py b/backend/migrations/versions/134d92edd430_create_translation_and_security_tables.py new file mode 100644 index 0000000..a8ff5a1 --- /dev/null +++ b/backend/migrations/versions/134d92edd430_create_translation_and_security_tables.py @@ -0,0 +1,210 @@ +"""create_translation_and_security_tables + +Revision ID: 134d92edd430 +Revises: bc5669f12ffd +Create Date: 2026-02-10 20:04:23.924164 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '134d92edd430' +down_revision: Union[str, Sequence[str], None] = 'bc5669f12ffd' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('translations', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('key', sa.String(length=100), nullable=False), + sa.Column('lang_code', sa.String(length=5), nullable=False), + sa.Column('value', sa.Text(), nullable=False), + sa.Column('is_published', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('key', 'lang_code', name='uq_translation_key_lang'), + schema='data' + ) + op.create_index(op.f('ix_data_translations_id'), 'translations', ['id'], unique=False, schema='data') + op.create_index(op.f('ix_data_translations_key'), 'translations', ['key'], unique=False, schema='data') + op.create_index(op.f('ix_data_translations_lang_code'), 'translations', ['lang_code'], unique=False, schema='data') + op.create_table('pending_actions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('requester_id', sa.Integer(), nullable=False), + sa.Column('approver_id', sa.Integer(), nullable=True), + sa.Column('status', sa.Enum('pending', 'approved', 'rejected', 'expired', name='actionstatus'), nullable=False), + sa.Column('action_type', sa.String(length=50), nullable=False), + sa.Column('payload', sa.JSON(), nullable=False), + sa.Column('reason', sa.String(length=255), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('processed_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['approver_id'], ['data.users.id'], ), + sa.ForeignKeyConstraint(['requester_id'], ['data.users.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_index(op.f('ix_data_pending_actions_id'), 'pending_actions', ['id'], unique=False, schema='data') + op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey') + op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey') + op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey') + op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey') + op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey') + op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey') + op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey') + op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey') + op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey') + op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey') + op.create_foreign_key(None, 'assets', 'vehicle_catalog', ['catalog_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('audit_logs_user_id_fkey'), 'audit_logs', type_='foreignkey') + op.create_foreign_key(None, 'audit_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('credit_logs_org_id_fkey'), 'credit_logs', type_='foreignkey') + op.create_foreign_key(None, 'credit_logs', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('documents_uploaded_by_fkey'), 'documents', type_='foreignkey') + op.create_foreign_key(None, 'documents', 'users', ['uploaded_by'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey') + op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.create_foreign_key(None, 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'org_subscriptions', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('organization_members_user_id_fkey'), 'organization_members', type_='foreignkey') + op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey') + op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + existing_nullable=True) + op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey') + op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey') + op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey') + op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey') + op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey') + op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('service_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey') + op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey') + op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey') + op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey') + op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey') + op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey') + op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE') + op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey') + op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id']) + op.drop_constraint(None, 'users', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id']) + op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id']) + op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id']) + op.drop_constraint(None, 'ratings', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id']) + op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'persons', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id']) + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id']) + op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id']) + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + existing_nullable=True) + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organization_members_organization_id_fkey'), 'organization_members', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('organization_members_user_id_fkey'), 'organization_members', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id']) + op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id']) + op.drop_constraint(None, 'documents', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id']) + op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'assets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id']) + op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id']) + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id']) + op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id']) + op.drop_index(op.f('ix_data_pending_actions_id'), table_name='pending_actions', schema='data') + op.drop_table('pending_actions', schema='data') + op.drop_index(op.f('ix_data_translations_lang_code'), table_name='translations', schema='data') + op.drop_index(op.f('ix_data_translations_key'), table_name='translations', schema='data') + op.drop_index(op.f('ix_data_translations_id'), table_name='translations', schema='data') + op.drop_table('translations', schema='data') + # ### end Alembic commands ### diff --git a/backend/migrations/versions/6197bfddfb4f_add_lang_and_region_to_user.py b/backend/migrations/versions/6197bfddfb4f_add_lang_and_region_to_user.py new file mode 100644 index 0000000..7d0c857 --- /dev/null +++ b/backend/migrations/versions/6197bfddfb4f_add_lang_and_region_to_user.py @@ -0,0 +1,186 @@ +"""add_lang_and_region_to_user + +Revision ID: 6197bfddfb4f +Revises: 134d92edd430 +Create Date: 2026-02-10 20:46:57.170479 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '6197bfddfb4f' +down_revision: Union[str, Sequence[str], None] = '134d92edd430' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey') + op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey') + op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey') + op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey') + op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey') + op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey') + op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey') + op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey') + op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey') + op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey') + op.create_foreign_key(None, 'assets', 'vehicle_catalog', ['catalog_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('audit_logs_user_id_fkey'), 'audit_logs', type_='foreignkey') + op.create_foreign_key(None, 'audit_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('credit_logs_org_id_fkey'), 'credit_logs', type_='foreignkey') + op.create_foreign_key(None, 'credit_logs', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('documents_uploaded_by_fkey'), 'documents', type_='foreignkey') + op.create_foreign_key(None, 'documents', 'users', ['uploaded_by'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey') + op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.create_foreign_key(None, 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'org_subscriptions', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('organization_members_user_id_fkey'), 'organization_members', type_='foreignkey') + op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey') + op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + existing_nullable=True) + op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey') + op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey') + op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('pending_actions_approver_id_fkey'), 'pending_actions', type_='foreignkey') + op.drop_constraint(op.f('pending_actions_requester_id_fkey'), 'pending_actions', type_='foreignkey') + op.create_foreign_key(None, 'pending_actions', 'users', ['requester_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'pending_actions', 'users', ['approver_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey') + op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey') + op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey') + op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('service_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey') + op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey') + op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey') + op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey') + op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.add_column('users', sa.Column('preferred_language', sa.String(length=5), server_default='hu', nullable=True)) + op.add_column('users', sa.Column('region_code', sa.String(length=5), server_default='HU', nullable=True)) + op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey') + op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey') + op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE') + op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey') + op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id']) + op.drop_constraint(None, 'users', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id']) + op.drop_column('users', 'region_code') + op.drop_column('users', 'preferred_language') + op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id']) + op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id']) + op.drop_constraint(None, 'ratings', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id']) + op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'persons', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id']) + op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('pending_actions_requester_id_fkey'), 'pending_actions', 'users', ['requester_id'], ['id']) + op.create_foreign_key(op.f('pending_actions_approver_id_fkey'), 'pending_actions', 'users', ['approver_id'], ['id']) + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id']) + op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id']) + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + existing_nullable=True) + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organization_members_organization_id_fkey'), 'organization_members', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('organization_members_user_id_fkey'), 'organization_members', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id']) + op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id']) + op.drop_constraint(None, 'documents', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id']) + op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'assets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id']) + op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id']) + # ### end Alembic commands ### diff --git a/backend/migrations/versions/8e06c5386cba_finalize_gamification_v1_5_clean.py b/backend/migrations/versions/8e06c5386cba_finalize_gamification_v1_5_clean.py new file mode 100644 index 0000000..d0a495d --- /dev/null +++ b/backend/migrations/versions/8e06c5386cba_finalize_gamification_v1_5_clean.py @@ -0,0 +1,200 @@ +"""finalize_gamification_v1.5_clean + +Revision ID: 8e06c5386cba +Revises: 2cfe9285eb9d +Create Date: 2026-02-10 16:18:15.900078 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '8e06c5386cba' +down_revision: Union[str, Sequence[str], None] = '2cfe9285eb9d' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey') + op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey') + op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey') + op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey') + op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey') + op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey') + op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey') + op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey') + op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey') + op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey') + op.create_foreign_key(None, 'assets', 'vehicle_catalog', ['catalog_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('audit_logs_user_id_fkey'), 'audit_logs', type_='foreignkey') + op.create_foreign_key(None, 'audit_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('credit_logs_org_id_fkey'), 'credit_logs', type_='foreignkey') + op.create_foreign_key(None, 'credit_logs', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('documents_uploaded_by_fkey'), 'documents', type_='foreignkey') + op.create_foreign_key(None, 'documents', 'users', ['uploaded_by'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey') + op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.create_foreign_key(None, 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'org_subscriptions', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('organization_members_user_id_fkey'), 'organization_members', type_='foreignkey') + op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey') + op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + existing_nullable=True) + op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey') + op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey') + op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey') + op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_column('persons', 'medical_emergency') + op.drop_column('persons', 'birth_date') + op.drop_column('persons', 'ice_contact') + op.drop_column('persons', 'birth_place') + op.drop_column('persons', 'mothers_first_name') + op.drop_column('persons', 'mothers_last_name') + op.add_column('points_ledger', sa.Column('penalty_change', sa.Integer(), server_default=sa.text('0'), nullable=False)) + op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey') + op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey') + op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('service_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey') + op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey') + op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey') + op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data') + op.add_column('user_stats', sa.Column('penalty_points', sa.Integer(), server_default=sa.text('0'), nullable=False)) + op.add_column('user_stats', sa.Column('restriction_level', sa.Integer(), server_default=sa.text('0'), nullable=False)) + op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey') + op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey') + op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_column('users', 'region_code') + op.drop_column('users', 'preferred_language') + op.drop_column('users', 'preferred_currency') + op.drop_column('users', 'timezone') + op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey') + op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE') + op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey') + op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id']) + op.add_column('users', sa.Column('timezone', sa.VARCHAR(length=50), autoincrement=False, nullable=True)) + op.add_column('users', sa.Column('preferred_currency', sa.VARCHAR(length=3), autoincrement=False, nullable=True)) + op.add_column('users', sa.Column('preferred_language', sa.VARCHAR(length=5), autoincrement=False, nullable=True)) + op.add_column('users', sa.Column('region_code', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'users', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id']) + op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id']) + op.drop_column('user_stats', 'restriction_level') + op.drop_column('user_stats', 'penalty_points') + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id']) + op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id']) + op.drop_constraint(None, 'ratings', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id']) + op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id']) + op.drop_column('points_ledger', 'penalty_change') + op.add_column('persons', sa.Column('mothers_last_name', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column('persons', sa.Column('mothers_first_name', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column('persons', sa.Column('birth_place', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column('persons', sa.Column('ice_contact', postgresql.JSON(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), autoincrement=False, nullable=True)) + op.add_column('persons', sa.Column('birth_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=True)) + op.add_column('persons', sa.Column('medical_emergency', postgresql.JSON(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'persons', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id']) + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id']) + op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id']) + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + existing_nullable=True) + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organization_members_organization_id_fkey'), 'organization_members', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('organization_members_user_id_fkey'), 'organization_members', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id']) + op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id']) + op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id']) + op.drop_constraint(None, 'documents', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id']) + op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'assets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id']) + op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id']) + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id']) + # ### end Alembic commands ### diff --git a/backend/migrations/versions/__pycache__/134d92edd430_create_translation_and_security_tables.cpython-312.pyc b/backend/migrations/versions/__pycache__/134d92edd430_create_translation_and_security_tables.cpython-312.pyc new file mode 100644 index 0000000..82c5ab7 Binary files /dev/null and b/backend/migrations/versions/__pycache__/134d92edd430_create_translation_and_security_tables.cpython-312.pyc differ diff --git a/backend/migrations/versions/__pycache__/16d09fe82f82_fix_all_circular_imports_and_finalize_.cpython-312.pyc b/backend/migrations/versions/__pycache__/16d09fe82f82_fix_all_circular_imports_and_finalize_.cpython-312.pyc deleted file mode 100644 index 5933b88..0000000 Binary files a/backend/migrations/versions/__pycache__/16d09fe82f82_fix_all_circular_imports_and_finalize_.cpython-312.pyc and /dev/null differ diff --git a/backend/migrations/versions/__pycache__/5ca03588ce28_add_penalty_system_and_wallet_.cpython-312.pyc b/backend/migrations/versions/__pycache__/5ca03588ce28_add_penalty_system_and_wallet_.cpython-312.pyc new file mode 100644 index 0000000..874f99b Binary files /dev/null and b/backend/migrations/versions/__pycache__/5ca03588ce28_add_penalty_system_and_wallet_.cpython-312.pyc differ diff --git a/backend/migrations/versions/__pycache__/6197bfddfb4f_add_lang_and_region_to_user.cpython-312.pyc b/backend/migrations/versions/__pycache__/6197bfddfb4f_add_lang_and_region_to_user.cpython-312.pyc new file mode 100644 index 0000000..c2e6e97 Binary files /dev/null and b/backend/migrations/versions/__pycache__/6197bfddfb4f_add_lang_and_region_to_user.cpython-312.pyc differ diff --git a/backend/migrations/versions/__pycache__/8e06c5386cba_finalize_gamification_v1_5_clean.cpython-312.pyc b/backend/migrations/versions/__pycache__/8e06c5386cba_finalize_gamification_v1_5_clean.cpython-312.pyc new file mode 100644 index 0000000..1804c83 Binary files /dev/null and b/backend/migrations/versions/__pycache__/8e06c5386cba_finalize_gamification_v1_5_clean.cpython-312.pyc differ diff --git a/backend/migrations/versions/__pycache__/bc5669f12ffd_add_pending_actions_for_dual_control.cpython-312.pyc b/backend/migrations/versions/__pycache__/bc5669f12ffd_add_pending_actions_for_dual_control.cpython-312.pyc new file mode 100644 index 0000000..b891b20 Binary files /dev/null and b/backend/migrations/versions/__pycache__/bc5669f12ffd_add_pending_actions_for_dual_control.cpython-312.pyc differ diff --git a/backend/migrations/versions/__pycache__/ffffad1dbe37_upgrade_audit_log_for_security.cpython-312.pyc b/backend/migrations/versions/__pycache__/ffffad1dbe37_upgrade_audit_log_for_security.cpython-312.pyc new file mode 100644 index 0000000..497dad5 Binary files /dev/null and b/backend/migrations/versions/__pycache__/ffffad1dbe37_upgrade_audit_log_for_security.cpython-312.pyc differ diff --git a/backend/migrations/versions/bc5669f12ffd_add_pending_actions_for_dual_control.py b/backend/migrations/versions/bc5669f12ffd_add_pending_actions_for_dual_control.py new file mode 100644 index 0000000..0328b64 --- /dev/null +++ b/backend/migrations/versions/bc5669f12ffd_add_pending_actions_for_dual_control.py @@ -0,0 +1,174 @@ +"""add_pending_actions_for_dual_control + +Revision ID: bc5669f12ffd +Revises: ffffad1dbe37 +Create Date: 2026-02-10 17:43:45.357771 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'bc5669f12ffd' +down_revision: Union[str, Sequence[str], None] = 'ffffad1dbe37' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey') + op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey') + op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey') + op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey') + op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey') + op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey') + op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey') + op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey') + op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey') + op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey') + op.create_foreign_key(None, 'assets', 'vehicle_catalog', ['catalog_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('audit_logs_user_id_fkey'), 'audit_logs', type_='foreignkey') + op.create_foreign_key(None, 'audit_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('credit_logs_org_id_fkey'), 'credit_logs', type_='foreignkey') + op.create_foreign_key(None, 'credit_logs', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('documents_uploaded_by_fkey'), 'documents', type_='foreignkey') + op.create_foreign_key(None, 'documents', 'users', ['uploaded_by'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey') + op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.create_foreign_key(None, 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'org_subscriptions', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('organization_members_user_id_fkey'), 'organization_members', type_='foreignkey') + op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey') + op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + existing_nullable=True) + op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey') + op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey') + op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey') + op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey') + op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey') + op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('service_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey') + op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey') + op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey') + op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey') + op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey') + op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey') + op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE') + op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey') + op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id']) + op.drop_constraint(None, 'users', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id']) + op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id']) + op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id']) + op.drop_constraint(None, 'ratings', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id']) + op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'persons', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id']) + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id']) + op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id']) + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + existing_nullable=True) + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organization_members_organization_id_fkey'), 'organization_members', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('organization_members_user_id_fkey'), 'organization_members', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id']) + op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id']) + op.drop_constraint(None, 'documents', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id']) + op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'assets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id']) + op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id']) + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id']) + # ### end Alembic commands ### diff --git a/backend/migrations/versions/ffffad1dbe37_upgrade_audit_log_for_security.py b/backend/migrations/versions/ffffad1dbe37_upgrade_audit_log_for_security.py new file mode 100644 index 0000000..5c97066 --- /dev/null +++ b/backend/migrations/versions/ffffad1dbe37_upgrade_audit_log_for_security.py @@ -0,0 +1,192 @@ +"""upgrade_audit_log_for_security + +Revision ID: ffffad1dbe37 +Revises: 8e06c5386cba +Create Date: 2026-02-10 17:33:17.436161 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'ffffad1dbe37' +down_revision: Union[str, Sequence[str], None] = '8e06c5386cba' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey') + op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey') + op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey') + op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey') + op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey') + op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey') + op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey') + op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey') + op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey') + op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey') + op.create_foreign_key(None, 'assets', 'vehicle_catalog', ['catalog_id'], ['id'], source_schema='data', referent_schema='data') + op.add_column('audit_logs', sa.Column('severity', sa.Enum('info', 'warning', 'critical', 'emergency', name='logseverity'), nullable=False)) + op.add_column('audit_logs', sa.Column('old_data', sa.JSON(), nullable=True)) + op.add_column('audit_logs', sa.Column('new_data', sa.JSON(), nullable=True)) + op.add_column('audit_logs', sa.Column('ip_address', sa.String(length=45), nullable=True)) + op.add_column('audit_logs', sa.Column('user_agent', sa.Text(), nullable=True)) + op.create_index(op.f('ix_data_audit_logs_action'), 'audit_logs', ['action'], unique=False, schema='data') + op.create_index(op.f('ix_data_audit_logs_ip_address'), 'audit_logs', ['ip_address'], unique=False, schema='data') + op.create_index(op.f('ix_data_audit_logs_timestamp'), 'audit_logs', ['timestamp'], unique=False, schema='data') + op.drop_constraint(op.f('audit_logs_user_id_fkey'), 'audit_logs', type_='foreignkey') + op.create_foreign_key(None, 'audit_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_column('audit_logs', 'changes') + op.drop_constraint(op.f('credit_logs_org_id_fkey'), 'credit_logs', type_='foreignkey') + op.create_foreign_key(None, 'credit_logs', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('documents_uploaded_by_fkey'), 'documents', type_='foreignkey') + op.create_foreign_key(None, 'documents', 'users', ['uploaded_by'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey') + op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.create_foreign_key(None, 'org_subscriptions', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('organization_members_user_id_fkey'), 'organization_members', type_='foreignkey') + op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey') + op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + existing_nullable=True) + op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey') + op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey') + op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey') + op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey') + op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey') + op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('service_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey') + op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey') + op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey') + op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey') + op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey') + op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey') + op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE') + op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey') + op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id']) + op.drop_constraint(None, 'users', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id']) + op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id']) + op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id']) + op.drop_constraint(None, 'ratings', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id']) + op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'persons', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id']) + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id']) + op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id']) + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + existing_nullable=True) + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organization_members_organization_id_fkey'), 'organization_members', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('organization_members_user_id_fkey'), 'organization_members', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id']) + op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id']) + op.drop_constraint(None, 'documents', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id']) + op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id']) + op.add_column('audit_logs', sa.Column('changes', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id']) + op.drop_index(op.f('ix_data_audit_logs_timestamp'), table_name='audit_logs', schema='data') + op.drop_index(op.f('ix_data_audit_logs_ip_address'), table_name='audit_logs', schema='data') + op.drop_index(op.f('ix_data_audit_logs_action'), table_name='audit_logs', schema='data') + op.drop_column('audit_logs', 'user_agent') + op.drop_column('audit_logs', 'ip_address') + op.drop_column('audit_logs', 'new_data') + op.drop_column('audit_logs', 'old_data') + op.drop_column('audit_logs', 'severity') + op.drop_constraint(None, 'assets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id']) + op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id']) + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id']) + # ### end Alembic commands ### diff --git a/backend/static/locales/en.json b/backend/static/locales/en.json new file mode 100644 index 0000000..ebb70b5 --- /dev/null +++ b/backend/static/locales/en.json @@ -0,0 +1,23 @@ +{ + "AUTH": { + "LOGIN": { + "SUCCESS": "Login successful. Welcome back!" + }, + "ERROR": { + "EMAIL_EXISTS": "This email is already registered." + } + }, + "SENTINEL": { + "LOCK": { + "MSG": "Account locked for security reasons." + } + }, + "EMAIL": { + "REG": { + "SUBJECT": "Confirm your registration" + }, + "PWD_RESET": { + "SUBJECT": "Password Reset Request" + } + } +} \ No newline at end of file diff --git a/backend/static/locales/hu.json b/backend/static/locales/hu.json new file mode 100644 index 0000000..4f623ec --- /dev/null +++ b/backend/static/locales/hu.json @@ -0,0 +1,31 @@ +{ + "AUTH": { + "LOGIN": { + "SUCCESS": "Sikeres bejelentkezés. Üdvözlünk ismét!" + }, + "ERROR": { + "EMAIL_EXISTS": "Ez az e-mail cím már foglalt.", + "UNAUTHORIZED": "Nincs jogosultságod a művelethez." + } + }, + "SENTINEL": { + "LOCK": { + "MSG": "A fiók biztonsági okokból zárolva lett." + }, + "APPROVAL": { + "REQUIRED": "A művelet végrehajtásához egy másik admin jóváhagyása szükséges." + } + }, + "EMAIL": { + "REG": { + "SUBJECT": "Regisztráció megerősítése - Service Finder", + "GREETING": "Szia {{first_name}}! Kattints a linkre a flottád aktiválásához: {{link}}" + }, + "PWD_RESET": { + "SUBJECT": "Jelszó visszaállítás" + } + }, + "COMMON": { + "SAVE_SUCCESS": "Sikeres mentés!" + } +} \ No newline at end of file diff --git a/docs/V01_gemini/11_Gamification_Social.md b/docs/V01_gemini/11_Gamification_Social.md index 93bd6fc..e910ab1 100644 --- a/docs/V01_gemini/11_Gamification_Social.md +++ b/docs/V01_gemini/11_Gamification_Social.md @@ -101,4 +101,47 @@ A rendszer különválasztja a tekintélyt és a jutalmat: Minden érték (szorzók, határok) a \`GAMIFICATION_MASTER_CONFIG\` JSON paraméterben állítható Admin felületről, kódmódosítás nélkül. ### 3. Audit -Minden pontmozgás a \`PointsLedger\` táblába kerül rögzítésre a visszakövethetőség érdekében. \ No newline at end of file +Minden pontmozgás a \`PointsLedger\` táblába kerül rögzítésre a visszakövethetőség érdekében. + +XP Formula: $XP_{required} = BaseXP \times Level^{1.5}$Penalty Logic: restriction_level bevezetése (0-3).Weighting: Saját adat vs. Közösségi adat súlyozási táblázata. + +# 11. Gamification és Social Engine Specifikáció + +## 1. XP (Experience Points) - A Tekintély +Az XP a felhasználó végleges, nem csökkenthető tekintélypontja. +- **Képlet:** A szintlépéshez szükséges összes XP: + $$XP_{total} = 500 \times Level^{1.5}$$ +- **Súlyozás:** + - **Saját adat (Fleet):** Alacsony érték (pl. 10 XP). + - **Közösségi adat (Service Discovery):** Magas érték (pl. 100 XP). + +## 2. Social Points - A Valuta Alapja +Szezonális pontok, amelyek Kreditre válthatóak. +- **Váltószám:** Alapértelmezett: 100 Social Point = 1 Kredit. +- **Váltási mód:** Automatikus (rendszerparaméter alapján) vagy manuális (felhasználói döntés). + +## 3. Trust & Penalty Engine (Büntetőrendszer) +A rendszer integritásának védelme érdekében hibapontokat (Penalty Points) alkalmazunk. +- **Szintek (Restriction Level):** + - **0 (Normal):** Teljes pontszorzó (1.0x). + - **1 (Warning):** Csökkentett pontszerzés (0.5x). + - **2 (Restricted):** Szigorú moderátori ellenőrzés minden adatnál, 0.1x pontszerzés. + - **3 (Blocked):** Pontszerzés és adatbeküldés tiltva. +- **Ledolgozás:** Minden pozitív XP szerzés a büntetőpontokat is csökkenti (pl. 1 XP jóváírás = 0.5 Penalty pont levonás). + +## 4. Szintlépési Bónuszok +Minden 10. szint elérésekor a rendszer automatikus Kredit jutalmat oszt a `GAMIFICATION_MASTER_CONFIG` alapján. + +## 5. Büntetőrendszer (Strike System) +A rendszer integritásának megőrzése érdekében hibapontokat alkalmazunk, amelyek befolyásolják a pontszerzés hatékonyságát. + +- **Szorzók (Multipliers):** + - Level 0 (Normal): 1.0x + - Level 1 (Warning): 0.5x + - Level 2 (Restricted): 0.1x + - Level 3 (Blocked): 0.0x + +- **Ledolgozás (Recovery):** + A büntetőpontok pozitív aktivitással (XP szerzéssel) ledolgozhatóak. Az elért XP egy admin által meghatározott része (alapértelmezett: 50%) levonásra kerül a büntetőpontokból. + +- **Admin-Vezérelt Küszöbök:** Minden szintváltási határ a `GAMIFICATION_MASTER_CONFIG` paraméterben definiált. \ No newline at end of file diff --git a/docs/V01_gemini/15_Changelog.md b/docs/V01_gemini/15_Changelog.md index c03cedb..e4fd6c9 100644 --- a/docs/V01_gemini/15_Changelog.md +++ b/docs/V01_gemini/15_Changelog.md @@ -249,4 +249,41 @@ A rendszer most már képes egyetlen KYC folyamat alatt aktiválni a felhasznál ### Changed - `AssetCost` modell mezőnevek szinkronizálva a pénzügyi standardokhoz (`amount_local`, `amount_eur`). -- `SystemParameter` modell elnevezés igazítva a meglévő adatbázis sémához. \ No newline at end of file +- `SystemParameter` modell elnevezés igazítva a meglévő adatbázis sémához. + +## [1.5.0] - 2026-02-10 + +### Added +- **Judge & Penalty System**: Bevezetve a `penalty_points` és `restriction_level` mechanizmus a visszaélések kiszűrésére. +- **Dynamic Multipliers**: Admin felületről (JSON config) állítható pontszorzók a büntetési szintekhez. +- **Social-to-Credit Auto-conversion**: Automatikus Kredit jóváírás a Walletbe meghatározott Social pont elérésekor. +- **Level Achievement Bonus**: 10-es szintenkénti automatikus Kredit jutalmazás. + +### Fixed +- **Circular Dependency Fix**: A modellek közötti import hurok végleges felszámolva (string-alapú relationship hivatkozások). +- **Identity Schema Protection**: Visszaállítva a `User` modell hiányzó `scope_id`, `scope_level` és `custom_permissions` mezői. +- **Database Consistency**: A `user_stats` és `asset_costs` táblák sikeresen migráltak a NOT NULL kényszerek és alapértelmezett értékek (server_default) beállításával. + +### Changed +- **GamificationService**: Mostantól központi "Bíróként" funkcionál, leválasztva a pontszámítási logikát a többi szervizről. +- **Identity Model**: A `Wallet` és `VerificationToken` osztályok integrálva az `identity.py` modulba. + +# Changelog - Service Finder Backend +**Verzió:** 1.6.0 (Sentinel & i18n Update) +**Dátum:** 2026.02.10. + +## [1.6.0] - 2026-02-10 +### Hozzáadva +- **Sentinel Biztonsági Rendszer:** - `PendingAction` modell bevezetése a "Négy szem elv" (Dual Control) biztosításához. + - `SecurityService` implementálása: Adatlopás elleni védelem (Throttling) és automata vészleállító (Emergency Lock). + - `AuditLog` bővítése szigorúbb súlyossági szintekkel (`critical`, `emergency`). +- **Nyelvi Modul (i18n):** + - `Translation` modell a `data` sémában. + - `TranslationService`: Adatbázis-alapú fordításkezelés, szerveroldali cache, Fallback (EN) logika és JSON export funkció a Frontend számára. +- **Admin Kontroll Panel:** + - Új API végpontok a függőben lévő műveletek jóváhagyásához, a rendszerbiztonsági állapot monitorozásához és a nyelvi szinkronizációhoz. + +### Javítva +- **Circular Import Fix:** A modellek importálási rendjének optimalizálása a `app.db.base_class` közvetlen használatával, megszüntetve a hurok-importokat. +- **Függőségkezelés:** `deps.py` bővítve a `get_current_active_user` függőséggel a biztonsági zárolások érvényesítéséhez. +- **Soft-Delete Logika:** A felhasználói fiók törlése mostantól felszabadítja az eredeti e-mail címet a TWINS-elvű újra-regisztrációhoz. \ No newline at end of file diff --git a/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md b/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md index 029e2fe..c72a30a 100644 --- a/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md +++ b/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md @@ -102,4 +102,20 @@ A rendszer a \`system_parameters\` táblában tárolt \`RBAC_MASTER_CONFIG\` JSO ### 2. Scope (Hatókör) Védelem Minden műveletnél ellenőrizzük a \`scope_id\` egyezését: - Ha a felhasználó \`scope_level = 'region'\`, akkor csak olyan adatot szerkeszthet, ami ugyanahhoz a régióhoz tartozik. -- Kivétel: Impersonation (Megszemélyesítés) - Audit loggal védve. \ No newline at end of file +- Kivétel: Impersonation (Megszemélyesítés) - Audit loggal védve. + + +## 1. Gamification Adminisztráció +A `data.system_parameters` táblában a `GAMIFICATION_MASTER_CONFIG` kulcs alatt az alábbiak állíthatóak: +- `xp_logic`: `base_xp`, `exponent`. +- `penalty_thresholds`: A szintekhez tartozó büntetőpont határok. +- `level_up_rewards`: 10-es szintenkénti Kredit jutalom mértéke. +- `blocked_roles`: [superadmin, service_bot]. +- `auto_convert_social`: True/False. + +## 2. Gamification Konfiguráció (JSON Schema) +A `GAMIFICATION_MASTER_CONFIG` struktúrája: +- `xp_logic`: Alap XP és kitevő a nehezedő szintezéshez. +- `penalty_logic`: Küszöbértékek, szorzók és ledolgozási ráta. +- `conversion_logic`: Social-to-Credit váltási arány. +- `level_rewards`: Szintlépési bónuszok mértéke. \ No newline at end of file