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

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

View File

@@ -15,10 +15,6 @@ reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def get_current_token_payload( async def get_current_token_payload(
token: str = Depends(reusable_oauth2) token: str = Depends(reusable_oauth2)
) -> Dict[str, Any]: ) -> 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": if token == "dev_bypass_active":
return { return {
"sub": "1", "sub": "1",
@@ -40,9 +36,6 @@ async def get_current_user(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
payload: Dict[str, Any] = Depends(get_current_token_payload), payload: Dict[str, Any] = Depends(get_current_token_payload),
) -> User: ) -> User:
"""
Visszaadja a teljes User modellt. Akkor használjuk, ha módosítani kell az usert.
"""
user_id = payload.get("sub") user_id = payload.get("sub")
if not user_id: if not user_id:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token azonosítási hiba.") 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 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): 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)): def rank_checker(payload: Dict[str, Any] = Depends(get_current_token_payload)):
user_rank = payload.get("rank", 0) user_rank = payload.get("rank", 0)
if user_rank < required_rank: if user_rank < required_rank:

View File

@@ -1,22 +1,26 @@
from fastapi import APIRouter 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() api_router = APIRouter()
# Hitelesítés # Hitelesítés (Authentication)
api_router.include_router(auth.router, prefix="/auth", tags=["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"]) 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"]) 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"]) api_router.include_router(assets.router, prefix="/assets", tags=["Assets"])
# Szervezetek # Szervezetek (Organizations)
api_router.include_router(organizations.router, prefix="/organizations", tags=["Organizations"]) api_router.include_router(organizations.router, prefix="/organizations", tags=["Organizations"])
# Dokumentumok # Dokumentumok (Documents)
api_router.include_router(documents.router, prefix="/documents", tags=["Documents"]) api_router.include_router(documents.router, prefix="/documents", tags=["Documents"])
# --- 🛡️ SENTINEL ADMIN KONTROLL PANEL ---
# Ez a rész tette láthatóvá az Admin API-t a felületen
api_router.include_router(admin.router, prefix="/admin", tags=["Admin Control Center (Sentinel)"])

View File

@@ -1,79 +1,115 @@
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select, func
from typing import List 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.api import deps
from app.models.user import User, UserRole from app.models.identity import User, UserRole
from app.models.system_settings import SystemSetting # ÚJ import from app.models.system_config import SystemParameter
from app.models.gamification import PointRule, LevelConfig, RegionalSetting from app.models.security import PendingAction, ActionStatus
from app.models.translation import Translation from app.models.history import AuditLog, LogSeverity
from app.services.translation_service import TranslationService 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() router = APIRouter()
def check_admin_access(current_user: User, required_roles: List[UserRole]): # --- 🛡️ ADMIN JOGOSULTSÁG ELLENŐRZŐ ---
if current_user.role not in required_roles: 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( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="Nincs jogosultságod ehhez a művelethez." 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]) @router.get("/pending-actions", response_model=List[PendingActionResponse])
async def get_all_system_settings( async def list_pending_actions(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(deps.get_db),
current_user: User = Depends(deps.get_current_user) admin: User = Depends(check_admin_access)
): ):
"""Az összes globális rendszerbeállítás listázása.""" """Jóváhagyásra váró kritikus kérések listázása."""
check_admin_access(current_user, [UserRole.SUPERUSER]) stmt = select(PendingAction).where(PendingAction.status == ActionStatus.pending)
result = await db.execute(select(SystemSetting)) result = await db.execute(stmt)
settings = result.scalars().all() return result.scalars().all()
return [{"key": s.key, "value": s.value, "description": s.description} for s in settings]
@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}") @router.put("/settings/{key}")
async def update_system_setting( async def update_setting(key: str, value: Any, db: AsyncSession = Depends(deps.get_db), admin: User = Depends(check_admin_access)):
key: str, """Paraméter módosítása és Audit Log generálása."""
new_value: int, # Később lehet JSON is, ha komplexebb a beállítás stmt = select(SystemParameter).where(SystemParameter.key == key)
db: AsyncSession = Depends(get_db), param = (await db.execute(stmt)).scalar_one_or_none()
current_user: User = Depends(deps.get_current_user) if not param:
): raise HTTPException(status_code=404, detail="Nincs ilyen beállítás.")
"""Egy adott beállítás (pl. FREE_VEHICLE_LIMIT) módosítása."""
check_admin_access(current_user, [UserRole.SUPERUSER])
result = await db.execute(select(SystemSetting).where(SystemSetting.key == key)) old_val = param.value
setting = result.scalar_one_or_none() param.value = value
if not setting: await security_service.log_event(
raise HTTPException(status_code=404, detail="Beállítás nem található") db, admin.id, action="SETTING_CHANGE", severity=LogSeverity.warning,
old_data={key: old_val}, new_data={key: value}
setting.value = new_value )
await db.commit() 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/sync")
async def sync_translations_to_json(
@router.post("/translations", status_code=status.HTTP_201_CREATED) db: AsyncSession = Depends(deps.get_db),
async def add_translation_draft( admin: User = Depends(check_admin_access)
key: str, lang: str, value: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_user)
): ):
check_admin_access(current_user, [UserRole.SUPERUSER, UserRole.REGIONAL_ADMIN]) """Szinkronizálja az adatbázisban tárolt fordításokat a JSON fájlokba."""
new_t = Translation(key=key, lang_code=lang, value=value, is_published=False) # A TranslationService-ben kell megírni a fájlbaíró logikát
db.add(new_t) await TranslationService.export_to_json(db)
await db.commit() return {"message": "JSON nyelvi fájlok frissítve."}
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."}

