From 425f598fa38598da5113bbd654be36d94801b1cb Mon Sep 17 00:00:00 2001 From: Kincses Date: Tue, 10 Feb 2026 21:01:58 +0000 Subject: [PATCH] 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 --- .../app/api/__pycache__/deps.cpython-312.pyc | Bin 3502 -> 3580 bytes backend/app/api/deps.py | 22 +- .../api/v1/__pycache__/api.cpython-312.pyc | Bin 1103 -> 1256 bytes backend/app/api/v1/api.py | 20 +- .../__pycache__/admin.cpython-312.pyc | Bin 0 -> 7910 bytes backend/app/api/v1/endpoints/admin.py | 154 ++++++++----- .../core/__pycache__/config.cpython-312.pyc | Bin 2633 -> 3025 bytes backend/app/core/config.py | 9 +- .../app/db/__pycache__/base.cpython-312.pyc | Bin 1260 -> 1379 bytes backend/app/db/base.py | 4 +- backend/app/models/__init__.py | 5 +- .../__pycache__/__init__.cpython-312.pyc | Bin 1493 -> 1598 bytes .../models/__pycache__/asset.cpython-312.pyc | Bin 8701 -> 8866 bytes .../__pycache__/gamification.cpython-312.pyc | Bin 6263 -> 6083 bytes .../__pycache__/history.cpython-312.pyc | Bin 2134 -> 2810 bytes .../__pycache__/identity.cpython-312.pyc | Bin 5520 -> 5061 bytes .../__pycache__/security.cpython-312.pyc | Bin 0 -> 2317 bytes .../__pycache__/translation.cpython-312.pyc | Bin 0 -> 1008 bytes backend/app/models/asset.py | 7 + backend/app/models/gamification.py | 30 ++- backend/app/models/history.py | 35 ++- backend/app/models/identity.py | 21 +- backend/app/models/security.py | 44 ++++ backend/app/models/translation.py | 5 +- .../admin_security.cpython-312.pyc | Bin 0 -> 1531 bytes backend/app/schemas/admin_security.py | 26 +++ .../__pycache__/auth_service.cpython-312.pyc | Bin 14323 -> 15627 bytes .../gamification_service.cpython-312.pyc | Bin 2391 -> 6849 bytes .../security_service.cpython-312.pyc | Bin 0 -> 8915 bytes .../translation_service.cpython-312.pyc | Bin 0 -> 6232 bytes backend/app/services/auth_service.py | 70 +++--- backend/app/services/gamification_service.py | 127 ++++++++--- backend/app/services/security_service.py | 169 ++++++++++++++ backend/app/services/translation_service.py | 91 ++++++-- ..._create_translation_and_security_tables.py | 210 ++++++++++++++++++ ...197bfddfb4f_add_lang_and_region_to_user.py | 186 ++++++++++++++++ ...386cba_finalize_gamification_v1_5_clean.py | 200 +++++++++++++++++ ...lation_and_security_tables.cpython-312.pyc | Bin 0 -> 22221 bytes ...ular_imports_and_finalize_.cpython-312.pyc | Bin 23019 -> 0 bytes ...penalty_system_and_wallet_.cpython-312.pyc | Bin 0 -> 19072 bytes ...dd_lang_and_region_to_user.cpython-312.pyc | Bin 0 -> 19853 bytes ...ze_gamification_v1_5_clean.cpython-312.pyc | Bin 0 -> 22117 bytes ...g_actions_for_dual_control.cpython-312.pyc | Bin 0 -> 18292 bytes ...ade_audit_log_for_security.cpython-312.pyc | Bin 0 -> 20881 bytes ...fd_add_pending_actions_for_dual_control.py | 174 +++++++++++++++ ...ad1dbe37_upgrade_audit_log_for_security.py | 192 ++++++++++++++++ backend/static/locales/en.json | 23 ++ backend/static/locales/hu.json | 31 +++ docs/V01_gemini/11_Gamification_Social.md | 45 +++- docs/V01_gemini/15_Changelog.md | 39 +++- .../19_ADMIN_AND_PERMISSIONS_SPEC.md | 18 +- 51 files changed, 1753 insertions(+), 204 deletions(-) create mode 100644 backend/app/api/v1/endpoints/__pycache__/admin.cpython-312.pyc create mode 100644 backend/app/models/__pycache__/security.cpython-312.pyc create mode 100644 backend/app/models/__pycache__/translation.cpython-312.pyc create mode 100644 backend/app/models/security.py create mode 100644 backend/app/schemas/__pycache__/admin_security.cpython-312.pyc create mode 100644 backend/app/schemas/admin_security.py create mode 100644 backend/app/services/__pycache__/security_service.cpython-312.pyc create mode 100644 backend/app/services/__pycache__/translation_service.cpython-312.pyc create mode 100644 backend/app/services/security_service.py create mode 100644 backend/migrations/versions/134d92edd430_create_translation_and_security_tables.py create mode 100644 backend/migrations/versions/6197bfddfb4f_add_lang_and_region_to_user.py create mode 100644 backend/migrations/versions/8e06c5386cba_finalize_gamification_v1_5_clean.py create mode 100644 backend/migrations/versions/__pycache__/134d92edd430_create_translation_and_security_tables.cpython-312.pyc delete mode 100644 backend/migrations/versions/__pycache__/16d09fe82f82_fix_all_circular_imports_and_finalize_.cpython-312.pyc create mode 100644 backend/migrations/versions/__pycache__/5ca03588ce28_add_penalty_system_and_wallet_.cpython-312.pyc create mode 100644 backend/migrations/versions/__pycache__/6197bfddfb4f_add_lang_and_region_to_user.cpython-312.pyc create mode 100644 backend/migrations/versions/__pycache__/8e06c5386cba_finalize_gamification_v1_5_clean.cpython-312.pyc create mode 100644 backend/migrations/versions/__pycache__/bc5669f12ffd_add_pending_actions_for_dual_control.cpython-312.pyc create mode 100644 backend/migrations/versions/__pycache__/ffffad1dbe37_upgrade_audit_log_for_security.cpython-312.pyc create mode 100644 backend/migrations/versions/bc5669f12ffd_add_pending_actions_for_dual_control.py create mode 100644 backend/migrations/versions/ffffad1dbe37_upgrade_audit_log_for_security.py create mode 100644 backend/static/locales/en.json create mode 100644 backend/static/locales/hu.json diff --git a/backend/app/api/__pycache__/deps.cpython-312.pyc b/backend/app/api/__pycache__/deps.cpython-312.pyc index b57151682a0ceebb082c9f5555c6b5fe8f6c5192..8e23a62bf3304f4b2f200554b75519bbe386e421 100644 GIT binary patch delta 767 zcmY*W-%C?*6#ssAf4IAO&796_>UP((HZx6&Oo*xUP!LF{PZF--z0>8I^u4Gknue9DG&QEytstb|Y$Yc+bEChK)kUUMx8qs>~a?upv8x+Q+p zJ8KKNGb8vK#)Fwot_5Q;Gr~_}jOaVTXyVtnRa^O*MLf?BqaO6IZ=IXLL1B=-5d-wQ z_^sLx1S1Xb1MS^LmYsg6cyXa}+$2O#B*Uhmlko#7rJH!byI{qu0GAO)PtZKNUMl?$ zbe})bgx75UC_uxg^g3s@0hdaWZzwXA%&wK$e`qz2Te9-oXDbBXLqJe!HB(O5P! zrOstn$aFjvQB#ow*u<2QziGyZu7wFT?K60g-jjYekRGT9Il|VQm)JVPB$AjSy>#92R_ui= z3DHTX>KcGB1ZD&P;7@FKo~6&6ez6aZL6yFD?uGgxB&`gX;F5CE&LY3(LKUvg*=sEh{EmD$?N1x&!2L0cY^pI>g9kL#js`#J5Q*ARqQ$@ zvGIzUw;WYVIWC^Lh0|gqC&~=lqGFk1%NoHA(d`0u2`d=bB5C4omvW*44fMQ28O}Jm z!#vAnqO7NgWf6P3L<^$A+>wTMw@o9}od8BCR{a%p5NhHXnIV~fle?Bf-u#KNhX!K> zRn;=8?~Pa)!tet|D9$<>v~@@Dv}rmNr&U(4McK-D<}e&^R#Y{Ii49k^4Qv7vs|Dd=kGh&GfHBtmDW)0iOdRgaq%-v&KsymKn$O^GFmRL@cWpfdTiy$`>mq(LH zoSMROL^m9JT7!!VjW6OKB7gX1_)L3{#=*#L#JAtj&@o<%ZcdCrm5u|j0KgKa=X_f1 z|Nitk6dR=TkiY{EqtfUV{;?$nx;6FQnWH|3%lsfxf<@oAO@4)B58((^F t?#pecwHEHE2O;h$ZN+$z6%#KO>VCZ*l0wmgW Dict[str, Any]: - """ - Kinyeri a token payload-ot DB hívás nélkül. - Ez teszi lehetővé a gyors jogosultság-ellenőrzést. - """ if token == "dev_bypass_active": return { "sub": "1", @@ -40,9 +36,6 @@ async def get_current_user( db: AsyncSession = Depends(get_db), payload: Dict[str, Any] = Depends(get_current_token_payload), ) -> User: - """ - Visszaadja a teljes User modellt. Akkor használjuk, ha módosítani kell az usert. - """ user_id = payload.get("sub") if not user_id: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token azonosítási hiba.") @@ -55,11 +48,18 @@ async def get_current_user( return user +async def get_current_active_user( + current_user: User = Depends(get_current_user), +) -> User: + """Ellenőrzi, hogy a felhasználó aktív-e.""" + if not current_user.is_active: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="A felhasználói fiók zárolva van vagy inaktív." + ) + return current_user + def check_min_rank(required_rank: int): - """ - Függőség-gyár: Ellenőrzi, hogy a felhasználó rangja eléri-e a minimumot. - Használat: Depends(check_min_rank(60)) -> RegionAdmin+ - """ def rank_checker(payload: Dict[str, Any] = Depends(get_current_token_payload)): user_rank = payload.get("rank", 0) if user_rank < required_rank: diff --git a/backend/app/api/v1/__pycache__/api.cpython-312.pyc b/backend/app/api/v1/__pycache__/api.cpython-312.pyc index 87e4886ffa108fe5522bb01743a8295865ddb133..d7138eed5b65f578d881362263e99e0c944582b0 100644 GIT binary patch delta 443 zcmX@l@q&}@G%qg~0}!~?bY}*!OyrYb{4!BpUoV$4ij$Fni6ND7HAof+qPSAIvcw^5 zWHOaGOJZV!sw8$v4xr>dT#_t6$v?OxS+gW2TQTC2Wdq7qFsd>GZJoS;QIt_)@-{}5 zFdQ!A%#uKMF@&8WkRq7QxQ2B#Glb8`P${G-{1U{`WW2@c7~mO{Us{q{q{%+{6Qecz zE!M=8+|0bmx=ibIs@U|woGN)okeGsVeqKpYevX23Y93I7f<`cq&df{A(e%^goqUhU ziK&QdvMh6;oFvd}Mj$T!2qZo*Gcq#XWstqkka(RT@ghUwQ@*-K5#HFa$jeV gyvQIqLv_B+Oq~@K7wi)**d<0xIe?Ofa7nUciA`d}CCQp4HrWoREE`a^2B$1LP<9uiibSP=rr=AEV>KCX zaXJQg2IZHQq!wwiO*Ub&o?OPXj@eI>XL10u)8y&Qe$o;^b&Nn%-AU}j`wyvrba WpCRfxL)2tumVF#o8A6K0fKmWEnlAYO diff --git a/backend/app/api/v1/api.py b/backend/app/api/v1/api.py index 656ab36..fedd04f 100755 --- a/backend/app/api/v1/api.py +++ b/backend/app/api/v1/api.py @@ -1,22 +1,26 @@ from fastapi import APIRouter -from app.api.v1.endpoints import auth, catalog, assets, organizations, documents, services +from app.api.v1.endpoints import auth, catalog, assets, organizations, documents, services, admin api_router = APIRouter() -# Hitelesítés +# Hitelesítés (Authentication) api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"]) -# Szolgáltatások és Vadászat (Ez az új rész!) +# Szolgáltatások és Vadászat (Service Hunt & Discovery) api_router.include_router(services.router, prefix="/services", tags=["Service Hunt & Discovery"]) -# Katalógus +# Katalógus (Vehicle Catalog) api_router.include_router(catalog.router, prefix="/catalog", tags=["Vehicle Catalog"]) -# Eszközök (Járművek) +# Eszközök / Járművek (Assets) api_router.include_router(assets.router, prefix="/assets", tags=["Assets"]) -# Szervezetek +# Szervezetek (Organizations) api_router.include_router(organizations.router, prefix="/organizations", tags=["Organizations"]) -# Dokumentumok -api_router.include_router(documents.router, prefix="/documents", tags=["Documents"]) \ No newline at end of file +# Dokumentumok (Documents) +api_router.include_router(documents.router, prefix="/documents", tags=["Documents"]) + +# --- 🛡️ SENTINEL ADMIN KONTROLL PANEL --- +# Ez a rész tette láthatóvá az Admin API-t a felületen +api_router.include_router(admin.router, prefix="/admin", tags=["Admin Control Center (Sentinel)"]) \ No newline at end of file diff --git a/backend/app/api/v1/endpoints/__pycache__/admin.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/admin.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cdb564fa0cd699679f359ddbd30efc23bfacc389 GIT binary patch literal 7910 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 W`HSz*Uwl8(XnhR#-mgvrll(8q-|A!l literal 0 HcmV?d00001 diff --git a/backend/app/api/v1/endpoints/admin.py b/backend/app/api/v1/endpoints/admin.py index 987241f..6f555c1 100755 --- a/backend/app/api/v1/endpoints/admin.py +++ b/backend/app/api/v1/endpoints/admin.py @@ -1,79 +1,115 @@ from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select -from typing import List +from sqlalchemy import select, func +from typing import List, Any, Dict +from datetime import datetime, timedelta -from app.db.session import get_db from app.api import deps -from app.models.user import User, UserRole -from app.models.system_settings import SystemSetting # ÚJ import -from app.models.gamification import PointRule, LevelConfig, RegionalSetting -from app.models.translation import Translation -from app.services.translation_service import TranslationService +from app.models.identity import User, UserRole +from app.models.system_config import SystemParameter +from app.models.security import PendingAction, ActionStatus +from app.models.history import AuditLog, LogSeverity +from app.schemas.admin_security import PendingActionResponse, SecurityStatusResponse + +from app.services.security_service import security_service +# Feltételezve, hogy a JSON-alapú TranslationService-ed már készen van +from app.services.translation_service import TranslationService router = APIRouter() -def check_admin_access(current_user: User, required_roles: List[UserRole]): - if current_user.role not in required_roles: +# --- 🛡️ ADMIN JOGOSULTSÁG ELLENŐRZŐ --- +async def check_admin_access(current_user: User = Depends(deps.get_current_active_user)): + if current_user.role not in [UserRole.admin, UserRole.superadmin]: raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Nincs jogosultságod ehhez a művelethez." + status_code=status.HTTP_403_FORBIDDEN, + detail="Admin jogosultság szükséges!" ) + return current_user -# --- ⚙️ ÚJ: DINAMIKUS RENDSZERBEÁLLÍTÁSOK (Pl. Jármű limit) --- +# --- 1. SENTINEL: NÉGY SZEM ELV (Approval System) --- -@router.get("/settings", response_model=List[dict]) -async def get_all_system_settings( - db: AsyncSession = Depends(get_db), - current_user: User = Depends(deps.get_current_user) +@router.get("/pending-actions", response_model=List[PendingActionResponse]) +async def list_pending_actions( + db: AsyncSession = Depends(deps.get_db), + admin: User = Depends(check_admin_access) ): - """Az összes globális rendszerbeállítás listázása.""" - check_admin_access(current_user, [UserRole.SUPERUSER]) - result = await db.execute(select(SystemSetting)) - settings = result.scalars().all() - return [{"key": s.key, "value": s.value, "description": s.description} for s in settings] + """Jóváhagyásra váró kritikus kérések listázása.""" + stmt = select(PendingAction).where(PendingAction.status == ActionStatus.pending) + result = await db.execute(stmt) + return result.scalars().all() + +@router.post("/approve/{action_id}") +async def approve_action( + action_id: int, + db: AsyncSession = Depends(deps.get_db), + admin: User = Depends(check_admin_access) +): + """Művelet véglegesítése (második admin által).""" + try: + await security_service.approve_action(db, admin.id, action_id) + return {"status": "success", "message": "Művelet végrehajtva."} + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +# --- 2. SENTINEL: BIZTONSÁGI ÖSSZEGZÉS --- + +@router.get("/security-status", response_model=SecurityStatusResponse) +async def get_security_status( + db: AsyncSession = Depends(deps.get_db), + admin: User = Depends(check_admin_access) +): + """Rendszerállapot: Zárolt júzerek és kritikus események.""" + day_ago = datetime.now() - timedelta(days=1) + + crit_count = (await db.execute(select(func.count(AuditLog.id)).where( + AuditLog.severity.in_([LogSeverity.critical, LogSeverity.emergency]), + AuditLog.timestamp >= day_ago + ))).scalar() or 0 + + locked_count = (await db.execute(select(func.count(User.id)).where( + User.is_active == False, User.is_deleted == False + ))).scalar() or 0 + + return { + "total_pending": (await db.execute(select(func.count(PendingAction.id)).where(PendingAction.status == ActionStatus.pending))).scalar() or 0, + "critical_logs_last_24h": crit_count, + "emergency_locks_active": locked_count + } + +# --- 3. RENDSZERBEÁLLÍTÁSOK (Dynamic Config) --- + +@router.get("/settings") +async def get_settings(db: AsyncSession = Depends(deps.get_db), admin: User = Depends(check_admin_access)): + """Minden globális paraméter (Gamification, Limitek stb.) lekérése.""" + result = await db.execute(select(SystemParameter)) + return result.scalars().all() @router.put("/settings/{key}") -async def update_system_setting( - key: str, - new_value: int, # Később lehet JSON is, ha komplexebb a beállítás - db: AsyncSession = Depends(get_db), - current_user: User = Depends(deps.get_current_user) -): - """Egy adott beállítás (pl. FREE_VEHICLE_LIMIT) módosítása.""" - check_admin_access(current_user, [UserRole.SUPERUSER]) +async def update_setting(key: str, value: Any, db: AsyncSession = Depends(deps.get_db), admin: User = Depends(check_admin_access)): + """Paraméter módosítása és Audit Log generálása.""" + stmt = select(SystemParameter).where(SystemParameter.key == key) + param = (await db.execute(stmt)).scalar_one_or_none() + if not param: + raise HTTPException(status_code=404, detail="Nincs ilyen beállítás.") - result = await db.execute(select(SystemSetting).where(SystemSetting.key == key)) - setting = result.scalar_one_or_none() + old_val = param.value + param.value = value - if not setting: - raise HTTPException(status_code=404, detail="Beállítás nem található") - - setting.value = new_value + await security_service.log_event( + db, admin.id, action="SETTING_CHANGE", severity=LogSeverity.warning, + old_data={key: old_val}, new_data={key: value} + ) await db.commit() - return {"status": "success", "key": key, "new_value": new_value} + return {"status": "success", "key": key, "new_value": value} +# --- 🌍 JSON FORDÍTÁSOK KEZELÉSE --- -# --- 🌍 FORDÍTÁSOK KEZELÉSE (Meglévő kódod) --- - -@router.post("/translations", status_code=status.HTTP_201_CREATED) -async def add_translation_draft( - key: str, lang: str, value: str, - db: AsyncSession = Depends(get_db), - current_user: User = Depends(deps.get_current_user) +@router.post("/translations/sync") +async def sync_translations_to_json( + db: AsyncSession = Depends(deps.get_db), + admin: User = Depends(check_admin_access) ): - check_admin_access(current_user, [UserRole.SUPERUSER, UserRole.REGIONAL_ADMIN]) - new_t = Translation(key=key, lang_code=lang, value=value, is_published=False) - db.add(new_t) - await db.commit() - return {"message": "Fordítás piszkozatként mentve. Ne felejtsd el publikálni!"} - -@router.post("/translations/publish") -async def publish_translations( - db: AsyncSession = Depends(get_db), - current_user: User = Depends(deps.get_current_user) -): - check_admin_access(current_user, [UserRole.SUPERUSER, UserRole.REGIONAL_ADMIN]) - await TranslationService.publish_all(db) - return {"message": "Sikeres publikálás! A változások minden szerveren élesedtek."} - \ No newline at end of file + """Szinkronizálja az adatbázisban tárolt fordításokat a JSON fájlokba.""" + # A TranslationService-ben kell megírni a fájlbaíró logikát + await TranslationService.export_to_json(db) + return {"message": "JSON nyelvi fájlok frissítve."} \ No newline at end of file diff --git a/backend/app/core/__pycache__/config.cpython-312.pyc b/backend/app/core/__pycache__/config.cpython-312.pyc index 277aaa3a532e8d76be18886e6c684d1552809312..0da2b5049fb190a40c676878c1a22dcb3e8dfb88 100644 GIT binary patch delta 1365 zcma)*&2Jk;7{+II*Sl+bZ6~SMagru+KAhN1lNOpDAf-~xN1Gl{A>}fauw2h1U9%ry zcd2R#$wewsrCx}92tph{Jt1>I>Oa60ak3&5G*#3yw^o!p^NwAx#DzILGrwnN-g)Pp z*$s}Wvl9EznoK1*zX-O72 zCBJ1PWl3IAPSO~$SwYMkmoz~pnbi?^N@AWAd)c4&1yXO7xk_E{7iog#S8P7xG-i7(YeH6I6VLO3%>c6Et&z&b`#$!1}|_ TUn0@HxQmgMfAr=TrVRHt0No(_ delta 978 zcmYk4O-~b16o&7e?|w`vP-qJU%9mJB!9+KPATA6siY|;#uW=UV+ihyJB|DR?>SV%ES|Z~x$n$5xw(hM&xPC<*G)6^@VVE` zxAtDH)Y(?{jtM5X#{l1&$9)YnUk9C|>z?78VEPtVIUC?Hg(u z^}s~CQd>%#`!bH3!B+I_z|(K)-0ps~<}=eO5Y8ZGyFV<+7Y^3#Jm=N!BWJdgQT0TR z;xOr}*Be35iW_mW6-4!Vn8i^*PbeHgR1jkb zoHHCpOdzU=Nrl_t6fPYEUa{~p!cs`GeoIKNzR?Ocn_KVMA5~-F5&~~K!B&fCQr)zd z?0e}-_b|1_oo+e(_0HECIKMi4hQ>QV2^LK8ot*6HYp z*YGyBrQZ(i?yXFvNdscv#QC4Uj`>vtPL(*zYT&1yTnX2ds-ajkukcIfoPT4fV^%$8 fD@Sbfh%KHNSGo1-`3Y06-vMXY!kcR^9QxN^WJ|}H diff --git a/backend/app/core/config.py b/backend/app/core/config.py index cb6770c..e19274e 100755 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,10 +1,16 @@ import os +from pathlib import Path from typing import Any, Optional from pydantic_settings import BaseSettings, SettingsConfigDict from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession class Settings(BaseSettings): + # --- Paths (ÚJ SZEKCIÓ) --- + # Meghatározzuk a projekt gyökérmappáját és a statikus fájlok helyét + BASE_DIR: Path = Path(__file__).resolve().parent.parent.parent + STATIC_DIR: str = os.path.join(str(BASE_DIR), "static") + # --- General --- PROJECT_NAME: str = "Traffic Ecosystem SuperApp" VERSION: str = "1.0.0" @@ -16,8 +22,7 @@ class Settings(BaseSettings): ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 nap - # --- Initial Admin (ÚJ SZEKCIÓ) --- - # Ezeket a .env-ből fogja venni + # --- Initial Admin --- INITIAL_ADMIN_EMAIL: str = "admin@servicefinder.hu" INITIAL_ADMIN_PASSWORD: str = "Admin123!" diff --git a/backend/app/db/__pycache__/base.cpython-312.pyc b/backend/app/db/__pycache__/base.cpython-312.pyc index ac879e93e6ae97840cd2da0f9f8fa3565c7c55e0..e14e879cac5a9f8d7c9f5884e7680d153496f650 100644 GIT binary patch delta 258 zcmaFE`Iw9EG%qg~0}#ya?ao}tGLcV$@ykT@^ID7yDGVvxIhwgzQChj$QQEmWQ98N0 zQMzDpo*cbg{V07#hDu&dzRetrt&Dt(x41)!67!045=%1k^CtH&1&e4h-r@~N%}dG5 zOLt5LiTY_8Y<|QP#TX}+SWuvso1c=JQ><5l&{d=ebjK}GB+2CbqSW}D{PfJ^A|;?) zkur!-0THSorNT%`i&K+Ji!w_pi_|73vuJUd0p%HixHxq3bQU-9y9|2YnV1-vJ~IG` NZ)^;VdPOEcSpdOmN=5(x delta 120 zcmaFN^@fx0G%qg~0}#ml?aF-0Jdsa=@y9K|?!B8#|yB2aIU5{OU+5h{}}v1oA_0hx?ITZ*hkdCFT|9B$j06=S`l;R3yZBi#H%OFC{ZC-7y&?sHr)bhj}`q#^m|T zGE7D4lXoy%F`7;O%={&Vy9A-NND=7jTU^QcMXB*Q`RSR-MM^+{B4rSv0wPpF^6VLz z#U=Sgl|^bGA$5=tM{#O$X;EfLWswF*OcO+Cfe397p)*;F)tbu|$YccK;*`nxtk#Bi z8C32wq+e!8zt5n5mqGah8w0mM1N#j=;Rfy-g0c;KHw2^_ct5dlaWgft7g+=K0RWb_ BTyX#Z delta 228 zcmdnTbCsL#G%qg~0}$x^?aI8vI+0I;=?B9^jYC$s>QU;s8c`a#no*jJ3@J=0+&Nmg z+ELoMI#D`|45>U(x4M3ew^%}pQ;YmGB_{{+7)|~!n5uh=r4%SqWB^oGWC$XRK!gX3Z~-!k=KzToh8rTv zlQ;5fPrf6{F?pYmpV&hli5cP>B(LyTH@M#97N5a(nOnZmWwL~@0+R>h zChrs80&e zB*K{7n2?;Lw7EvIfl&qME`<~cxVx0Vd`UQ8xl&3~dXls;cLUJ0qH++?IoVFyinpi< z#I67mewr#!Co3@(HBa6ry;r*pD13`8r6{uuGg#e0`anb`5EmP7o+%^7r~nB^@hd!* z2+uV5+~k#