View File

@@ -1,10 +1,16 @@
import os import os
from pathlib import Path
from typing import Any, Optional from typing import Any, Optional
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
class Settings(BaseSettings): 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 --- # --- General ---
PROJECT_NAME: str = "Traffic Ecosystem SuperApp" PROJECT_NAME: str = "Traffic Ecosystem SuperApp"
VERSION: str = "1.0.0" VERSION: str = "1.0.0"
@@ -16,8 +22,7 @@ class Settings(BaseSettings):
ALGORITHM: str = "HS256" ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 nap ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 nap
# --- Initial Admin (ÚJ SZEKCIÓ) --- # --- Initial Admin ---
# Ezeket a .env-ből fogja venni
INITIAL_ADMIN_EMAIL: str = "admin@servicefinder.hu" INITIAL_ADMIN_EMAIL: str = "admin@servicefinder.hu"
INITIAL_ADMIN_PASSWORD: str = "Admin123!" INITIAL_ADMIN_PASSWORD: str = "Admin123!"

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,15 @@
import uuid import uuid
from datetime import datetime from datetime import datetime
from typing import Optional, TYPE_CHECKING 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.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from app.db.base_class import Base from app.db.base_class import Base
# Típusvizsgálathoz a körkörös import elkerülése érdekében
if TYPE_CHECKING: if TYPE_CHECKING:
from app.models.identity import User 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"} SCHEMA_ARGS = {"schema": "data"}
class PointRule(Base): class PointRule(Base):
@@ -30,39 +29,36 @@ class LevelConfig(Base):
min_points: Mapped[int] = mapped_column(Integer) min_points: Mapped[int] = mapped_column(Integer)
rank_name: Mapped[str] = mapped_column(String) 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): class PointsLedger(Base):
__tablename__ = "points_ledger" __tablename__ = "points_ledger"
__table_args__ = SCHEMA_ARGS __table_args__ = SCHEMA_ARGS
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id")) 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) reason: Mapped[str] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(DateTime, default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime, default=func.now())
# Kapcsolat a felhasználóhoz
user: Mapped["User"] = relationship("User") user: Mapped["User"] = relationship("User")
class UserStats(Base): class UserStats(Base):
__tablename__ = "user_stats" __tablename__ = "user_stats"
__table_args__ = SCHEMA_ARGS __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) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"), primary_key=True)
total_xp: Mapped[int] = mapped_column(Integer, default=0) total_xp: Mapped[int] = mapped_column(Integer, default=0)
social_points: 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) 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()) 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") user: Mapped["User"] = relationship("User", back_populates="stats")
class Badge(Base): class Badge(Base):
__tablename__ = "badges" __tablename__ = "badges"
__table_args__ = SCHEMA_ARGS __table_args__ = SCHEMA_ARGS
@@ -81,7 +77,7 @@ class UserBadge(Base):
user: Mapped["User"] = relationship("User") user: Mapped["User"] = relationship("User")
class Rating(Base): # <--- Az új értékelési modell class Rating(Base):
__tablename__ = "ratings" __tablename__ = "ratings"
__table_args__ = SCHEMA_ARGS __table_args__ = SCHEMA_ARGS
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)

View File

@@ -1,9 +1,16 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON, Date, Text import enum
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON, Date, Text, Enum
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql import func from sqlalchemy.sql import func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from app.db.base_class import Base 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): class VehicleOwnership(Base):
__tablename__ = "vehicle_ownerships" __tablename__ = "vehicle_ownerships"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
@@ -20,11 +27,25 @@ class VehicleOwnership(Base):
class AuditLog(Base): class AuditLog(Base):
__tablename__ = "audit_logs" __tablename__ = "audit_logs"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=True) user_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
target_type = Column(String, index=True) severity = Column(Enum(LogSeverity), default=LogSeverity.info, nullable=False)
target_id = Column(String, index=True)
action = Column(String, nullable=False) # Mi történt és min?
changes = Column(JSON, nullable=True) action = Column(String(100), nullable=False, index=True)
timestamp = Column(DateTime(timezone=True), server_default=func.now()) 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") user = relationship("User")

View File

@@ -7,12 +7,12 @@ from sqlalchemy.sql import func
from app.db.base_class import Base from app.db.base_class import Base
class UserRole(str, enum.Enum): class UserRole(str, enum.Enum):
superadmin = "superadmin"
admin = "admin" admin = "admin"
user = "user" user = "user"
service = "service" service = "service"
fleet_manager = "fleet_manager" fleet_manager = "fleet_manager"
driver = "driver" driver = "driver"
superadmin = "superadmin" # Hozzáadva a biztonság kedvéért
class Person(Base): class Person(Base):
__tablename__ = "persons" __tablename__ = "persons"
@@ -24,16 +24,9 @@ class Person(Base):
last_name = Column(String, nullable=False) last_name = Column(String, nullable=False)
first_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) phone = Column(String, nullable=True)
identity_docs = Column(JSON, server_default=text("'{}'::jsonb")) 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) is_active = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now())
@@ -49,27 +42,27 @@ class User(Base):
hashed_password = Column(String, nullable=True) hashed_password = Column(String, nullable=True)
role = Column(Enum(UserRole), default=UserRole.user) role = Column(Enum(UserRole), default=UserRole.user)
is_active = Column(Boolean, default=False) is_active = Column(Boolean, default=False)
region_code = Column(String, default="HU")
is_deleted = Column(Boolean, default=False) is_deleted = Column(Boolean, default=False)
person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True) 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_level = Column(String(30), server_default="individual")
scope_id = Column(String(50)) scope_id = Column(String(50))
custom_permissions = Column(JSON, server_default=text("'{}'::jsonb")) custom_permissions = Column(JSON, server_default=text("'{}'::jsonb"))
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
# Kapcsolatok
person = relationship("Person", back_populates="users") person = relationship("Person", back_populates="users")
wallet = relationship("Wallet", back_populates="user", uselist=False) wallet = relationship("Wallet", back_populates="user", uselist=False)
stats = relationship("UserStats", back_populates="user", uselist=False) stats = relationship("UserStats", back_populates="user", uselist=False)
ownership_history = relationship("VehicleOwnership", back_populates="user") ownership_history = relationship("VehicleOwnership", back_populates="user")
owned_organizations = relationship("Organization", back_populates="owner") owned_organizations = relationship("Organization", back_populates="owner")
# A Wallet és VerificationToken osztályok maradnak változatlanok...
class Wallet(Base): class Wallet(Base):
__tablename__ = "wallets" __tablename__ = "wallets"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}

View File

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

View File

@@ -1,5 +1,6 @@
from sqlalchemy import Column, Integer, String, Text, Boolean, UniqueConstraint from 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): class Translation(Base):
__tablename__ = "translations" __tablename__ = "translations"
@@ -12,4 +13,4 @@ class Translation(Base):
key = Column(String(100), nullable=False, index=True) key = Column(String(100), nullable=False, index=True)
lang_code = Column(String(5), nullable=False, index=True) lang_code = Column(String(5), nullable=False, index=True)
value = Column(Text, nullable=False) value = Column(Text, nullable=False)
is_published = Column(Boolean, default=False) # Publikálási állapot is_published = Column(Boolean, default=False)

View File

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

View File