~;9vC(bwZrM;q#=gmd@)C^wleOd*ae?Ba2t?FRz9!$rIA?OA!d)O~ zuPDp40w@ZK_Tt{jX^J6^Jd6qxRK73(sV_3zj0O`@zAyl(FH#(g))Pd&FaW79A|i}R a6Ii}50I4rJ9E@&^6HLAWCEOUn$pHYpt)*lD delta 426 zcmXYrO-lk{5XW~^LT!E9U3IrKO~Mj7WG_%E_PTWwi8^&u4?&SAVnRg*9a;}APGAHP z)wM&ix1bLYodPcfeTNQD&?V@3tOw@*pLvG)&D^R#%gz^v!_Lw7{H<*l<&JaF4T6z% zi(sjFVggn@IgEQFSmZ_Y3I)_$Zqx-QwD4Luo+iTq@mIVv?m%zQRxM`9Cb!<>%|Fap@xfTiBFuAZo;w zQIn-6#}qsmoa#SfvsEUi_&AuL>%^Z zDb6O>@W!VYCqpwZpwar>tZuHClB|-1=o zG<&zEcK7m?okF^FM@yhlZlWWyv22HwK0-KZM7F^K=y}31Cq64Ab%}7)h~$BUZXp~s Q61BoA=+j@cu?oEnzpY4dUjP6A diff --git a/backend/app/models/__pycache__/gamification.cpython-312.pyc b/backend/app/models/__pycache__/gamification.cpython-312.pyc index 0ded2200594c6966c46c41d122b147c00cf876dc..ac42ebd9fde10ac05f86cd9c696d0de2e46471d4 100644 GIT binary patch delta 2513 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 diff --git a/backend/app/models/__pycache__/history.cpython-312.pyc b/backend/app/models/__pycache__/history.cpython-312.pyc index db83aa03d7668e8519002319d90818387a1d3eb1..4767261e52b35bce73b8f9a413c256bc4bf0e1ae 100644 GIT binary patch delta 1636 zcmZuxU1%It6ux(6c4l@b`~RDtHfftSuB1s@qe!hnAWf6D2CT8rArRJ`n{>m@ZoE7B z*}Uu&iU_`B889n-iGmnN9wNR}is++MgqE&$GZG~D)Hh4xi~8WXvp-ErFU)t(`R;el z<=%VF{1*5pdgEuW*MneuIs0j5#*NUQ%xMm>VLba0j5VYo4QG+cW-g0$PUUo7<#j<7 zbeHPVMOD-#Rnlcu*4=9PMpwy)nbwu{>j5?B*kU%Mht)7f9J+)w={C}25+P_V(%ka- zyeZUeHR@>Hp!JaOCf{IV4yyp$c7Tl+ank#QNhXr+^1Wfv7B1!%^zwhNPvd$Zde#NT z8X`!=8d5n8tGvdkg2t;ZO;E+6D=FH_)%@)HWRcK}QM83jZYFO_ODUSmEc9EvJ}2ILPL<7h8eIE(I|lR3*UP#$UFuze1#amZB$c&=19#KH!s!v z?dDi5(0OZ|b~J_G_TsNFK4snKhW(TU&^UtxfHlZp$6o7We&}=?xVE%NZf4S1Vhale zLTNkGIe9vnb)*SC>tFthW6lEO((tuL>!m-#*7w3>i9^Igl+DJS5oM0;`@+!VEqCiV zyb$4n&!>5sQJSm?TS`meZzSA~xgf{{Bm|t)3JSl3i?kB? zUb+#~6Xb~<$`fzIB!KmAVs@=CfNY-|vhL%K zQM3nOk{)AbAA^$sqe+gw!kkk8Pc#4^m`zVG;v6Gz-9GKuwrpf{vXak{Bxn2JU7;GM zH8PW0$Qq80_JeLBdGug$lz^fvo(?czy+T;gdBM0(73Yq6v2;)7_P zIkD&VuS)Ba+mWjK4b!TL(ULz})+%p3iuG-GK0Z7A@a*tU7pwkj=Gapa`6A`f%4^n@ zjg#9()qBAl-3cW=kF9feIyQ!?p)=-{oj|O-SozRe*@#pF1LpWs83kgi+WKVG|Ef8* z!B1c`N6dLfvX?!jfbz+mM`deZ6QnEq1PaS^;=;a;4dCy{5w+q zKvR#=)URl27xnF;?p<_#7aiXh1l+ZHV%1nHmW%rcoc%ThkKk2fAF+AABjLk6pBR6m J1nzOp_CMyQb$0*& delta 952 zcmZ9K%TE(Q9LHz++U;&z`UVNe!z!+jsu2!86AUTw#l_mhUYe!5l$!0s*)6tQs0k*- zi*5EG$9glu%?rkZe}EcIXq0Z!c<{tY66?j2zgZ}vlk8`Hzwdm1llkq;@6Z?T)O)v^ zLojv+o@P}$Lf>i99gv&j=Ne4Uk%T0ypd3!tFAg%8rWU1se<0Rl#&*S<^Cf*4SVwYLs^uBR$8@W0TK|M2`dwQB zyYyGK$p8;dgISUnvuQ;(Y$a7DI%A1&UeE~zqx1F`+_`R=uKPlkJ*0oM#}{NyvTd>( z7FdIkd&r^rj776G=waQd*@b z8_t6yD;W+ktz`>&!cr5HUKH~gStSly=c1A|Uw8z5IGC!5xgxnruZct{&Qh4yah^sO zx}g23PXMcECpcWavlBkvaC{DpRp(mz4|XKw?^_Qyw5ETedaLCP*OnUbruR(sM#~+j z#TxgT?(u4@F6p z0p^6D{=qpp2LJD&A}Z-cIai4mNRF7V>!=9p#HCMgkrg*R_$h$l6N|;DG#{N8RXLSb zL{%jox@bN@v%i0PHrOkcuEYvelk=wneQ0ee=A@i&zCiW1u>(MDr!G}K0y+Gw<6 YINsIk_Q-blQd^yJCeg)NN-H`zDyzBli^?|Uvne8_l5TxALty?F@vR2cK!LkW22d%4z(Y13aXCRhZtBUu55 zAR+6kOR$BJQ@#_~m?aV&;qtMB#yH;)cShjD%~t&xYce?4a3EYSAUFh^bs}QZixL~d zqJuW%TJy2yn;?`WCr*5I_A;4oeC>p0lAtLZ!pM~C)KA%%g2QAak-Fg1HX8j0ZMD%D z4maet*;pJW8nE<=+R(+3RI>WZZWZh(M^D=>-zYi^}MP9J&*Ne9WPDI9rnNu zlR^r-Wd>0OzKb2u&9;XW>zKzV%xU)Z8Uw_K2kb?bF|@mSAwM}?z#4pHuhLl(F$RI< zZqsdKTkM1aHwjfP+4`HNVyWl!?)^Rgo3?WF%4d)AO^j?SyXaZ;Rc~3mRLVgopL74< zq6e$|IfO>RY|hF#QsvNkz*YD4@$8Y5JKznj5R&UbDKKg^w9GZw36{|4K~i{x&?J4O>7%Q6yDi5YiIqt>z~+(|I#Fc5Nbz_6QC4SN=cJARSDD_EReN$Ce5b)>v$c> zx!6cZq?}qtf*g7%YAY4RLWz3e#Gz78Tq0#9h{dJcazaQxBWBj})Ir})SVgEFHWb((MSIKYxND!fvW?Ei+|nG7OS-i!U`;^C32o`ke~U0tGW1_6!~HW zM=2kBiDC`$37(YQ1p)_(4HvYu#66xRiAT|EXot zKSup$pa)7V+1%bZ`PT9Wm>~7R+wV)ovbKS6HCIroTCSuNQCsdo>niSH_Rm$UZR9Ei zC6BO`?yW6$5vXY22xGHtsfeJOR|+{)MEC|OtZ z4$ZE$`K8RZo2p7l(Cb|-m#PUBo(wYe@Ef%gb}iZiOPR%!b9A)r{-B1-6_mVGgGvQe zwG(#p+oD>6>Xr&?N&(LjQH%pWnGvsmSBXcOCildtF+ZbOaJhgCVd6BK+cCPKmP=H! z5RqTSg`ix)XdPjUU@l*yA1Z1*N0}sKzbLHI0lrFd_RoYFI;^gcr}muPP^?dv6U{V< zJK}Rx&)*C8Z#@{=PwAmInWjx(UJt#+0Js0{`tC<$^YFE|b@zql63IK#gMSH}Yvjp+ zBMCs3Ja;bo*Vu9yWmd=WG7Q)QOLerUf{~|AlHLfoO|E+9CZjvmJD0y(zPsGv e1Q6|bg5dnl^$tfrom3cH13Re>7bcIq@qYm7b6lAK diff --git a/backend/app/models/__pycache__/security.cpython-312.pyc b/backend/app/models/__pycache__/security.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b8e14e3e88f1a7bc8b018e90ea7fe9ba8b0ced66 GIT binary patch literal 2317 zcmZuyO>7fK6rQ!$_WD2m$$vs4ehQ|@&{9MIi&5SM^IW3sTXKgB5Z|F)KgAv6?*BVeY0yDLSuQ~zW3&PGjHDC z{t}Nz5d3zuuaraxq2HNM{{?Hz?)ShvKp0_8MWXlSL=I%1>eGCpPxFg@j_Lg>uLZ<_ z78HY8NDOI$C}?3ZtVP6#78RpfOpLiWUX5!BG2!w7HK{d;O)ei)Q(9U~bI6DK5f05F zERYOAlPz_Z-n>_Mj~BBpHVoJZNw4^8+U5$Ei|#OCFz0gwEStnsG~$FA#Y8pboZtj6 zl~ha9ozPX?B;$lS!6B0>`nV$$06)y|k-ico%D6s2Dh_{b=;olqU)C+n;Z1Va%yCYP z5>+;pl5R{WWsvzXOE0qbMcE)l&l!($HDR|4m!%%c;GWyRsU?WRez(WG!ifPNB0&pXo}1pI z%QS3a``SRgx}G${9UH!Yu4OX+jw>Lp)Ksg9mHL@&``P2SA0 zPLaqsx0-HfyK36#9&L9elgm2s&%G-2O4B+6o(--)9Ix~kGeqkl>h#&38$W+HO;lo% zsS`c2rcCuruKzH-zBD0^SJsydD))dub$Kf9Bx@xt$!?(=PS7Y$5KVS?=u~+aS~gmy ziYC*FGzFa(P;^Z0&cxYb-ZBU^@(S*QHWPG9Rpn8YfJF$CG1*d0dI((G`G}E!fiM(n z`k9hWfKPz+G@()ryr)RnNb$q7TLAI^ne~=MleMscy78;0#thU|m|OMLQ$7RAA(ZnE z?z901=LRN97BzBy$_{Y+dg)8bj-&02xINR!7KC%Etc_y%{7lQ9c<1Z-V}NqouZn(Z zd(!o|YyE(I>x-wO*uF)6?IrG3IgTD?oAI~dvT$~6%o}1Xmd0T20A=kAIUz~H zr6P=qxFne@nzbpCsELwH#|=rM?531;vg1=QWrKjtzdIH&Oo*t0ooH=9F@Z)I+8rBg z=0JwsX-45Ig8=B}T{0_WLfIDxhMZhcOEPwXl*mR&rvfuiFmi&eM~a|_fhWnP6Y*xA z!T1{MaHvg5a_eJ*;TWzqLd$1<_gA6ExJlWd+FUc784j#v9Z zN@VZ%&7b~yVEKq0KUMA9PPHxw%ZKf@laGW){1f4^upY5f1J!FU8Vk?P5IZ#nnoRq` zvE_o@@!^jJJJVaezTMO{KeBdiEpIpVRwpTmbT?pX0dSP!#^J(6Lu&WMvm8qSDtHF4wh6FK ztUWE(aguc-Zyw7aZ*p_KLL<-TiA6soWx}}mGGwk_=6Zo_ggh}jf6-QG6(Xom(tnVBBQm}~=uCl5+hG0#S zz!tXllYQ2)Br0|i+j~m<1Dp~W=|P4)$jJOBBh^z*C2UL}$8wH&h2?q&Phhf&A{s8|v)qDE zszbBB6w4B*k{%Hd{+f1|z2V+1%E83vJhTXFlqk?4!nFH+`XbzkREdzT0@(9lg@Jvpdrnz4vB`P4p{R z&fDA$Y7|j;^JgcJa1T{CY35?6ASd|n{!4p=kcB2dc8J+XEORZ z`PD$^PqjFDtQnd72;?P-QH)cRVIO0~jnv2t-_YDl&5ZaYvwSPFeLHh}N83s2W-Y&k zk%6{RY&}P@P4O$UIoEG%s{>Z|l(nO+EwHvvS-Vxo>;9#F^ssE*4LSX+h-nH7Hx30A zNk%;)-Oo#r6nU6Rb1SbT`6!8mB%dZ+z{c)Yq@4FfC`LSLVrY=e7L%tyULuNo93$U& zfT&4HjBkTgGea{AY^G*5wWzaWysi8sCdXb&GZ$>OW-VaOF>@6=N%9qxT|ItVr6?D_ zpn06+yG^k7C@+hgQ)wiz>@xazM7f|WfK?~d=>(!GsdUP)N{cX-Hlraga%pp&rff%G z5Ht=#A=~u(GGUYhKa)Og0zi{XKu*$+v2Rv}XTDjvFr>{7i+m^9{c@=49t3%q(IAlR zAjpb%lq%j0g2$sUoq6VVSdqcif+gRM1m(Y=iMsdA`pvK`^~&pvcvQYOf@88(x z`|s|3GbUH-?%cCa>h98*EY+7z@IQjB!9NI~IbNN7rV+EjK$CUwoHXJA44w~4q?T5i z9cvv{*fZrqBa^MNWW^JDuhGX)I#H2jG#8$8t|06NoXpfEV8~_w{RXy$N4!xj6g{@SG0+!ZA{#rN^iuo+bPDXYFpr^H}#i0ewq@ zvKb25k3bSh1eUbeQBiW}JWxF(wDsV*Kfy7}rp3tFZoiD%|#CBoY?p0G~M7p<|p}NCVY1O>)4*>o)F~)~z?GUa0jyA?<;}C63 iNe2%OKAs}@OqUJ3esF7w;4^hieC^edn{!zO&bVIiGztj+-A8LW#k4_%Q_H1jvH89^Q?!Vb^D7 z*Q7QF2Q8{hgMf5s6BQ`90uf|V(z>l^RngF+2!T}DrK(RG5J5_%s+HO$L?S5??Yuc( z9HFY}%J|_S(0HAf zc-RODs@G#uOmC1Hn9Qp+>TxMvWF4Q@q&G{=dO}L*Nh!(3{923NDz)mOB(i=$ zYt!4McD+ODVEv%hsdq_TjEA%hdP+($9@e_`w3KGNM(fdgrC!EswT#{;^&!r|Dd^zc z;0gL%QE3<|S1h7RR~m3|J2|=TF;0#wI@ZQE&Z2a5=^IE!p)|5;3}s>ltLALhAa6Pn z$Rej5V?|zZ%WlOddldJ6kL-PnUtHT?@+(dmDZK1ceEWIXf0-n(CJE4WK`IsG&}Gu# zx|FU9UnU8yOJG;IM)5_d(%Q?WglFs0k)>nKfAeC38ZvGw)8fW$;+SeG;%;S9HReoQ zT9{BvOZ{YvcQ@)Hk9%)Nz2uBnK^$bqIwbRAi4>OuFAFPd%< zx?$CAWLNW_i(NqL06GEKaif%3K6Oe~A5i6)oQ5+XVh7&=YzQC%PySCdU&a4g2V@5uhMm_F#$d$=-U{*H~~~^6qW~ z?j26cR~+HN4l3@mb^zffBX^6v?fV>DBz@6t;>2q%+I60W&_(-Nh~EW!0~c($P!6<{e+1je zd*UA>0#o&dxVEv1s4vG`(v?ojAk7L*TE3=(^sB{1ldU?0$CR7 zoxE8~KJ8DyYRAEPQX7k0?W$^dmXrB{@GSSdP;h14)m>!h372|QN!DNOnQ?NYCOOI# zoU&V{m)%TOB?VO7RrVDG713QBvi~{9Yk@^iWzJX5#36E9M?3jLOej_k$sul5*vdWe zJNj#-Kko_WA(Rfz{E{yBRTG3==RtACEGU<@&kUD=LG@gkz9=-9%jq~0! z4XrQouecZ7$7YYC*ScTNyqa0;U+o!M=@~lJ{Xx%7%jvOqhu&4+y=%FC-#q_>uiVr^ zn{YV}P3XM9g9{8)-;~ej&+}VAyIJ#1Q z)%jX)*!MZ-_4;fEF)m#Dl=`@O_`X$7eAyE(ha>hzt~PbSZlrTA*fGxA^e<8B z_~9^mOF+Y3{w?>lowQTxMZQ42#Ps8Z_9C%W@RhPJkT%%){)G{WnkYFy%&oCV-m&;s9d+>E~{*O#y zE`Y*d`HYF2mc!Znl!Es)z;cLarGR)Vsl;?ZKH=X2{1gCAT}@}KOjuqh5j3%Nlhqaj zj9V)xqDT{LXz&X3<bkFTJN1Hb~SaZr?&GPO0&cW!~63Xlyh}}d1-6L`4P8`EN_j53r-Y2N53^tobCA^24I2> delta 2528 zcma)8TWlOf7VUD+jHl=2*VyBEc--+L_V|?yiIX_WCgkNLivuAqkp^T<+!edy?rBrq zHnPd?WFD*@RD7$m?lk&tFrN_O`HG(;dFP$Ccs4HA*y1BnkhP9$QaT~W8j zj=ihZivBot>fWkbRrj7M^>1hHn&A(_;ef!tcRzSuo1YEG(dO9>xlo~thNy^!NAVb5 z#ft=0P(|HmR4SF6dvw1cDH7*iJzxZtpb=6+yzkS)Mns8lUa40Z)k-zze!a%1Rceho zrOt>dQKMd|HyV@%?vwPG(Wo?X9?;_jBcUXWq>?mJN{SnUdXte>(nhn=%=;l-Hr6TY zj25MZ_rrRt(WbO<9?{#4^~!qAtMm?|Q|aWqTF)3=N*59;1dPs}gj4_LhkF(fwK zQ6(Hu1I+7FgKNB@RWF+tTH^_?dQ=uw4YRp^HL_+zWV$L-?F4sE+GEEsnbJnEQ*^~U zp28p5+5vg_J7(hvIlEmR(rhdr#iN=vW0TU{h*m1foriOljfuR+G{@y^UpDh#R_W4N z`Gmz>rFl!%RIH8aIFBv)2qu=9mkAy>i7M-*d7I!dbEY(B<>dS{)~7VNB=afPW@k#p zd``}(Ioli`&*^;+f>R_NMQ!xBbP{#Z1qq`TS`#>yYGKc(kj6P|Nkq}th z>G?p5uM6l-`etCk*9=sqr-Cm>8cPeZ9&wb2(kXFF-v%~0xkD0R;#G`G`YXbjcT zRCv1nF(ibWmfSQ)pAer$OKu~5BfN+agScDn5^v}_0O+JlT5S@)-r7_2IYbMi=V&AdsUu}G@xgWCF$-oIt zU}CVKnwhrHw+%Y5;}+4g)+2@EE}307n^QLWGx_@AgS%a6Fc6 z*p5g%*dlxB`^o*WCz#m+sjSEzT5^iErn>8ogYUAqB9O>a2oy4sI^kv2u8z@vq|(5e zTIrXm_c-gIuQd&!2I@AQgMO@)n(0I6Hoclo`eUJ34kS#LpBW8a#B+X zg%N)MfVh@V5Txa7u{?d%*^RCigP9cawZo9z#on$b(DBv1aBBu!tIIV)cRhoZHOo`M zy1Lk2)`i7Z*ZRD>yYgeOJ|oaw+5Y0gJr~7fA2T?SiJWCU%U+gYf-P*5EHjc#^y>{B zz5$@yY4ygo!vEh?5(e%A;F8-9Y!HBpk7sxfbpKZ-J>WP5kO25406Z_`5df~^A%V$1 zQ(4Y2KQBKjoYD#MT@R0$lf)t?sk1TVI|cR_U6+l~x@tXhaalA9`CXyv~LX9?!%H-}awj)d#|GJZDWUUw#k&D+N$cvIXET06#-1JOyx} zoPafw({p6l%wuvY2GgOUWhqH(jtP1qmjPY|fKxkCSzZK>I1b$RoiBX9J@bFOfD9V2g#Fv8k*b}jOD zc{rHdgChNW;7C)WH~D6dE3oE@wFu4f7{=Vqba4B|rmHL4$!u^cOv|a%&CyXUKgvs3 zd};d}dPEw<_7JmTLjDS)kE~1~SAg*dTqyn>hN({KcqjRgIpBZ9I?ccp5kg-G(icL6 mzO`eZ^|q9_7`rJYZb*q2w|yeDo%j9VAtH(|GF78EkM|SW18ltj diff --git a/backend/app/services/__pycache__/gamification_service.cpython-312.pyc b/backend/app/services/__pycache__/gamification_service.cpython-312.pyc index 04264e8f20dc874881fd56b4463ace769993deb7..923b6b5897c8ad0d22c0c0d75d30ab9a92e5ab73 100644 GIT binary patch literal 6849 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 literal 2391 zcmZ`5ZD&$7GYcy>oUi zCf7|{s+4FWu^>%KDUHw{N-PM4lD`s!K>yq&kvq1PwzP%%?`i^V{neShUGI=M$IN?g z=6%hZ_uk%*)zwu1kMGZ4Q{VOi@H-*g!8pkJn;4k|3Q(vxNKi3~BDg2+NzgIc#_2ed zU}J28i*Yv1#Jw@E2b=^7dj%-mf~TY@#!oO&?_)wlDN8u2PiXQW)O9tfVVcun9Lh%2 zWBJbL5DglVpiU+a^jVFUA z3%Ltb3U`HBa71F>32&6Q>P|`t^@1u(2KHbOqA^v5mPdKt@}{u^qN-TF@subfl4;Gb zs`aF-N^zIu5R~+!CbN!80(d+xU2c;yJtl23ci9Ce!760AacU&wa+aa#$zZFp zMkPhxcU`3gM}u&rWebEUHLK`Jq`e}Per>Z$cafj+FIsu zF_U&YdKOWva}z#=A3cw?#$I%q!C5iM+g7%@y35ey9V>#n_ac83K7%8IBUJWh)0e7z z2gPUdUje1+Yue=R`WKv+ccW#$WRq*R4BdQrKDRJGHwgBF8`O07elYF(j=n@ih4g7G zSru`{+0zki>4z~GU%I33jSOaRX=5~nsfet@(a{LOj;4)dLNcUCKZ1&CM8=kG#f_y~ zdgn7Wrx2;7$E1v+EFy1Z@kY|X#W|jOR;MYuaah%I8X>3|VjN<-mRGiw>QSa7=fwp+ zB&nL^cgmX(h@`l>f%OlavB#gUKUVNoriS>|At$vlh)CBu6v^7Llaq;rYD9gA^nsECBm)phn35}uZX^tg)$Mlb)8Hj15`Ka@Zt6tZeWb|K zXqS{q*%P-x(iHWsp^8o^I=h{z2`iLBNqnx0l5D7Bsxgt-xk*#)e4Yz`YJh$o2ly}W z@5f+60HLin)zx4mAKaY_?*4Wn-*zC^c3|;juI<=r@c0x{sBf6fe4d%#xmw?ruW8GN zdUBzj)zFbC_P1d8=kWIVp+#!BCL24Kg%{SsLsNYPAuw~|>WLfO(*s2g`1~{6Rc_iW zgqsTC?S*Z-3Uv(|J~mXf0a%~E7y`b)OyF8zE}G+8uLu4J*UxSFruoa}+pW2Vj(gp? zZ3kDwhZf(-g?p!b1%LQPI$z(Ot8ZWNw-{YWMtCe1ihM#;5u&9OX}rGA96apQHMEnET%29hi8qkLv4X9&~#N-b>;26#f|U zqNqs;D2i55$JjZw*LZpeggh~m?*X73I118^7Mb}Cn}Br diff --git a/backend/app/services/__pycache__/security_service.cpython-312.pyc b/backend/app/services/__pycache__/security_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8c6b61f04ea71015fc33492f856ddd207d9e9ec2 GIT binary patch literal 8915 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 Tl;jFUG;Gg1J92~_-}8R~4&Mke literal 0 HcmV?d00001 diff --git a/backend/app/services/__pycache__/translation_service.cpython-312.pyc b/backend/app/services/__pycache__/translation_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c8696253ec7ffa1ac2295d4e32d2fc47d3e12bb8 GIT binary patch literal 6232 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 stats.current_level: - stats.current_level = new_level - - # 4. Automata Kredit váltás - # Példa: Minden 100 Social pont automatikusan 1 Kredit lesz - stats.social_points += social_amount - if stats.social_points >= 100: - new_credits = stats.social_points // 100 - stats.credits += new_credits - stats.social_points %= 100 # A maradék megmarad a következő váltáshoz + # 3. Büntető logika (Penalty) + if is_penalty: + stats.penalty_points += xp_amount + th = config["penalty_logic"]["thresholds"] + if stats.penalty_points >= th["level_3"]: stats.restriction_level = 3 + elif stats.penalty_points >= th["level_2"]: stats.restriction_level = 2 + elif stats.penalty_points >= th["level_1"]: stats.restriction_level = 1 - # Külön log a váltásról - db.add(PointsLedger(user_id=user_id, reason=f"Auto-conversion: {new_credits} Credits", credits_change=new_credits)) + db.add(PointsLedger(user_id=user_id, points=0, penalty_change=xp_amount, reason=f"PENALTY: {reason}")) + await db.commit() + return stats + # 4. Dinamikus szorzó alkalmazása + multipliers = config["penalty_logic"]["multipliers"] + multiplier = multipliers.get(f"level_{stats.restriction_level}", 1.0) + + if multiplier <= 0: + logger.warning(f"User {user_id} activity blocked (Level {stats.restriction_level})") + return stats + + # 5. XP, Ledolgozás és Szintlépés + final_xp = int(xp_amount * multiplier) + if final_xp > 0: + stats.total_xp += final_xp + if stats.penalty_points > 0: + rec_rate = config["penalty_logic"]["recovery_rate"] + stats.penalty_points = max(0, stats.penalty_points - int(final_xp * rec_rate)) + + xp_cfg = config["xp_logic"] + new_level = int((stats.total_xp / xp_cfg["base_xp"]) ** (1/xp_cfg["exponent"])) + 1 + if new_level > stats.current_level: + if new_level % 10 == 0: + reward = config["level_rewards"]["credits_per_10_levels"] + await self._add_credits(db, user_id, reward, f"Level {new_level} Achievement Bonus") + stats.current_level = new_level + + # 6. Social pont és váltás + final_social = int(social_amount * multiplier) + if final_social > 0: + stats.social_points += final_social + rate = config["conversion_logic"]["social_to_credit_rate"] + if stats.social_points >= rate: + new_credits = stats.social_points // rate + stats.social_points %= rate + await self._add_credits(db, user_id, new_credits, "Social conversion") + + db.add(PointsLedger(user_id=user_id, points=final_xp, reason=reason)) await db.commit() - return stats \ No newline at end of file + return stats + + async def _add_credits(self, db: AsyncSession, user_id: int, amount: int, reason: str): + wallet_stmt = select(Wallet).where(Wallet.user_id == user_id) + wallet = (await db.execute(wallet_stmt)).scalar_one_or_none() + if wallet: + wallet.credit_balance += Decimal(amount) + db.add(CreditTransaction(org_id=None, amount=Decimal(amount), description=reason)) + +gamification_service = GamificationService() \ No newline at end of file diff --git a/backend/app/services/security_service.py b/backend/app/services/security_service.py new file mode 100644 index 0000000..a9737f1 --- /dev/null +++ b/backend/app/services/security_service.py @@ -0,0 +1,169 @@ +import logging +from datetime import datetime, timedelta +from typing import Optional, Any, Dict +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, func, and_ +from app.models.security import PendingAction, ActionStatus +from app.models.history import AuditLog, LogSeverity +from app.models.identity import User +from app.models.system_config import SystemParameter + +logger = logging.getLogger(__name__) + +class SecurityService: + + @staticmethod + async def get_sec_config(db: AsyncSession) -> Dict[str, Any]: + """Lekéri a biztonsági korlátokat a központi rendszerparaméterekből.""" + keys = ["SECURITY_MAX_RECORDS_PER_HOUR", "SECURITY_DUAL_CONTROL_ENABLED"] + stmt = select(SystemParameter).where(SystemParameter.key.in_(keys)) + res = await db.execute(stmt) + params = {p.key: p.value for p in res.scalars().all()} + + return { + "max_records": int(params.get("SECURITY_MAX_RECORDS_PER_HOUR", 500)), + "dual_control": str(params.get("SECURITY_DUAL_CONTROL_ENABLED", "true")).lower() == "true" + } + + # --- 1. SZINT: AUDIT & LOGGING (A Mindenlátó Szem) --- + async def log_event( + self, + db: AsyncSession, + user_id: Optional[int], + action: str, + severity: LogSeverity, + old_data: Optional[Dict] = None, + new_data: Optional[Dict] = None, + ip: Optional[str] = None, + ua: Optional[str] = None, + target_type: Optional[str] = None, + target_id: Optional[str] = None, + reason: Optional[str] = None + ): + """Minden rendszerművelet rögzítése és azonnali biztonsági elemzése.""" + new_log = AuditLog( + user_id=user_id, + severity=severity, + action=action, + target_type=target_type, + target_id=target_id, + old_data=old_data, + new_data=new_data, + ip_address=ip, + user_agent=ua + ) + db.add(new_log) + + # Ha a szint EMERGENCY, azonnal lőjük le a júzert + if severity == LogSeverity.emergency: + await self._execute_emergency_lock(db, user_id, f"Auto-lock triggered by: {action}") + + await db.commit() + + # --- 2. SZINT: PENDING ACTIONS (Négy szem elv) --- + async def request_action( + self, + db: AsyncSession, + requester_id: int, + action_type: str, + payload: Dict, + reason: str + ): + """Kritikus művelet kezdeményezése jóváhagyásra (nem hajtódik végre azonnal).""" + new_action = PendingAction( + requester_id=requester_id, + action_type=action_type, + payload=payload, + reason=reason, + status=ActionStatus.pending + ) + db.add(new_action) + + await self.log_event( + db, requester_id, + action=f"REQUEST_{action_type}", + severity=LogSeverity.critical, + new_data=payload, + reason=f"Approval requested: {reason}" + ) + + await db.commit() + return new_action + + async def approve_action(self, db: AsyncSession, approver_id: int, action_id: int): + """Művelet végrehajtása egy második admin által.""" + stmt = select(PendingAction).where(PendingAction.id == action_id) + action = (await db.execute(stmt)).scalar_one_or_none() + + if not action or action.status != ActionStatus.pending: + raise Exception("A művelet nem található vagy már feldolgozták.") + + if action.requester_id == approver_id: + raise Exception("Önmagad kérését nem hagyhatod jóvá! (Négy szem elv)") + + # ITT TÖRTÉNIK A TÉNYLEGES ÜZLETI LOGIKA (Példa: Rangmódosítás) + if action.action_type == "CHANGE_ROLE": + user_id = action.payload.get("user_id") + new_role = action.payload.get("new_role") + + user_stmt = select(User).where(User.id == user_id) + user = (await db.execute(user_stmt)).scalar_one_or_none() + if user: + user.role = new_role + logger.info(f"Role for user {user_id} changed to {new_role} via approved action {action_id}") + + action.status = ActionStatus.approved + action.approver_id = approver_id + action.processed_at = func.now() + + await self.log_event( + db, approver_id, + action=f"APPROVE_{action.action_type}", + severity=LogSeverity.info, + target_id=str(action.id), + reason=f"Approved action requested by {action.requester_id}" + ) + + await db.commit() + return True + + # --- 3. SZINT: DATA THROTTLING & EMERGENCY LOCK --- + async def check_data_access_limit(self, db: AsyncSession, user_id: int): + """Figyeli a tömeges adatlekérést (Adatlopás elleni védelem).""" + config = await self.get_sec_config(db) + one_hour_ago = datetime.now() - timedelta(hours=1) + + # Megszámoljuk az utolsó egy óra GET (lekérési) logjait + stmt = select(func.count(AuditLog.id)).where( + and_( + AuditLog.user_id == user_id, + AuditLog.timestamp >= one_hour_ago, + AuditLog.action.like("GET_%") + ) + ) + count = (await db.execute(stmt)).scalar() or 0 + + if count > config["max_records"]: + await self.log_event( + db, user_id, + action="MASS_DATA_ACCESS_DETECTED", + severity=LogSeverity.emergency, + reason=f"Access count: {count} (Limit: {config['max_records']})" + ) + # A log_event automatikusan hívja a _execute_emergency_lock-ot + return False + return True + + async def _execute_emergency_lock(self, db: AsyncSession, user_id: int, reason: str): + """Azonnali fiókfelfüggesztés vészhelyzet esetén.""" + if not user_id: return + + stmt = select(User).where(User.id == user_id) + user = (await db.execute(stmt)).scalar_one_or_none() + + if user: + user.is_active = False + logger.critical(f"🚨 SECURITY EMERGENCY LOCK: User {user_id} suspended. Reason: {reason}") + # Itt lehetne bekötni egy külső SMS/Slack/Email riasztást + +security_service = SecurityService() \ No newline at end of file diff --git a/backend/app/services/translation_service.py b/backend/app/services/translation_service.py index 4efbb18..abbe1e9 100755 --- a/backend/app/services/translation_service.py +++ b/backend/app/services/translation_service.py @@ -1,15 +1,21 @@ +import json +import os +import logging from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, update from app.models.translation import Translation -from typing import Dict +from app.core.config import settings +from typing import Dict, Any, Optional + +logger = logging.getLogger(__name__) class TranslationService: - # Ez a memória-cache tárolja az élesített szövegeket + # Memória-cache a szerveroldali hibaüzenetekhez és emailekhez _published_cache: Dict[str, Dict[str, str]] = {} @classmethod async def load_cache(cls, db: AsyncSession): - """Betölti az összes PUBLIKÁLT fordítást az adatbázisból a memóriába.""" + """Betölti a publikált szövegeket a memóriába az adatbázisból.""" result = await db.execute( select(Translation).where(Translation.is_published == True) ) @@ -20,27 +26,80 @@ class TranslationService: if t.lang_code not in cls._published_cache: cls._published_cache[t.lang_code] = {} cls._published_cache[t.lang_code][t.key] = t.value - print(f"🌍 i18n Cache: {len(translations)} szöveg élesítve.") + logger.info(f"🌍 i18n Cache: {len(translations)} szöveg betöltve.") @classmethod - def get_text(cls, key: str, lang: str = "en") -> str: - """Villámgyors lekérés a memóriából Fallback logikával.""" - # 1. Kért nyelv + def get_text(cls, key: str, lang: str = "hu", variables: Optional[Dict[str, Any]] = None) -> str: + """ + Szerveroldali lekérés Fallback (EN) logikával és változó behelyettesítéssel. + Példa: get_text("AUTH.WELCOME", "hu", {"name": "Péter"}) + """ + # 1. Kért nyelv lekérése text = cls._published_cache.get(lang, {}).get(key) - if text: return text - - # 2. Fallback: Angol - if lang != "en": - text = cls._published_cache.get("en", {}).get(key) - if text: return text - return f"[{key}]" + # 2. Fallback angolra, ha nincs meg a kért nyelven + if not text and lang != "en": + text = cls._published_cache.get("en", {}).get(key) + + # 3. Ha sehol nincs meg, adjuk vissza a kulcsot + if not text: + return f"[{key}]" + + # 4. Változók behelyettesítése (pl. {{name}}) + if variables: + for k, v in variables.items(): + text = text.replace(f"{{{{{k}}}}}", str(v)) + + return text @classmethod async def publish_all(cls, db: AsyncSession): - """Élesíti a piszkozatokat és frissíti a szerver memóriáját.""" + """Minden piszkozatot élesít, frissíti a memóriát és legenerálja a JSON-öket.""" await db.execute( update(Translation).where(Translation.is_published == False).values(is_published=True) ) await db.commit() - await cls.load_cache(db) \ No newline at end of file + await cls.load_cache(db) + await cls.export_to_json(db) + + @staticmethod + async def export_to_json(db: AsyncSession): + """ + Adatbázis -> Hierarchikus JSON export. + 'AUTH.LOGIN.TITLE' -> { "AUTH": { "LOGIN": { "TITLE": "..." } } } + """ + stmt = select(Translation).where(Translation.is_published == True) + result = await db.execute(stmt) + translations = result.scalars().all() + + languages: Dict[str, Any] = {} + for t in translations: + if t.lang_code not in languages: + languages[t.lang_code] = {} + + # Hierarchikus struktúra felépítése + parts = t.key.split('.') + current_level = languages[t.lang_code] + for part in parts[:-1]: + if part not in current_level: + current_level[part] = {} + current_level = current_level[part] + + current_level[parts[-1]] = t.value + + # Fájlok mentése + locales_path = os.path.join(settings.STATIC_DIR, "locales") + os.makedirs(locales_path, exist_ok=True) + + for lang, content in languages.items(): + file_path = os.path.join(locales_path, f"{lang}.json") + try: + with open(file_path, "w", encoding="utf-8") as f: + json.dump(content, f, ensure_ascii=False, indent=2) + logger.info(f"🚀 JSON legenerálva: {file_path}") + except Exception as e: + logger.error(f"Fájl hiba ({lang}): {str(e)}") + + return True + +translation_service = TranslationService() \ No newline at end of file diff --git a/backend/migrations/versions/134d92edd430_create_translation_and_security_tables.py b/backend/migrations/versions/134d92edd430_create_translation_and_security_tables.py new file mode 100644 index 0000000..a8ff5a1 --- /dev/null +++ b/backend/migrations/versions/134d92edd430_create_translation_and_security_tables.py @@ -0,0 +1,210 @@ +"""create_translation_and_security_tables + +Revision ID: 134d92edd430 +Revises: bc5669f12ffd +Create Date: 2026-02-10 20:04:23.924164 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '134d92edd430' +down_revision: Union[str, Sequence[str], None] = 'bc5669f12ffd' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('translations', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('key', sa.String(length=100), nullable=False), + sa.Column('lang_code', sa.String(length=5), nullable=False), + sa.Column('value', sa.Text(), nullable=False), + sa.Column('is_published', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('key', 'lang_code', name='uq_translation_key_lang'), + schema='data' + ) + op.create_index(op.f('ix_data_translations_id'), 'translations', ['id'], unique=False, schema='data') + op.create_index(op.f('ix_data_translations_key'), 'translations', ['key'], unique=False, schema='data') + op.create_index(op.f('ix_data_translations_lang_code'), 'translations', ['lang_code'], unique=False, schema='data') + op.create_table('pending_actions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('requester_id', sa.Integer(), nullable=False), + sa.Column('approver_id', sa.Integer(), nullable=True), + sa.Column('status', sa.Enum('pending', 'approved', 'rejected', 'expired', name='actionstatus'), nullable=False), + sa.Column('action_type', sa.String(length=50), nullable=False), + sa.Column('payload', sa.JSON(), nullable=False), + sa.Column('reason', sa.String(length=255), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('processed_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['approver_id'], ['data.users.id'], ), + sa.ForeignKeyConstraint(['requester_id'], ['data.users.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_index(op.f('ix_data_pending_actions_id'), 'pending_actions', ['id'], unique=False, schema='data') + op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey') + op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey') + op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey') + op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey') + op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey') + op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey') + op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey') + op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey') + op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey') + op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey') + op.create_foreign_key(None, 'assets', 'vehicle_catalog', ['catalog_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('audit_logs_user_id_fkey'), 'audit_logs', type_='foreignkey') + op.create_foreign_key(None, 'audit_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('credit_logs_org_id_fkey'), 'credit_logs', type_='foreignkey') + op.create_foreign_key(None, 'credit_logs', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('documents_uploaded_by_fkey'), 'documents', type_='foreignkey') + op.create_foreign_key(None, 'documents', 'users', ['uploaded_by'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey') + op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.create_foreign_key(None, 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'org_subscriptions', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('organization_members_user_id_fkey'), 'organization_members', type_='foreignkey') + op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey') + op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + existing_nullable=True) + op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey') + op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey') + op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey') + op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey') + op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey') + op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('service_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey') + op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey') + op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey') + op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey') + op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey') + op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey') + op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE') + op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey') + op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id']) + op.drop_constraint(None, 'users', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id']) + op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id']) + op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id']) + op.drop_constraint(None, 'ratings', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id']) + op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'persons', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id']) + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id']) + op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id']) + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + existing_nullable=True) + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organization_members_organization_id_fkey'), 'organization_members', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('organization_members_user_id_fkey'), 'organization_members', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id']) + op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id']) + op.drop_constraint(None, 'documents', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id']) + op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'assets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id']) + op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id']) + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id']) + op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id']) + op.drop_index(op.f('ix_data_pending_actions_id'), table_name='pending_actions', schema='data') + op.drop_table('pending_actions', schema='data') + op.drop_index(op.f('ix_data_translations_lang_code'), table_name='translations', schema='data') + op.drop_index(op.f('ix_data_translations_key'), table_name='translations', schema='data') + op.drop_index(op.f('ix_data_translations_id'), table_name='translations', schema='data') + op.drop_table('translations', schema='data') + # ### end Alembic commands ### diff --git a/backend/migrations/versions/6197bfddfb4f_add_lang_and_region_to_user.py b/backend/migrations/versions/6197bfddfb4f_add_lang_and_region_to_user.py new file mode 100644 index 0000000..7d0c857 --- /dev/null +++ b/backend/migrations/versions/6197bfddfb4f_add_lang_and_region_to_user.py @@ -0,0 +1,186 @@ +"""add_lang_and_region_to_user + +Revision ID: 6197bfddfb4f +Revises: 134d92edd430 +Create Date: 2026-02-10 20:46:57.170479 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '6197bfddfb4f' +down_revision: Union[str, Sequence[str], None] = '134d92edd430' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey') + op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey') + op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey') + op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey') + op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey') + op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey') + op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey') + op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey') + op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey') + op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey') + op.create_foreign_key(None, 'assets', 'vehicle_catalog', ['catalog_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('audit_logs_user_id_fkey'), 'audit_logs', type_='foreignkey') + op.create_foreign_key(None, 'audit_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('credit_logs_org_id_fkey'), 'credit_logs', type_='foreignkey') + op.create_foreign_key(None, 'credit_logs', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('documents_uploaded_by_fkey'), 'documents', type_='foreignkey') + op.create_foreign_key(None, 'documents', 'users', ['uploaded_by'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey') + op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.create_foreign_key(None, 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'org_subscriptions', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('organization_members_user_id_fkey'), 'organization_members', type_='foreignkey') + op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey') + op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + existing_nullable=True) + op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey') + op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey') + op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('pending_actions_approver_id_fkey'), 'pending_actions', type_='foreignkey') + op.drop_constraint(op.f('pending_actions_requester_id_fkey'), 'pending_actions', type_='foreignkey') + op.create_foreign_key(None, 'pending_actions', 'users', ['requester_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'pending_actions', 'users', ['approver_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey') + op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey') + op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey') + op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('service_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey') + op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey') + op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey') + op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey') + op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.add_column('users', sa.Column('preferred_language', sa.String(length=5), server_default='hu', nullable=True)) + op.add_column('users', sa.Column('region_code', sa.String(length=5), server_default='HU', nullable=True)) + op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey') + op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey') + op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE') + op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey') + op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id']) + op.drop_constraint(None, 'users', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id']) + op.drop_column('users', 'region_code') + op.drop_column('users', 'preferred_language') + op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id']) + op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id']) + op.drop_constraint(None, 'ratings', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id']) + op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'persons', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id']) + op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('pending_actions_requester_id_fkey'), 'pending_actions', 'users', ['requester_id'], ['id']) + op.create_foreign_key(op.f('pending_actions_approver_id_fkey'), 'pending_actions', 'users', ['approver_id'], ['id']) + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id']) + op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id']) + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + existing_nullable=True) + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organization_members_organization_id_fkey'), 'organization_members', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('organization_members_user_id_fkey'), 'organization_members', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id']) + op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id']) + op.drop_constraint(None, 'documents', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id']) + op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'assets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id']) + op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id']) + # ### end Alembic commands ### diff --git a/backend/migrations/versions/8e06c5386cba_finalize_gamification_v1_5_clean.py b/backend/migrations/versions/8e06c5386cba_finalize_gamification_v1_5_clean.py new file mode 100644 index 0000000..d0a495d --- /dev/null +++ b/backend/migrations/versions/8e06c5386cba_finalize_gamification_v1_5_clean.py @@ -0,0 +1,200 @@ +"""finalize_gamification_v1.5_clean + +Revision ID: 8e06c5386cba +Revises: 2cfe9285eb9d +Create Date: 2026-02-10 16:18:15.900078 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '8e06c5386cba' +down_revision: Union[str, Sequence[str], None] = '2cfe9285eb9d' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey') + op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey') + op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey') + op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey') + op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey') + op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey') + op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey') + op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey') + op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey') + op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey') + op.create_foreign_key(None, 'assets', 'vehicle_catalog', ['catalog_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('audit_logs_user_id_fkey'), 'audit_logs', type_='foreignkey') + op.create_foreign_key(None, 'audit_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('credit_logs_org_id_fkey'), 'credit_logs', type_='foreignkey') + op.create_foreign_key(None, 'credit_logs', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('documents_uploaded_by_fkey'), 'documents', type_='foreignkey') + op.create_foreign_key(None, 'documents', 'users', ['uploaded_by'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey') + op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.create_foreign_key(None, 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'org_subscriptions', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('organization_members_user_id_fkey'), 'organization_members', type_='foreignkey') + op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey') + op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + existing_nullable=True) + op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey') + op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey') + op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey') + op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_column('persons', 'medical_emergency') + op.drop_column('persons', 'birth_date') + op.drop_column('persons', 'ice_contact') + op.drop_column('persons', 'birth_place') + op.drop_column('persons', 'mothers_first_name') + op.drop_column('persons', 'mothers_last_name') + op.add_column('points_ledger', sa.Column('penalty_change', sa.Integer(), server_default=sa.text('0'), nullable=False)) + op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey') + op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey') + op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('service_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey') + op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey') + op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey') + op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data') + op.add_column('user_stats', sa.Column('penalty_points', sa.Integer(), server_default=sa.text('0'), nullable=False)) + op.add_column('user_stats', sa.Column('restriction_level', sa.Integer(), server_default=sa.text('0'), nullable=False)) + op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey') + op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey') + op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_column('users', 'region_code') + op.drop_column('users', 'preferred_language') + op.drop_column('users', 'preferred_currency') + op.drop_column('users', 'timezone') + op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey') + op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE') + op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey') + op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id']) + op.add_column('users', sa.Column('timezone', sa.VARCHAR(length=50), autoincrement=False, nullable=True)) + op.add_column('users', sa.Column('preferred_currency', sa.VARCHAR(length=3), autoincrement=False, nullable=True)) + op.add_column('users', sa.Column('preferred_language', sa.VARCHAR(length=5), autoincrement=False, nullable=True)) + op.add_column('users', sa.Column('region_code', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'users', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id']) + op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id']) + op.drop_column('user_stats', 'restriction_level') + op.drop_column('user_stats', 'penalty_points') + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id']) + op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id']) + op.drop_constraint(None, 'ratings', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id']) + op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id']) + op.drop_column('points_ledger', 'penalty_change') + op.add_column('persons', sa.Column('mothers_last_name', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column('persons', sa.Column('mothers_first_name', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column('persons', sa.Column('birth_place', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column('persons', sa.Column('ice_contact', postgresql.JSON(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), autoincrement=False, nullable=True)) + op.add_column('persons', sa.Column('birth_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=True)) + op.add_column('persons', sa.Column('medical_emergency', postgresql.JSON(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'persons', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id']) + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id']) + op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id']) + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + existing_nullable=True) + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organization_members_organization_id_fkey'), 'organization_members', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('organization_members_user_id_fkey'), 'organization_members', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id']) + op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id']) + op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id']) + op.drop_constraint(None, 'documents', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id']) + op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'assets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id']) + op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id']) + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id']) + # ### end Alembic commands ### diff --git a/backend/migrations/versions/__pycache__/134d92edd430_create_translation_and_security_tables.cpython-312.pyc b/backend/migrations/versions/__pycache__/134d92edd430_create_translation_and_security_tables.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82c5ab7c001604d01dbc31b50578590a02819a69 GIT binary patch literal 22221 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 literal 0 HcmV?d00001 diff --git a/backend/migrations/versions/__pycache__/16d09fe82f82_fix_all_circular_imports_and_finalize_.cpython-312.pyc b/backend/migrations/versions/__pycache__/16d09fe82f82_fix_all_circular_imports_and_finalize_.cpython-312.pyc deleted file mode 100644 index 5933b880cfa82352b465db4c8c8dcccbab5edfac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23019 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 O|LwIrIKHJ9Ecm~Q@M|sr diff --git a/backend/migrations/versions/__pycache__/5ca03588ce28_add_penalty_system_and_wallet_.cpython-312.pyc b/backend/migrations/versions/__pycache__/5ca03588ce28_add_penalty_system_and_wallet_.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..874f99bcd1bd4632b1447b6457e01ac46b61b4a6 GIT binary patch literal 19072 zcmcIsTTB~Snzl`W!C)Yl#=!}JTs!0r2?WT65H1NMkc2ztQrJF)aqxwdZ3ww^x_4&O zE$vHMTB*CGm4=Z<8d}MNA8RxtjUO$|%W2u3Y1Pa=t@dHxTHD=PX`lB0%XYbp%a=+M zr2MWrm;Zd{`~P$5xc=YytJ!SQ!QVfQ{HuHWwodn-c$5BPQWrl8aPgIHN=I}y9nlkm zNAES*4Bj$ZnI7-UJdC&8RxVvLo(h{0%E~>J-YQ!alvUVFTs2oij5k0FskE87GPpNz z)s1-1td!v=)K^LMjIB0Ly(v{+m#4m3s;?zAzh-Rpq?VZBbKe%zTKD`nc0$;7&S&=o zx7ff|Ajo-HyN|G&c8`Y(vM#&Vz2bJ-gKodiWE$r-+yS_186N0q>2TUFU+L)Ta&qlm z$r>)u)6(u-;kw(qIygr+G4=DDJ;=2TfPk9I?VT4cw_j+x+|t(8)6v<})^@S2?edk* z%O=x*!!&iR2C;I2dm7?=PEIVJ^1(o@dePutAA=9mx<3$f@m%1kN32l}Emra=RAP;C zaM4Wsn?6>qG&^{^&$-Ha><-Qo5KV-GS(5$ud z5mn2GYVsn|uGL(ER`q$-fG^e+ooBwj^<-bszS?KAU!Ki@J>=-%2hsXb30l?JJ3tzU zr6_y(`j*0czP_bcT)Wi{$xJlnwVFDj#*%zcJ0h!$=&(jahd&UaBQm05uMS9=nSbRtsj7+vofM{8WEiXj?RAoL>FX47d0Zf_<>F%mt;hj^CHsj3vDH6 zRbRoENjtexl=*ypKa3w;m1onjha7d5pjCaI9VM;ce0@tfH&iOm!pUn`qt}Af>GCHv z>8s2q=_1{vhg>H&$W3zVU2poltfd#6v~R!bds~$n^Y*(tZ_AU{zwp%EFSA|0hc+B2 zL94nQ>f!sv;CuRcJ=lM!XkYEC_O3jedwDrhpUu6JW>cH-mGl)E4(>}|KiQwY%6yU$ zMg74{dHU*Y>Vn*V_aObM)Q)ITM%17YQ3JV0mWuj@c0{aIERU@X=Y)s1coLl9s z?&oz=>n`3-xR!u(mGjy!icRpk8eReixB!bUgzX;I=_efPChW=@w6{|g&%5kC_jBnbyWF)xY8MbI(;7vyvRy!|Q{F;1 zu_f8k3B15-Z8e?MiWXv{;$|0YtFnsl?hTHYag~Px93K#?&=#U3K=APenxK{jP7M2nN>P5z9_i~fpmhq$T9YCx|?GLqX-zvw0;gk2@mWgKWnL7}4 z`;sVl{JJ6TlgoS|4@9FpoW!Hd1uTl>9w+m7SUF=@+K5iBG-X3v}BrRb8!$WU`7)6@_1G9 zz1W=IH6%j?R^98n=CD7vfp|Q(j7=A%SujNq+L}P#7 zM1SAFU~3uQ0Dj6_&{-?}J_1{1kP{CkBf`5h8B?XL)jYl{5$g!=Ux#P14>+@fe~J1P zaX;*55CLXW4`db})HPsyaAK$56Y~0aJg|t&;Mmlti0+&!77YQrSkW&v5skyXAP0{6 zt?ox?toy&am+b57m%MH`9!PuDC72S1rURFh!C`d&0@3Y@>sz8RlssbazX7!n^VJ2m z;SJEIx}Pd^<;K4=4c{>h|Hgk6{4z+7T#s392y685VuD%vCsXlvrtv$b@#~Y@)$~Fy zy)#bxCg`+{PR-M`r$6X)1BMa9&pKWCeS_3ispw|Ra*N)cp!2Q-vzk*o5VH)@yEf|Z zB^bYA9NKavJR30yFQX(nO`UESAeYy#4M+}do zwvTi1+=^LxBTaPLkzkxTaIVEHUBZRv%LKEKQ`Z%qlE;ss6e`H-q{om3+5;j z*`SL~8hVjnUS`+7rhVf`L4Lwg9}-4{8_|=|2Fh}D^HqZRBu6>ACB5~K z&NvecQD~Njb=2&*gsSk1$Yf-Q+LkH*EWvE$%s}dEj)E82lzq)RuOo*drqb!Z z9`z)cd8LZ#otf~fus@ezC3IYl5i^~#Cm4savzcD%qXUz)e~Qj7(3wT*4S){}86HSJ zFq+F_(v!xiZ6(3Da%P#dlbWAhp&i4~;plZbw@7D~s4ob^+%=3!!;B?|Q7rAqv&a)V zF+;~^>8cM}_8IOWx`$(>t?t8F=yL0JHDQI2C~s~3Uzd3o(?Zi)`<%&_#E1f8YZN+li2q2ho3!s^Kjet&!&%m z%YMV|EC}6nXqMVs)a|EFUqJJI;PWS)?kMp2QS(>qm+baJxSQU3M8{clS~qh3LO)83s*%sYaB&V*59!W6M z3U0TncCHIYgxcL((&wX}jlQ|``}RL{|E`-}xcO)PFVFw@{7*0G^nCopLOiv*DyNXM z-^g1=o-IY$Sz7R0(V7G^m5uFMcs@KD8HjY!ITjup3Fetpuk3p}X6d8-N}o#mW(U3Y zAo?QerArRn^#faAs|SX0$r&bcowGA$xh6DamaKA+-4C7AtttHzSRa>g;ESw1%gpL1%IuLZW3Z z0(;ejLKUo-YvH-@{m98k18&0p(BGI1{#S5P%SzmW2UaaH@X)0hP?Yc85 zbk-uAtIxE#05{svWK!UoNi#WCV5aJ5j_)a&6Eg3|fg4O`%eN1xqdB#wXim#$P8G3~ zn#Z3aA8YXVgL{hnjEwvsX(eYh^9X5dd3PCg<_q=E?v>|d{O5rA3kC3N*L_i;yD&G> z4?^Iiq4*=`$fck;UGPLLEPa`@VQLw9i)$(4*mbbzQUV z3iZ&=?+uyX>%i8{J+<8}h3@w`dG%`Mrce*<+}xJ&_X0P4d&6ip`K>p>AOP`A|Bo}zgub2C=t zDW{GG(j52DQjcUb_2e-b*KDbYJw=mA@|u9}B~zNwOcy|-&Ss$=+Wme;W^=j-e(kzv z6}sPNFNV~pJ@gx!jApLLOx1ax-%~W11hDz{LbF%^jXM6l?V;TpmSp}HVHaTwtQ76K zmle7TuR`s*GYMmhMNTW~ygM{os!$K@_%kVDjw1NA>n1Yqj`#B7)&ksUN5jczTGdVz zTl&_O=lcdA)}EC_HpV}L(>jl_UcMegtAT%r*O;BbZ=yJX;yemWQ00Rt0w_WtTJ`(} zUazBgisCQ|9)%x;7sVV39||`L4~jJuPf&0uR#3Q5tfC+&94PE4@Z~1YqF6$)jKYRu z5yd=;Srqu*lb=S>gklQC42nq<11JVj457G-;vR}&6eB3^gP7#U@bUqQ$0+Wg=tprB zMIVZrC~l$XM1emk=X+7yMsXcQ4+>2BijPs8MsW&73yR|?PNF!5q8SDLbe^}OID`Vfm$g<(Sql6X7Q8_5 z9E4b%N?MR|5nf_3z9!{gq4*aRze4eA5Rmqk_9p%dxJMHP%)9di-UPp%+u0BxoH-)D z$Nu-@zdiZQN!r{Ivvvwqk!#Vp=zVHif-p-w6%xkq$D1H+Y8N`QgGTADeKG4DIxt7) z9hCD?;-~yZGO~{mw4WietB6RVqBh}lCc=}}o>hoZm9+N$n00^-+2{hHt^i#L(#;nT zFOR3<<;s?~V^#hw^Dm#zolrF@*(kS3cP=UOyJFUEh=p6|!~z{(q{{?C;&CZr|M}QwV{dNIV^`x$2Xsr{ zw#AutC8ou2X9U7=V|mMzv+RjkuM3OOdvwMQi4l}@Lm)J+h;gKO>yBA_gwg0VI<-V6 zS?XAUpk-W<)ltx-Nz&)(BL9O7yBHBao#*S3T6_jWfQSIoyg_dn3(~ zb9gA5<(Q?#p@Hxz!#witF|12b>8C}a(qNm0LA5wE+Wm?!6g{6{W)w5uRjBm1k(l*9 z9d%L;!~f6plKD6FBZy|y@EAWZn2OGj3Z^PM--%iK=^%I#EZ=py@{|UjL3lKt3Xdik zrcIZHhD?@+YN1jm(nKjPF=fnDqHr(tdM6gMQ7HiZZG z)V>A@D%2l<#Fuy~5hca+2c6#KOUiboJ}L0|9T}w)@G#k+WRpJs1TwO4eJC=raM#ebNbkn1ki@e{ zSxg1uAx#H9o+i^}?i%jn9VRT{Fsb8#5_-<^N9+c3x;^#SnGb#$g+^6&az?!WKcu8Fkgm8Dh7{KMmwb9 zJKWA>0;vi9gCDyGf9`tLz!N${~tqzp84Neo&Lms z=^Fl_v;2qd><<;Xea)X%esLnEJNkWF@2BQ3LT@I%KC^x0n+rP!=|>m7tGgJ}^?u*h zp literal 0 HcmV?d00001 diff --git a/backend/migrations/versions/__pycache__/6197bfddfb4f_add_lang_and_region_to_user.cpython-312.pyc b/backend/migrations/versions/__pycache__/6197bfddfb4f_add_lang_and_region_to_user.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c2e6e97231882ba763133c9a2dd151f9e14c0a5a GIT binary patch literal 19853 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 literal 0 HcmV?d00001 diff --git a/backend/migrations/versions/__pycache__/bc5669f12ffd_add_pending_actions_for_dual_control.cpython-312.pyc b/backend/migrations/versions/__pycache__/bc5669f12ffd_add_pending_actions_for_dual_control.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b891b2040ab225c6b1e64dcdc0bc3cb24c2815a6 GIT binary patch literal 18292 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 Vek~g@8aIr;Etuf?wNWPU{{enz<=p@P literal 0 HcmV?d00001 diff --git a/backend/migrations/versions/__pycache__/ffffad1dbe37_upgrade_audit_log_for_security.cpython-312.pyc b/backend/migrations/versions/__pycache__/ffffad1dbe37_upgrade_audit_log_for_security.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..497dad5565cb96159b318edb788c175187e810b9 GIT binary patch literal 20881 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 None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey') + op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey') + op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey') + op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey') + op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey') + op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey') + op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey') + op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey') + op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey') + op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey') + op.create_foreign_key(None, 'assets', 'vehicle_catalog', ['catalog_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('audit_logs_user_id_fkey'), 'audit_logs', type_='foreignkey') + op.create_foreign_key(None, 'audit_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('credit_logs_org_id_fkey'), 'credit_logs', type_='foreignkey') + op.create_foreign_key(None, 'credit_logs', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('documents_uploaded_by_fkey'), 'documents', type_='foreignkey') + op.create_foreign_key(None, 'documents', 'users', ['uploaded_by'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey') + op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.create_foreign_key(None, 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'org_subscriptions', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('organization_members_user_id_fkey'), 'organization_members', type_='foreignkey') + op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey') + op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + existing_nullable=True) + op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey') + op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey') + op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey') + op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey') + op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey') + op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('service_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey') + op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey') + op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey') + op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey') + op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey') + op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey') + op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE') + op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey') + op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id']) + op.drop_constraint(None, 'users', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id']) + op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id']) + op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id']) + op.drop_constraint(None, 'ratings', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id']) + op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'persons', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id']) + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id']) + op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id']) + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + existing_nullable=True) + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organization_members_organization_id_fkey'), 'organization_members', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('organization_members_user_id_fkey'), 'organization_members', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id']) + op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id']) + op.drop_constraint(None, 'documents', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id']) + op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'assets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id']) + op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id']) + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id']) + # ### end Alembic commands ### diff --git a/backend/migrations/versions/ffffad1dbe37_upgrade_audit_log_for_security.py b/backend/migrations/versions/ffffad1dbe37_upgrade_audit_log_for_security.py new file mode 100644 index 0000000..5c97066 --- /dev/null +++ b/backend/migrations/versions/ffffad1dbe37_upgrade_audit_log_for_security.py @@ -0,0 +1,192 @@ +"""upgrade_audit_log_for_security + +Revision ID: ffffad1dbe37 +Revises: 8e06c5386cba +Create Date: 2026-02-10 17:33:17.436161 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'ffffad1dbe37' +down_revision: Union[str, Sequence[str], None] = '8e06c5386cba' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey') + op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey') + op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey') + op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey') + op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey') + op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey') + op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey') + op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey') + op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey') + op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey') + op.create_foreign_key(None, 'assets', 'vehicle_catalog', ['catalog_id'], ['id'], source_schema='data', referent_schema='data') + op.add_column('audit_logs', sa.Column('severity', sa.Enum('info', 'warning', 'critical', 'emergency', name='logseverity'), nullable=False)) + op.add_column('audit_logs', sa.Column('old_data', sa.JSON(), nullable=True)) + op.add_column('audit_logs', sa.Column('new_data', sa.JSON(), nullable=True)) + op.add_column('audit_logs', sa.Column('ip_address', sa.String(length=45), nullable=True)) + op.add_column('audit_logs', sa.Column('user_agent', sa.Text(), nullable=True)) + op.create_index(op.f('ix_data_audit_logs_action'), 'audit_logs', ['action'], unique=False, schema='data') + op.create_index(op.f('ix_data_audit_logs_ip_address'), 'audit_logs', ['ip_address'], unique=False, schema='data') + op.create_index(op.f('ix_data_audit_logs_timestamp'), 'audit_logs', ['timestamp'], unique=False, schema='data') + op.drop_constraint(op.f('audit_logs_user_id_fkey'), 'audit_logs', type_='foreignkey') + op.create_foreign_key(None, 'audit_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_column('audit_logs', 'changes') + op.drop_constraint(op.f('credit_logs_org_id_fkey'), 'credit_logs', type_='foreignkey') + op.create_foreign_key(None, 'credit_logs', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('documents_uploaded_by_fkey'), 'documents', type_='foreignkey') + op.create_foreign_key(None, 'documents', 'users', ['uploaded_by'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey') + op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.create_foreign_key(None, 'org_subscriptions', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('organization_members_user_id_fkey'), 'organization_members', type_='foreignkey') + op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey') + op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + existing_nullable=True) + op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey') + op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey') + op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey') + op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey') + op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey') + op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('service_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey') + op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey') + op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey') + op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey') + op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey') + op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey') + op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE') + op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey') + op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id']) + op.drop_constraint(None, 'users', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id']) + op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id']) + op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id']) + op.drop_constraint(None, 'ratings', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id']) + op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'persons', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id']) + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id']) + op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id']) + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + existing_nullable=True) + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organization_members_organization_id_fkey'), 'organization_members', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('organization_members_user_id_fkey'), 'organization_members', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id']) + op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id']) + op.drop_constraint(None, 'documents', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id']) + op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id']) + op.add_column('audit_logs', sa.Column('changes', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id']) + op.drop_index(op.f('ix_data_audit_logs_timestamp'), table_name='audit_logs', schema='data') + op.drop_index(op.f('ix_data_audit_logs_ip_address'), table_name='audit_logs', schema='data') + op.drop_index(op.f('ix_data_audit_logs_action'), table_name='audit_logs', schema='data') + op.drop_column('audit_logs', 'user_agent') + op.drop_column('audit_logs', 'ip_address') + op.drop_column('audit_logs', 'new_data') + op.drop_column('audit_logs', 'old_data') + op.drop_column('audit_logs', 'severity') + op.drop_constraint(None, 'assets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id']) + op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id']) + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id']) + # ### end Alembic commands ### diff --git a/backend/static/locales/en.json b/backend/static/locales/en.json new file mode 100644 index 0000000..ebb70b5 --- /dev/null +++ b/backend/static/locales/en.json @@ -0,0 +1,23 @@ +{ + "AUTH": { + "LOGIN": { + "SUCCESS": "Login successful. Welcome back!" + }, + "ERROR": { + "EMAIL_EXISTS": "This email is already registered." + } + }, + "SENTINEL": { + "LOCK": { + "MSG": "Account locked for security reasons." + } + }, + "EMAIL": { + "REG": { + "SUBJECT": "Confirm your registration" + }, + "PWD_RESET": { + "SUBJECT": "Password Reset Request" + } + } +} \ No newline at end of file diff --git a/backend/static/locales/hu.json b/backend/static/locales/hu.json new file mode 100644 index 0000000..4f623ec --- /dev/null +++ b/backend/static/locales/hu.json @@ -0,0 +1,31 @@ +{ + "AUTH": { + "LOGIN": { + "SUCCESS": "Sikeres bejelentkezés. Üdvözlünk ismét!" + }, + "ERROR": { + "EMAIL_EXISTS": "Ez az e-mail cím már foglalt.", + "UNAUTHORIZED": "Nincs jogosultságod a művelethez." + } + }, + "SENTINEL": { + "LOCK": { + "MSG": "A fiók biztonsági okokból zárolva lett." + }, + "APPROVAL": { + "REQUIRED": "A művelet végrehajtásához egy másik admin jóváhagyása szükséges." + } + }, + "EMAIL": { + "REG": { + "SUBJECT": "Regisztráció megerősítése - Service Finder", + "GREETING": "Szia {{first_name}}! Kattints a linkre a flottád aktiválásához: {{link}}" + }, + "PWD_RESET": { + "SUBJECT": "Jelszó visszaállítás" + } + }, + "COMMON": { + "SAVE_SUCCESS": "Sikeres mentés!" + } +} \ No newline at end of file diff --git a/docs/V01_gemini/11_Gamification_Social.md b/docs/V01_gemini/11_Gamification_Social.md index 93bd6fc..e910ab1 100644 --- a/docs/V01_gemini/11_Gamification_Social.md +++ b/docs/V01_gemini/11_Gamification_Social.md @@ -101,4 +101,47 @@ A rendszer különválasztja a tekintélyt és a jutalmat: Minden érték (szorzók, határok) a \`GAMIFICATION_MASTER_CONFIG\` JSON paraméterben állítható Admin felületről, kódmódosítás nélkül. ### 3. Audit -Minden pontmozgás a \`PointsLedger\` táblába kerül rögzítésre a visszakövethetőség érdekében. \ No newline at end of file +Minden pontmozgás a \`PointsLedger\` táblába kerül rögzítésre a visszakövethetőség érdekében. + +XP Formula: $XP_{required} = BaseXP \times Level^{1.5}$Penalty Logic: restriction_level bevezetése (0-3).Weighting: Saját adat vs. Közösségi adat súlyozási táblázata. + +# 11. Gamification és Social Engine Specifikáció + +## 1. XP (Experience Points) - A Tekintély +Az XP a felhasználó végleges, nem csökkenthető tekintélypontja. +- **Képlet:** A szintlépéshez szükséges összes XP: + $$XP_{total} = 500 \times Level^{1.5}$$ +- **Súlyozás:** + - **Saját adat (Fleet):** Alacsony érték (pl. 10 XP). + - **Közösségi adat (Service Discovery):** Magas érték (pl. 100 XP). + +## 2. Social Points - A Valuta Alapja +Szezonális pontok, amelyek Kreditre válthatóak. +- **Váltószám:** Alapértelmezett: 100 Social Point = 1 Kredit. +- **Váltási mód:** Automatikus (rendszerparaméter alapján) vagy manuális (felhasználói döntés). + +## 3. Trust & Penalty Engine (Büntetőrendszer) +A rendszer integritásának védelme érdekében hibapontokat (Penalty Points) alkalmazunk. +- **Szintek (Restriction Level):** + - **0 (Normal):** Teljes pontszorzó (1.0x). + - **1 (Warning):** Csökkentett pontszerzés (0.5x). + - **2 (Restricted):** Szigorú moderátori ellenőrzés minden adatnál, 0.1x pontszerzés. + - **3 (Blocked):** Pontszerzés és adatbeküldés tiltva. +- **Ledolgozás:** Minden pozitív XP szerzés a büntetőpontokat is csökkenti (pl. 1 XP jóváírás = 0.5 Penalty pont levonás). + +## 4. Szintlépési Bónuszok +Minden 10. szint elérésekor a rendszer automatikus Kredit jutalmat oszt a `GAMIFICATION_MASTER_CONFIG` alapján. + +## 5. Büntetőrendszer (Strike System) +A rendszer integritásának megőrzése érdekében hibapontokat alkalmazunk, amelyek befolyásolják a pontszerzés hatékonyságát. + +- **Szorzók (Multipliers):** + - Level 0 (Normal): 1.0x + - Level 1 (Warning): 0.5x + - Level 2 (Restricted): 0.1x + - Level 3 (Blocked): 0.0x + +- **Ledolgozás (Recovery):** + A büntetőpontok pozitív aktivitással (XP szerzéssel) ledolgozhatóak. Az elért XP egy admin által meghatározott része (alapértelmezett: 50%) levonásra kerül a büntetőpontokból. + +- **Admin-Vezérelt Küszöbök:** Minden szintváltási határ a `GAMIFICATION_MASTER_CONFIG` paraméterben definiált. \ No newline at end of file diff --git a/docs/V01_gemini/15_Changelog.md b/docs/V01_gemini/15_Changelog.md index c03cedb..e4fd6c9 100644 --- a/docs/V01_gemini/15_Changelog.md +++ b/docs/V01_gemini/15_Changelog.md @@ -249,4 +249,41 @@ A rendszer most már képes egyetlen KYC folyamat alatt aktiválni a felhasznál ### Changed - `AssetCost` modell mezőnevek szinkronizálva a pénzügyi standardokhoz (`amount_local`, `amount_eur`). -- `SystemParameter` modell elnevezés igazítva a meglévő adatbázis sémához. \ No newline at end of file +- `SystemParameter` modell elnevezés igazítva a meglévő adatbázis sémához. + +## [1.5.0] - 2026-02-10 + +### Added +- **Judge & Penalty System**: Bevezetve a `penalty_points` és `restriction_level` mechanizmus a visszaélések kiszűrésére. +- **Dynamic Multipliers**: Admin felületről (JSON config) állítható pontszorzók a büntetési szintekhez. +- **Social-to-Credit Auto-conversion**: Automatikus Kredit jóváírás a Walletbe meghatározott Social pont elérésekor. +- **Level Achievement Bonus**: 10-es szintenkénti automatikus Kredit jutalmazás. + +### Fixed +- **Circular Dependency Fix**: A modellek közötti import hurok végleges felszámolva (string-alapú relationship hivatkozások). +- **Identity Schema Protection**: Visszaállítva a `User` modell hiányzó `scope_id`, `scope_level` és `custom_permissions` mezői. +- **Database Consistency**: A `user_stats` és `asset_costs` táblák sikeresen migráltak a NOT NULL kényszerek és alapértelmezett értékek (server_default) beállításával. + +### Changed +- **GamificationService**: Mostantól központi "Bíróként" funkcionál, leválasztva a pontszámítási logikát a többi szervizről. +- **Identity Model**: A `Wallet` és `VerificationToken` osztályok integrálva az `identity.py` modulba. + +# Changelog - Service Finder Backend +**Verzió:** 1.6.0 (Sentinel & i18n Update) +**Dátum:** 2026.02.10. + +## [1.6.0] - 2026-02-10 +### Hozzáadva +- **Sentinel Biztonsági Rendszer:** - `PendingAction` modell bevezetése a "Négy szem elv" (Dual Control) biztosításához. + - `SecurityService` implementálása: Adatlopás elleni védelem (Throttling) és automata vészleállító (Emergency Lock). + - `AuditLog` bővítése szigorúbb súlyossági szintekkel (`critical`, `emergency`). +- **Nyelvi Modul (i18n):** + - `Translation` modell a `data` sémában. + - `TranslationService`: Adatbázis-alapú fordításkezelés, szerveroldali cache, Fallback (EN) logika és JSON export funkció a Frontend számára. +- **Admin Kontroll Panel:** + - Új API végpontok a függőben lévő műveletek jóváhagyásához, a rendszerbiztonsági állapot monitorozásához és a nyelvi szinkronizációhoz. + +### Javítva +- **Circular Import Fix:** A modellek importálási rendjének optimalizálása a `app.db.base_class` közvetlen használatával, megszüntetve a hurok-importokat. +- **Függőségkezelés:** `deps.py` bővítve a `get_current_active_user` függőséggel a biztonsági zárolások érvényesítéséhez. +- **Soft-Delete Logika:** A felhasználói fiók törlése mostantól felszabadítja az eredeti e-mail címet a TWINS-elvű újra-regisztrációhoz. \ No newline at end of file diff --git a/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md b/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md index 029e2fe..c72a30a 100644 --- a/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md +++ b/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md @@ -102,4 +102,20 @@ A rendszer a \`system_parameters\` táblában tárolt \`RBAC_MASTER_CONFIG\` JSO ### 2. Scope (Hatókör) Védelem Minden műveletnél ellenőrizzük a \`scope_id\` egyezését: - Ha a felhasználó \`scope_level = 'region'\`, akkor csak olyan adatot szerkeszthet, ami ugyanahhoz a régióhoz tartozik. -- Kivétel: Impersonation (Megszemélyesítés) - Audit loggal védve. \ No newline at end of file +- Kivétel: Impersonation (Megszemélyesítés) - Audit loggal védve. + + +## 1. Gamification Adminisztráció +A `data.system_parameters` táblában a `GAMIFICATION_MASTER_CONFIG` kulcs alatt az alábbiak állíthatóak: +- `xp_logic`: `base_xp`, `exponent`. +- `penalty_thresholds`: A szintekhez tartozó büntetőpont határok. +- `level_up_rewards`: 10-es szintenkénti Kredit jutalom mértéke. +- `blocked_roles`: [superadmin, service_bot]. +- `auto_convert_social`: True/False. + +## 2. Gamification Konfiguráció (JSON Schema) +A `GAMIFICATION_MASTER_CONFIG` struktúrája: +- `xp_logic`: Alap XP és kitevő a nehezedő szintezéshez. +- `penalty_logic`: Küszöbértékek, szorzók és ledolgozási ráta. +- `conversion_logic`: Social-to-Credit váltási arány. +- `level_rewards`: Szintlépési bónuszok mértéke. \ No newline at end of file