@@ -1,6 +1,7 @@
import os import os
import logging import logging
import uuid import uuid
import json
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional from typing import Optional
@@ -18,18 +19,15 @@ from app.services.email_manager import email_manager
from app.core.config import settings from app.core.config import settings
from app.services.config_service import config from app.services.config_service import config
from app.services.geo_service import GeoService from app.services.geo_service import GeoService
from app.services.security_service import security_service # Sentinel integráció
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AuthService: class AuthService:
@staticmethod @staticmethod
async def register_lite(db: AsyncSession, user_in: UserLiteRegister): async def register_lite(db: AsyncSession, user_in: UserLiteRegister):
""" """Step 1: Lite Regisztráció."""
Step 1: Lite Regisztráció (Master Book 1.1)
Új User és ideiglenes Person rekord létrehozása nyelvi és időzóna adatokkal.
"""
try: try:
# Ideiglenes Person rekord a KYC-ig
new_person = Person( new_person = Person(
first_name=user_in.first_name, first_name=user_in.first_name,
last_name=user_in.last_name, last_name=user_in.last_name,
@@ -46,14 +44,12 @@ class AuthService:
is_active=False, is_active=False,
is_deleted=False, is_deleted=False,
region_code=user_in.region_code, region_code=user_in.region_code,
# --- NYELVI ÉS ADMIN BEÁLLÍTÁSOK MENTÉSE ---
preferred_language=user_in.lang, preferred_language=user_in.lang,
timezone=user_in.timezone timezone=user_in.timezone
) )
db.add(new_user) db.add(new_user)
await db.flush() 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) reg_hours = await config.get_setting("auth_registration_hours", region_code=user_in.region_code, default=48)
token_val = uuid.uuid4() token_val = uuid.uuid4()
db.add(VerificationToken( db.add(VerificationToken(
@@ -63,14 +59,12 @@ class AuthService:
expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reg_hours)) 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}" verification_link = f"{settings.FRONTEND_BASE_URL}/verify?token={token_val}"
await email_manager.send_email( await email_manager.send_email(
recipient=user_in.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}, 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() await db.commit()
@@ -83,23 +77,16 @@ class AuthService:
@staticmethod @staticmethod
async def complete_kyc(db: AsyncSession, user_id: int, kyc_in: UserKYCComplete): async def complete_kyc(db: AsyncSession, user_id: int, kyc_in: UserKYCComplete):
""" """1.3. Fázis: Atomi Tranzakció & Shadow Identity."""
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.
"""
try: try:
# 1. Aktuális technikai User lekérése
stmt = select(User).options(joinedload(User.person)).where(User.id == user_id) stmt = select(User).options(joinedload(User.person)).where(User.id == user_id)
res = await db.execute(stmt) res = await db.execute(stmt)
user = res.scalar_one_or_none() user = res.scalar_one_or_none()
if not user: return None if not user: return None
# --- PÉNZNEM PREFERENCIA FRISSÍTÉSE ---
if hasattr(kyc_in, 'preferred_currency') and kyc_in.preferred_currency: if hasattr(kyc_in, 'preferred_currency') and kyc_in.preferred_currency:
user.preferred_currency = kyc_in.preferred_currency user.preferred_currency = kyc_in.preferred_currency
# 2. Shadow Identity Ellenőrzése
identity_stmt = select(Person).where(and_( identity_stmt = select(Person).where(and_(
Person.mothers_last_name == kyc_in.mothers_last_name, Person.mothers_last_name == kyc_in.mothers_last_name,
Person.mothers_first_name == kyc_in.mothers_first_name, Person.mothers_first_name == kyc_in.mothers_first_name,
@@ -115,7 +102,6 @@ class AuthService:
else: else:
active_person = user.person active_person = user.person
# 3. Címkezelés
addr_id = await GeoService.get_or_create_full_address( addr_id = await GeoService.get_or_create_full_address(
db, db,
zip_code=kyc_in.address_zip, zip_code=kyc_in.address_zip,
@@ -126,7 +112,6 @@ class AuthService:
parcel_id=kyc_in.address_hrsz 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_last_name = kyc_in.mothers_last_name
active_person.mothers_first_name = kyc_in.mothers_first_name active_person.mothers_first_name = kyc_in.mothers_first_name
active_person.birth_place = kyc_in.birth_place 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.ice_contact = jsonable_encoder(kyc_in.ice_contact)
active_person.is_active = True active_person.is_active = True
# 5. Új, izolált INDIVIDUAL szervezet (4.2.3) i18n beállításokkal
new_org = Organization( new_org = Organization(
full_name=f"{active_person.last_name} {active_person.first_name} Egyéni Flotta", full_name=f"{active_person.last_name} {active_person.first_name} Egyéni Flotta",
name=f"{active_person.last_name} Flotta", name=f"{active_person.last_name} Flotta",
@@ -146,7 +130,6 @@ class AuthService:
is_transferable=False, is_transferable=False,
is_active=True, is_active=True,
status="verified", status="verified",
# Megörökölt adminisztrációs adatok
language=user.preferred_language, language=user.preferred_language,
default_currency=user.preferred_currency, default_currency=user.preferred_currency,
country_code=user.region_code country_code=user.region_code
@@ -154,7 +137,6 @@ class AuthService:
db.add(new_org) db.add(new_org)
await db.flush() await db.flush()
# 6. Tagság és Jogosultságok
db.add(OrganizationMember( db.add(OrganizationMember(
organization_id=new_org.id, organization_id=new_org.id,
user_id=user.id, user_id=user.id,
@@ -162,7 +144,6 @@ class AuthService:
permissions={"can_add_asset": True, "can_view_costs": True, "is_admin": True} permissions={"can_add_asset": True, "can_view_costs": True, "is_admin": True}
)) ))
# 7. Wallet & Stats
db.add(Wallet( db.add(Wallet(
user_id=user.id, user_id=user.id,
coin_balance=0, coin_balance=0,
@@ -171,7 +152,6 @@ class AuthService:
)) ))
db.add(UserStats(user_id=user.id, total_xp=0, current_level=1)) db.add(UserStats(user_id=user.id, total_xp=0, current_level=1))
# 8. Aktiválás
user.is_active = True user.is_active = True
await db.commit() await db.commit()
@@ -182,6 +162,39 @@ class AuthService:
logger.error(f"KYC Atomi Tranzakció Hiba: {str(e)}") logger.error(f"KYC Atomi Tranzakció Hiba: {str(e)}")
raise 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 @staticmethod
async def verify_email(db: AsyncSession, token_str: str): async def verify_email(db: AsyncSession, token_str: str):
try: try:
@@ -227,13 +240,12 @@ class AuthService:
expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reset_hours)) 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}" reset_link = f"{settings.FRONTEND_BASE_URL}/reset-password?token={token_val}"
await email_manager.send_email( await email_manager.send_email(
recipient=email, recipient=email,
template_key="pwd_reset", # hu.json: email.pwd_reset_subject stb. template_key="pwd_reset",
variables={"link": reset_link}, variables={"link": reset_link},
lang=user.preferred_language # Adatbázisból kinyert nyelv lang=user.preferred_language
) )
await db.commit() await db.commit()
return "success" return "success"

View File

@@ -1,47 +1,106 @@
import logging
import math
from decimal import Decimal
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from app.models.gamification import UserStats, PointsLedger 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: class GamificationService:
@staticmethod @staticmethod
async def process_activity(db: AsyncSession, user_id: int, xp_amount: int, social_amount: int, reason: str): async def get_config(db: AsyncSession):
""" """Kiolvassa a GAMIFICATION_MASTER_CONFIG-ot a rendszerparaméterekből."""
XP növelés, Szintlépés csekk és Automata Kredit váltás. stmt = select(SystemParameter).where(SystemParameter.key == "GAMIFICATION_MASTER_CONFIG")
""" res = await db.execute(stmt)
# 1. User statisztika lekérése param = res.scalar_one_or_none()
stmt = select(UserStats).where(UserStats.user_id == user_id) return param.value if param else {
stats = (await db.execute(stmt)).scalar_one_or_none() "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: 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) db.add(stats)
# 2. Részletes Logolás (PointsLedger) - A visszakövethetőség miatt # 3. Büntető logika (Penalty)
db.add(PointsLedger( if is_penalty:
user_id=user_id, stats.penalty_points += xp_amount
xp_gain=xp_amount, th = config["penalty_logic"]["thresholds"]
social_gain=social_amount, if stats.penalty_points >= th["level_3"]: stats.restriction_level = 3
reason=reason elif stats.penalty_points >= th["level_2"]: stats.restriction_level = 2
)) elif stats.penalty_points >= th["level_1"]: stats.restriction_level = 1
# 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
# Külön log a váltásról db.add(PointsLedger(user_id=user_id, points=0, penalty_change=xp_amount, reason=f"PENALTY: {reason}"))
db.add(PointsLedger(user_id=user_id, reason=f"Auto-conversion: {new_credits} Credits", credits_change=new_credits)) 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() await db.commit()
return stats return stats
async def _add_credits(self, db: AsyncSession, user_id: int, amount: int, reason: str):
wallet_stmt = select(Wallet).where(Wallet.user_id == user_id)
wallet = (await db.execute(wallet_stmt)).scalar_one_or_none()
if wallet:
wallet.credit_balance += Decimal(amount)
db.add(CreditTransaction(org_id=None, amount=Decimal(amount), description=reason))
gamification_service = GamificationService()

View File

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

View File

@@ -1,15 +1,21 @@
import json
import os
import logging
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update from sqlalchemy import select, update
from app.models.translation import Translation 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: 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]] = {} _published_cache: Dict[str, Dict[str, str]] = {}
@classmethod @classmethod
async def load_cache(cls, db: AsyncSession): 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( result = await db.execute(
select(Translation).where(Translation.is_published == True) select(Translation).where(Translation.is_published == True)
) )
@@ -20,27 +26,80 @@ class TranslationService:
if t.lang_code not in cls._published_cache: if t.lang_code not in cls._published_cache:
cls._published_cache[t.lang_code] = {} cls._published_cache[t.lang_code] = {}
cls._published_cache[t.lang_code][t.key] = t.value 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 @classmethod
def get_text(cls, key: str, lang: str = "en") -> str: def get_text(cls, key: str, lang: str = "hu", variables: Optional[Dict[str, Any]] = None) -> str:
"""Villámgyors lekérés a memóriából Fallback logikával.""" """
# 1. Kért nyelv 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) 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 @classmethod
async def publish_all(cls, db: AsyncSession): 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( await db.execute(
update(Translation).where(Translation.is_published == False).values(is_published=True) update(Translation).where(Translation.is_published == False).values(is_published=True)
) )
await db.commit() await db.commit()
await cls.load_cache(db) await cls.load_cache(db)
await cls.export_to_json(db)
@staticmethod
async def export_to_json(db: AsyncSession):
"""
Adatbázis -> Hierarchikus JSON export.
'AUTH.LOGIN.TITLE' -> { "AUTH": { "LOGIN": { "TITLE": "..." } } }
"""
stmt = select(Translation).where(Translation.is_published == True)
result = await db.execute(stmt)
translations = result.scalars().all()
languages: Dict[str, Any] = {}
for t in translations:
if t.lang_code not in languages:
languages[t.lang_code] = {}
# Hierarchikus struktúra felépítése
parts = t.key.split('.')
current_level = languages[t.lang_code]
for part in parts[:-1]:
if part not in current_level:
current_level[part] = {}
current_level = current_level[part]
current_level[parts[-1]] = t.value
# Fájlok mentése
locales_path = os.path.join(settings.STATIC_DIR, "locales")
os.makedirs(locales_path, exist_ok=True)
for lang, content in languages.items():
file_path = os.path.join(locales_path, f"{lang}.json")
try:
with open(file_path, "w", encoding="utf-8") as f:
json.dump(content, f, ensure_ascii=False, indent=2)
logger.info(f"🚀 JSON legenerálva: {file_path}")
except Exception as e:
logger.error(f"Fájl hiba ({lang}): {str(e)}")
return True
translation_service = TranslationService()

View File

@@ -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 ###

View File

@@ -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 ###

View File

@@ -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 ###

View File

@@ -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 ###

View File

@@ -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 ###

View File

@@ -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"
}
}
}

View File

@@ -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!"
}
}

View File

@@ -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. 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 ### 3. Audit
Minden pontmozgás a \`PointsLedger\` táblába kerül rögzítésre a visszakövethetőség érdekében. 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.

View File

@@ -249,4 +249,41 @@ A rendszer most már képes egyetlen KYC folyamat alatt aktiválni a felhasznál
### Changed ### Changed
- `AssetCost` modell mezőnevek szinkronizálva a pénzügyi standardokhoz (`amount_local`, `amount_eur`). - `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. - `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.

View File

@@ -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 ### 2. Scope (Hatókör) Védelem
Minden műveletnél ellenőrizzük a \`scope_id\` egyezését: 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. - 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. - 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.