feat: Asset Catalog system, PostGIS integration and RobotScout V1
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
from typing import Optional, Dict, Any
|
||||
from typing import Optional, Dict, Any, Union
|
||||
import logging
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
@@ -6,26 +6,37 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.core.security import decode_token, RANK_MAP
|
||||
from app.models.identity import User
|
||||
from app.core.security import decode_token, DEFAULT_RANK_MAP
|
||||
from app.models.identity import User, UserRole
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
|
||||
|
||||
# Az OAuth2 folyamat a bejelentkezési végponton keresztül
|
||||
reusable_oauth2 = OAuth2PasswordBearer(
|
||||
tokenUrl=f"{settings.API_V1_STR}/auth/login"
|
||||
)
|
||||
|
||||
async def get_current_token_payload(
|
||||
token: str = Depends(reusable_oauth2)
|
||||
) -> Dict[str, Any]:
|
||||
if token == "dev_bypass_active":
|
||||
return {
|
||||
"""
|
||||
JWT token visszafejtése és a típus (access) ellenőrzése.
|
||||
"""
|
||||
# Dev bypass (ha esetleg fejlesztéshez használtad korábban, itt a helye,
|
||||
# de élesben a token validáció fut le)
|
||||
if settings.DEBUG and token == "dev_bypass_active":
|
||||
return {
|
||||
"sub": "1",
|
||||
"role": "superadmin",
|
||||
"rank": 100,
|
||||
"scope_level": "global",
|
||||
"scope_id": "all"
|
||||
"scope_id": "all",
|
||||
"type": "access"
|
||||
}
|
||||
|
||||
payload = decode_token(token)
|
||||
if not payload:
|
||||
if not payload or payload.get("type") != "access":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Érvénytelen vagy lejárt munkamenet."
|
||||
@@ -33,39 +44,93 @@ async def get_current_token_payload(
|
||||
return payload
|
||||
|
||||
async def get_current_user(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
payload: Dict[str, Any] = Depends(get_current_token_payload),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
payload: Dict = Depends(get_current_token_payload)
|
||||
) -> User:
|
||||
"""
|
||||
Lekéri a felhasználót a token 'sub' mezője alapján.
|
||||
"""
|
||||
user_id = payload.get("sub")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token azonosítási hiba.")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Token azonosítási hiba."
|
||||
)
|
||||
|
||||
result = await db.execute(select(User).where(User.id == int(user_id)))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user or user.is_deleted:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="A felhasználó nem található.")
|
||||
|
||||
if not user or user.is_deleted:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="A felhasználó nem található."
|
||||
)
|
||||
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."""
|
||||
"""
|
||||
Ellenőrzi, hogy a felhasználó aktív-e.
|
||||
Ez elengedhetetlen az Admin felület és a védett végpontok számára.
|
||||
"""
|
||||
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."
|
||||
detail="A művelethez aktív profil és KYC azonosítás (Step 2) szükséges."
|
||||
)
|
||||
return current_user
|
||||
|
||||
def check_min_rank(required_rank: int):
|
||||
def rank_checker(payload: Dict[str, Any] = Depends(get_current_token_payload)):
|
||||
async def check_resource_access(
|
||||
resource_scope_id: Union[str, int],
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Scoped RBAC: Megakadályozza, hogy egy felhasználó más valaki erőforrásaihoz nyúljon.
|
||||
Kezeli az ID-t (int) és a Scope ID-t / Slug-ot (str) is.
|
||||
"""
|
||||
if current_user.role == UserRole.superadmin:
|
||||
return True
|
||||
|
||||
# Ha a usernek van beállított scope_id-ja (pl. egy flottához tartozik),
|
||||
# akkor ellenőrizzük, hogy a kért erőforrás abba a scope-ba tartozik-e.
|
||||
user_scope = current_user.scope_id
|
||||
requested_scope = str(resource_scope_id)
|
||||
|
||||
# 1. Saját erőforrás (saját ID)
|
||||
if str(current_user.id) == requested_scope:
|
||||
return True
|
||||
|
||||
# 2. Scope alapú hozzáférés (pl. flotta tagja)
|
||||
if user_scope and user_scope == requested_scope:
|
||||
return True
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Nincs jogosultsága ehhez az erőforráshoz."
|
||||
)
|
||||
|
||||
def check_min_rank(role_key: str):
|
||||
"""
|
||||
Dinamikus Rank ellenőrzés.
|
||||
Az adatbázisból (system_parameters) kéri le az elvárt szintet.
|
||||
"""
|
||||
async def rank_checker(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
payload: Dict = Depends(get_current_token_payload)
|
||||
):
|
||||
# A settings.get_db_setting-et használjuk a dinamikus lekéréshez
|
||||
ranks = await settings.get_db_setting(
|
||||
db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP
|
||||
)
|
||||
|
||||
required_rank = ranks.get(role_key, 0)
|
||||
user_rank = payload.get("rank", 0)
|
||||
|
||||
if user_rank < required_rank:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Nincs elegendő jogosultsága a művelethez. (Szükséges szint: {required_rank})"
|
||||
detail=f"Alacsony jogosultsági szint. (Szükséges: {required_rank})"
|
||||
)
|
||||
return True
|
||||
return rank_checker
|
||||
Binary file not shown.
@@ -1,11 +1,15 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from authlib.integrations.starlette_client import OAuth
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.services.auth_service import AuthService
|
||||
from app.core.security import create_access_token, RANK_MAP
|
||||
from app.services.social_auth_service import SocialAuthService
|
||||
from app.core.security import create_tokens, DEFAULT_RANK_MAP
|
||||
from app.core.config import settings
|
||||
from app.schemas.auth import (
|
||||
UserLiteRegister, Token, PasswordResetRequest,
|
||||
UserKYCComplete, PasswordResetConfirm
|
||||
@@ -15,57 +19,50 @@ from app.models.identity import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/register-lite", response_model=Token, status_code=status.HTTP_201_CREATED)
|
||||
async def register_lite(user_in: UserLiteRegister, db: AsyncSession = Depends(get_db)):
|
||||
"""Step 1: Alapszintű regisztráció. Az új felhasználó alapértelmezetten 'user' (Rank 10)."""
|
||||
stmt = select(User).where(User.email == user_in.email)
|
||||
result = await db.execute(stmt)
|
||||
if result.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Ez az e-mail cím már regisztrálva van."
|
||||
)
|
||||
|
||||
try:
|
||||
user = await AuthService.register_lite(db, user_in)
|
||||
|
||||
# Kezdeti token generálása
|
||||
token_data = {
|
||||
"sub": str(user.id),
|
||||
"role": "user",
|
||||
"rank": 10,
|
||||
"scope_level": "individual",
|
||||
"scope_id": str(user.id)
|
||||
}
|
||||
|
||||
token = create_access_token(data=token_data)
|
||||
return {
|
||||
"access_token": token,
|
||||
"token_type": "bearer",
|
||||
"is_active": user.is_active
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Sikertelen regisztráció: {str(e)}"
|
||||
)
|
||||
# --- GOOGLE OAUTH KONFIGURÁCIÓ ---
|
||||
oauth = OAuth()
|
||||
oauth.register(
|
||||
name='google',
|
||||
client_id=settings.GOOGLE_CLIENT_ID,
|
||||
client_secret=settings.GOOGLE_CLIENT_SECRET,
|
||||
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
|
||||
client_kwargs={'scope': 'openid email profile'}
|
||||
)
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
form_data: OAuth2PasswordRequestForm = Depends()
|
||||
):
|
||||
"""Bejelentkezés és okos JWT generálása RBAC adatokkal."""
|
||||
user = await AuthService.authenticate(db, form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Hibás e-mail cím vagy jelszó."
|
||||
)
|
||||
|
||||
# Szerepkör string kinyerése és rang meghatározása a RANK_MAP-ből
|
||||
# --- SOCIAL AUTH ENDPOINTS ---
|
||||
|
||||
@router.get("/login/google")
|
||||
async def login_google(request: Request):
|
||||
"""
|
||||
Step 1: Átirányítás a Google bejelentkező oldalára.
|
||||
"""
|
||||
redirect_uri = settings.GOOGLE_CALLBACK_URL
|
||||
return await oauth.google.authorize_redirect(request, redirect_uri)
|
||||
|
||||
@router.get("/callback/google")
|
||||
async def auth_google(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
Step 2: Google visszahívás lekezelése + Dupla Token generálás.
|
||||
"""
|
||||
try:
|
||||
token = await oauth.google.authorize_access_token(request)
|
||||
user_info = token.get('userinfo')
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Google hitelesítési hiba.")
|
||||
|
||||
if not user_info:
|
||||
raise HTTPException(status_code=400, detail="Nincs adat a Google-től.")
|
||||
|
||||
# Step 1: Technikai user létrehozása/keresése (inaktív, nincs mappa)
|
||||
user = await SocialAuthService.get_or_create_social_user(
|
||||
db, provider="google", social_id=user_info['sub'], email=user_info['email'],
|
||||
first_name=user_info.get('given_name'), last_name=user_info.get('family_name')
|
||||
)
|
||||
|
||||
# Dinamikus token generálás
|
||||
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP)
|
||||
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
|
||||
user_rank = RANK_MAP.get(role_name, 10)
|
||||
user_rank = ranks.get(role_name, 10)
|
||||
|
||||
token_data = {
|
||||
"sub": str(user.id),
|
||||
@@ -76,48 +73,104 @@ async def login(
|
||||
"region": user.region_code
|
||||
}
|
||||
|
||||
token = create_access_token(data=token_data)
|
||||
access, refresh = create_tokens(data=token_data)
|
||||
|
||||
# Visszatérés a frontendre mindkét tokennel
|
||||
response_url = f"{settings.FRONTEND_BASE_URL}/auth/callback?access={access}&refresh={refresh}"
|
||||
return RedirectResponse(url=response_url)
|
||||
|
||||
|
||||
# --- STANDARD AUTH ENDPOINTS ---
|
||||
|
||||
@router.post("/register-lite", response_model=Token, status_code=status.HTTP_201_CREATED)
|
||||
async def register_lite(user_in: UserLiteRegister, db: AsyncSession = Depends(get_db)):
|
||||
"""Step 1: Manuális regisztráció (inaktív, nincs mappa)."""
|
||||
stmt = select(User).where(User.email == user_in.email)
|
||||
if (await db.execute(stmt)).scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail="Email már regisztrálva.")
|
||||
|
||||
user = await AuthService.register_lite(db, user_in)
|
||||
|
||||
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP)
|
||||
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
|
||||
|
||||
token_data = {
|
||||
"sub": str(user.id),
|
||||
"role": role_name,
|
||||
"rank": ranks.get(role_name, 10),
|
||||
"scope_level": "individual",
|
||||
"scope_id": str(user.id),
|
||||
"region": user.region_code
|
||||
}
|
||||
|
||||
access, refresh = create_tokens(data=token_data)
|
||||
return {
|
||||
"access_token": token,
|
||||
"access_token": access,
|
||||
"refresh_token": refresh,
|
||||
"token_type": "bearer",
|
||||
"is_active": user.is_active
|
||||
}
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
"""Hagyományos belépés + Dupla Token."""
|
||||
user = await AuthService.authenticate(db, form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(status_code=401, detail="Hibás adatok.")
|
||||
|
||||
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP)
|
||||
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
|
||||
user_rank = ranks.get(role_name, 10)
|
||||
|
||||
token_data = {
|
||||
"sub": str(user.id),
|
||||
"role": role_name,
|
||||
"rank": user_rank,
|
||||
"scope_level": user.scope_level or "individual",
|
||||
"scope_id": user.scope_id or str(user.id),
|
||||
"region": user.region_code
|
||||
}
|
||||
|
||||
access, refresh = create_tokens(data=token_data)
|
||||
return {
|
||||
"access_token": access,
|
||||
"refresh_token": refresh,
|
||||
"token_type": "bearer",
|
||||
"is_active": user.is_active
|
||||
}
|
||||
|
||||
@router.get("/verify-email")
|
||||
async def verify_email(token: str, db: AsyncSession = Depends(get_db)):
|
||||
"""E-mail megerősítése."""
|
||||
success = await AuthService.verify_email(db, token)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="Érvénytelen vagy lejárt token.")
|
||||
return {"message": "Email sikeresen megerősítve!"}
|
||||
if not await AuthService.verify_email(db, token):
|
||||
raise HTTPException(status_code=400, detail="Érvénytelen token.")
|
||||
return {"message": "Email megerősítve!"}
|
||||
|
||||
@router.post("/complete-kyc")
|
||||
async def complete_kyc(
|
||||
kyc_in: UserKYCComplete,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Step 2: KYC adatok rögzítése és aktiválás."""
|
||||
"""
|
||||
Step 2: KYC Aktiválás.
|
||||
Itt használjuk a get_current_user-t (nem active), mert a user még inaktív.
|
||||
"""
|
||||
user = await AuthService.complete_kyc(db, current_user.id, kyc_in)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Felhasználó nem található.")
|
||||
return {"status": "success", "message": "A profil aktiválva."}
|
||||
raise HTTPException(status_code=404, detail="User nem található.")
|
||||
return {"status": "success", "message": "Fiók aktiválva."}
|
||||
|
||||
@router.post("/forgot-password")
|
||||
async def forgot_password(req: PasswordResetRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""Elfelejtett jelszó folyamat."""
|
||||
result = await AuthService.initiate_password_reset(db, req.email)
|
||||
if result == "cooldown":
|
||||
raise HTTPException(status_code=429, detail="Kérjük várjon 2 percet.")
|
||||
return {"message": "Amennyiben a cím létezik, a linket kiküldtük."}
|
||||
raise HTTPException(status_code=429, detail="Túl sok kérés.")
|
||||
return {"message": "Visszaállító link kiküldve."}
|
||||
|
||||
@router.post("/reset-password")
|
||||
async def reset_password(req: PasswordResetConfirm, db: AsyncSession = Depends(get_db)):
|
||||
"""Új jelszó beállítása."""
|
||||
if req.password != req.password_confirm:
|
||||
raise HTTPException(status_code=400, detail="A jelszavak nem egyeznek.")
|
||||
|
||||
success = await AuthService.reset_password(db, req.email, req.token, req.password)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="Hiba a jelszó frissítésekor.")
|
||||
return {"message": "A jelszó sikeresen frissítve!"}
|
||||
raise HTTPException(status_code=400, detail="Nem egyeznek a jelszavak.")
|
||||
if not await AuthService.reset_password(db, req.email, req.token, req.password):
|
||||
raise HTTPException(status_code=400, detail="Sikertelen frissítés.")
|
||||
return {"message": "Jelszó frissítve!"}
|
||||
@@ -1,37 +1,46 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, or_
|
||||
from app.db.session import get_db
|
||||
from app.models import AssetCatalog
|
||||
from app.services.asset_service import AssetService
|
||||
from typing import List
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/search")
|
||||
async def search_catalog(
|
||||
q: str = Query(..., min_length=2, description="Márka vagy típus keresése"),
|
||||
category: str = None,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Keresés a Robot által feltöltött katalógusban."""
|
||||
stmt = select(VehicleCatalog).where(
|
||||
or_(
|
||||
VehicleCatalog.brand.ilike(f"%{q}%"),
|
||||
VehicleCatalog.model.ilike(f"%{q}%")
|
||||
)
|
||||
)
|
||||
if category:
|
||||
stmt = stmt.where(VehicleCatalog.category == category)
|
||||
|
||||
result = await db.execute(stmt.limit(20))
|
||||
items = result.scalars().all()
|
||||
@router.get("/makes", response_model=List[str])
|
||||
async def list_makes(db: AsyncSession = Depends(get_db)):
|
||||
"""1. Szint: Márkák listázása."""
|
||||
return await AssetService.get_makes(db)
|
||||
|
||||
@router.get("/models", response_model=List[str])
|
||||
async def list_models(make: str, db: AsyncSession = Depends(get_db)):
|
||||
"""2. Szint: Típusok listázása egy adott márkához."""
|
||||
models = await AssetService.get_models(db, make)
|
||||
if not models:
|
||||
raise HTTPException(status_code=404, detail="Márka nem található vagy nincsenek típusok.")
|
||||
return models
|
||||
|
||||
@router.get("/generations", response_model=List[str])
|
||||
async def list_generations(make: str, model: str, db: AsyncSession = Depends(get_db)):
|
||||
"""3. Szint: Generációk/Évjáratok listázása."""
|
||||
generations = await AssetService.get_generations(db, make, model)
|
||||
if not generations:
|
||||
raise HTTPException(status_code=404, detail="Nincs generációs adat ehhez a típushoz.")
|
||||
return generations
|
||||
|
||||
@router.get("/engines")
|
||||
async def list_engines(make: str, model: str, gen: str, db: AsyncSession = Depends(get_db)):
|
||||
"""4. Szint: Motorváltozatok és technikai specifikációk."""
|
||||
engines = await AssetService.get_engines(db, make, model, gen)
|
||||
if not engines:
|
||||
raise HTTPException(status_code=404, detail="Nincs motorváltozat adat.")
|
||||
|
||||
# Itt visszaküldjük a teljes katalógus objektumokat (ID, motorváltozat, specifikációk)
|
||||
return [
|
||||
{
|
||||
"id": i.id,
|
||||
"full_name": f"{i.brand} {i.model}",
|
||||
"category": i.category,
|
||||
"status": i.verification_status,
|
||||
"specs": i.factory_specs
|
||||
} for i in items
|
||||
"id": e.id,
|
||||
"variant": e.engine_variant,
|
||||
"engine_code": e.engine_code,
|
||||
"fuel_type": e.fuel_type,
|
||||
"factory_data": e.factory_data
|
||||
} for e in engines
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
@@ -6,21 +6,21 @@ 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
|
||||
# --- Paths ---
|
||||
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"
|
||||
PROJECT_NAME: str = "Service Finder Ecosystem"
|
||||
VERSION: str = "2.1.0"
|
||||
API_V1_STR: str = "/api/v1"
|
||||
DEBUG: bool = False
|
||||
|
||||
# --- Security / JWT ---
|
||||
SECRET_KEY: str = "NOT_SET_DANGER"
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 nap
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
|
||||
|
||||
# --- Initial Admin ---
|
||||
INITIAL_ADMIN_EMAIL: str = "admin@servicefinder.hu"
|
||||
@@ -42,21 +42,35 @@ class Settings(BaseSettings):
|
||||
SMTP_PASSWORD: Optional[str] = None
|
||||
|
||||
# --- External URLs ---
|
||||
FRONTEND_BASE_URL: str = "http://localhost:3000"
|
||||
FRONTEND_BASE_URL: str = "https://dev.profibot.hu"
|
||||
|
||||
# --- Dinamikus Admin Motor ---
|
||||
# --- Google OAuth ---
|
||||
GOOGLE_CLIENT_ID: str = ""
|
||||
GOOGLE_CLIENT_SECRET: str = ""
|
||||
GOOGLE_CALLBACK_URL: str = "https://dev.profibot.hu/api/v1/auth/callback/google"
|
||||
|
||||
# --- Brute-Force & Security ---
|
||||
LOGIN_RATE_LIMIT_ANON: str = "5/minute"
|
||||
AUTH_MIN_PASSWORD_LENGTH: int = 8
|
||||
|
||||
# --- Dinamikus Admin Motor (Javított) ---
|
||||
async def get_db_setting(self, db: AsyncSession, key_name: str, default: Any = None) -> Any:
|
||||
"""
|
||||
Lekér egy beállítást a data.system_parameters táblából.
|
||||
Ha a tábla még nem létezik (migráció előtt), elkapja a hibát és default-ot ad.
|
||||
"""
|
||||
try:
|
||||
query = text("SELECT value_json FROM data.system_settings WHERE key_name = :key")
|
||||
# A lekérdezés a system_parameters táblát és a 'key' mezőt használja
|
||||
query = text("SELECT value FROM data.system_parameters WHERE key = :key")
|
||||
result = await db.execute(query, {"key": key_name})
|
||||
row = result.fetchone()
|
||||
if row and row[0] is not None:
|
||||
return row[0]
|
||||
return default
|
||||
except Exception:
|
||||
# Adatbázis hiba vagy hiányzó tábla esetén fallback az alapértelmezett értékre
|
||||
return default
|
||||
|
||||
# .env fájl konfigurációja
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
|
||||
@@ -1,65 +1,48 @@
|
||||
import secrets
|
||||
import string
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional, Dict, Any
|
||||
from typing import Optional, Dict, Any, Tuple
|
||||
import bcrypt
|
||||
from jose import jwt, JWTError
|
||||
from app.core.config import settings
|
||||
from fastapi_limiter import FastAPILimiter
|
||||
from fastapi_limiter.depends import RateLimiter
|
||||
|
||||
# Master Book 5.0: RBAC Rank Definition Matrix
|
||||
# Ezek a szintek határozzák meg a hozzáférést a Middleware szintjén.
|
||||
RANK_MAP = {
|
||||
"superadmin": 100,
|
||||
"country_admin": 80,
|
||||
"region_admin": 60,
|
||||
"moderator": 40,
|
||||
"sales": 20,
|
||||
"user": 10,
|
||||
"service": 15,
|
||||
"fleet_manager": 25,
|
||||
"driver": 5
|
||||
# Ezt az auth végpontokhoz adjuk hozzá:
|
||||
# @router.post("/login", dependencies=[Depends(RateLimiter(times=5, seconds=60))])
|
||||
|
||||
DEFAULT_RANK_MAP = {
|
||||
"superadmin": 100, "admin": 80, "fleet_manager": 25,
|
||||
"service": 15, "user": 10, "driver": 5
|
||||
}
|
||||
|
||||
def generate_secure_slug(length: int = 12) -> str:
|
||||
"""Biztonságos kód generálása (pl. mappákhoz)."""
|
||||
alphabet = string.ascii_lowercase + string.digits
|
||||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""Összehasonlítja a sima szöveges jelszót a hash-elt változattal."""
|
||||
if not hashed_password:
|
||||
return False
|
||||
if not hashed_password: return False
|
||||
try:
|
||||
return bcrypt.checkpw(
|
||||
plain_password.encode("utf-8"),
|
||||
hashed_password.encode("utf-8")
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
return bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8"))
|
||||
except Exception: return False
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
"""Létrehozza a jelszó hash-elt változatát."""
|
||||
salt = bcrypt.gensalt()
|
||||
return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8")
|
||||
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||
|
||||
def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
|
||||
"""
|
||||
Létrehozza a JWT access tokent bővített RBAC adatokkal.
|
||||
Várt kulcsok: sub (user_id), role, rank, scope_level, scope_id
|
||||
"""
|
||||
def create_tokens(data: Dict[str, Any], access_delta: Optional[timedelta] = None, refresh_delta: Optional[timedelta] = None) -> Tuple[str, str]:
|
||||
"""Access és Refresh token generálása."""
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
else:
|
||||
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
now = datetime.now(timezone.utc)
|
||||
acc_min = access_delta if access_delta else timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_payload = {**to_encode, "exp": now + acc_min, "iat": now, "type": "access", "iss": "service-finder-auth"}
|
||||
access_token = jwt.encode(access_payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
|
||||
# Rendszer szintű metaadatok hozzáadása
|
||||
to_encode.update({
|
||||
"exp": expire,
|
||||
"iat": datetime.now(timezone.utc),
|
||||
"iss": "service-finder-auth"
|
||||
})
|
||||
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
ref_days = refresh_delta if refresh_delta else timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
|
||||
refresh_payload = {"sub": str(to_encode.get("sub")), "exp": now + ref_days, "iat": now, "type": "refresh"}
|
||||
refresh_token = jwt.encode(refresh_payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return access_token, refresh_token
|
||||
|
||||
def decode_token(token: str) -> Optional[Dict[str, Any]]:
|
||||
"""JWT token visszafejtése és validálása."""
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
return payload
|
||||
except JWTError:
|
||||
return None
|
||||
try: return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
except JWTError: return None
|
||||
@@ -2,7 +2,8 @@ import os
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from app.api.v1.api import api_router # Ez már tartalmaz mindent (auth, services, stb.)
|
||||
from starlette.middleware.sessions import SessionMiddleware # ÚJ
|
||||
from app.api.v1.api import api_router
|
||||
from app.core.config import settings
|
||||
|
||||
os.makedirs("static/previews", exist_ok=True)
|
||||
@@ -14,12 +15,19 @@ app = FastAPI(
|
||||
docs_url="/docs"
|
||||
)
|
||||
|
||||
# --- SESSION MIDDLEWARE (Google Authhoz kötelező) ---
|
||||
app.add_middleware(
|
||||
SessionMiddleware,
|
||||
secret_key=settings.SECRET_KEY
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"http://192.168.100.10:3001",
|
||||
"http://localhost:3001",
|
||||
"https://dev.profibot.hu"
|
||||
"https://dev.profibot.hu",
|
||||
"https://app.profibot.hu"
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
@@ -27,8 +35,6 @@ app.add_middleware(
|
||||
)
|
||||
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
# CSAK EZT AZ EGYET KELL BEKÖTNI:
|
||||
app.include_router(api_router, prefix="/api/v1")
|
||||
|
||||
@app.get("/")
|
||||
@@ -36,5 +42,5 @@ async def root():
|
||||
return {
|
||||
"status": "online",
|
||||
"message": "Service Finder Master System v2.0",
|
||||
"features": ["Document Engine", "Asset Vault", "Org Onboarding", "Service Hunt"]
|
||||
"features": ["Google Auth Enabled", "Asset Vault", "Org Onboarding"]
|
||||
}
|
||||
@@ -1,34 +1,54 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/__init__.py
|
||||
from app.db.base_class import Base
|
||||
|
||||
from .identity import User, Person, Wallet, UserRole, VerificationToken
|
||||
# Identitás és Jogosultság
|
||||
from .identity import User, Person, Wallet, UserRole, VerificationToken, SocialAccount
|
||||
|
||||
# Szervezeti struktúra
|
||||
from .organization import Organization, OrganizationMember
|
||||
|
||||
# Járművek és Eszközök (Digital Twin)
|
||||
from .asset import (
|
||||
Asset, AssetCatalog, AssetCost, AssetEvent,
|
||||
AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate
|
||||
)
|
||||
|
||||
# Szerviz és Szakértelem (ÚJ)
|
||||
from .service import ServiceProfile, ExpertiseTag, ServiceExpertise
|
||||
|
||||
# Földrajzi adatok és Címek
|
||||
from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType
|
||||
|
||||
# Gamification és Economy
|
||||
from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, Rating, PointsLedger
|
||||
|
||||
# Rendszerkonfiguráció és Alapok
|
||||
from .system_config import SystemParameter
|
||||
from .document import Document
|
||||
from .translation import Translation # <--- HOZZÁADVA
|
||||
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
|
||||
from .history import AuditLog, VehicleOwnership
|
||||
from .security import PendingAction # <--- HOZZÁADVA
|
||||
from .translation import Translation
|
||||
|
||||
# Aliasok
|
||||
# Üzleti logika és Előfizetés
|
||||
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
|
||||
|
||||
# Naplózás és Biztonság
|
||||
from .history import AuditLog, VehicleOwnership
|
||||
from .security import PendingAction
|
||||
|
||||
# Aliasok a kényelmesebb fejlesztéshez
|
||||
Vehicle = Asset
|
||||
UserVehicle = Asset
|
||||
VehicleCatalog = AssetCatalog
|
||||
ServiceRecord = AssetEvent
|
||||
|
||||
__all__ = [
|
||||
"Base", "User", "Person", "Wallet", "UserRole", "VerificationToken",
|
||||
"Organization", "OrganizationMember", "Asset", "AssetCatalog", "AssetCost",
|
||||
"AssetEvent", "AssetFinancials", "AssetTelemetry", "AssetReview", "ExchangeRate",
|
||||
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "PointRule",
|
||||
"LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
|
||||
"SystemParameter", "Document", "Translation", "PendingAction", # <--- BŐVÍTVE
|
||||
"Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", "SocialAccount",
|
||||
"Organization", "OrganizationMember",
|
||||
"Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetFinancials",
|
||||
"AssetTelemetry", "AssetReview", "ExchangeRate",
|
||||
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", # <--- HOZZÁADVA
|
||||
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType",
|
||||
"PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
|
||||
"SystemParameter", "Document", "Translation", "PendingAction",
|
||||
"SubscriptionTier", "OrganizationSubscription",
|
||||
"CreditTransaction", "ServiceSpecialty", "AuditLog", "VehicleOwnership",
|
||||
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6,42 +6,57 @@ from sqlalchemy.sql import func
|
||||
from app.db.base_class import Base
|
||||
|
||||
class AssetCatalog(Base):
|
||||
"""Globális járműkatalógus (Márka -> Típus -> Generáció -> Motor)."""
|
||||
__tablename__ = "vehicle_catalog"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
make = Column(String, index=True, nullable=False)
|
||||
model = Column(String, index=True, nullable=False)
|
||||
generation = Column(String)
|
||||
make = Column(String, index=True, nullable=False) # 1. Szint: Audi
|
||||
model = Column(String, index=True, nullable=False) # 2. Szint: A4
|
||||
generation = Column(String, index=True) # 3. Szint: B8 (2008-2015)
|
||||
engine_variant = Column(String) # 4. Szint: 2.0 TDI (150 LE)
|
||||
|
||||
year_from = Column(Integer)
|
||||
year_to = Column(Integer)
|
||||
vehicle_class = Column(String)
|
||||
fuel_type = Column(String)
|
||||
engine_code = Column(String)
|
||||
factory_data = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
factory_data = Column(JSON, server_default=text("'{}'::jsonb")) # Technikai specifikációk
|
||||
|
||||
assets = relationship("Asset", back_populates="catalog")
|
||||
|
||||
class Asset(Base):
|
||||
"""Egyedi jármű (Asset) példány - Az ökoszisztéma magja."""
|
||||
__tablename__ = "assets"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
vin = Column(String(17), unique=True, index=True, nullable=False)
|
||||
license_plate = Column(String(20), index=True)
|
||||
name = Column(String)
|
||||
year_of_manufacture = Column(Integer)
|
||||
|
||||
# --- BIZTONSÁGI ÉS JOGOSULTSÁGI IZOLÁCIÓ ---
|
||||
# A current_organization_id biztosítja a gyors, adatbázis-szintű Scoped RBAC védelmet.
|
||||
current_organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=True)
|
||||
|
||||
catalog_id = Column(Integer, ForeignKey("data.vehicle_catalog.id"))
|
||||
is_verified = Column(Boolean, default=False)
|
||||
verification_method = Column(String(20)) # 'robot', 'ocr', 'manual'
|
||||
status = Column(String(20), default="active")
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Kapcsolatok (Digital Twin Modules)
|
||||
catalog = relationship("AssetCatalog", back_populates="assets")
|
||||
current_org = relationship("Organization")
|
||||
financials = relationship("AssetFinancials", back_populates="asset", uselist=False)
|
||||
telemetry = relationship("AssetTelemetry", back_populates="asset", uselist=False)
|
||||
assignments = relationship("AssetAssignment", back_populates="asset")
|
||||
events = relationship("AssetEvent", back_populates="asset")
|
||||
costs = relationship("AssetCost", back_populates="asset")
|
||||
reviews = relationship("AssetReview", back_populates="asset")
|
||||
ownership_history = relationship("VehicleOwnership", back_populates="vehicle")
|
||||
|
||||
class AssetFinancials(Base):
|
||||
__tablename__ = "asset_financials"
|
||||
@@ -77,9 +92,10 @@ class AssetReview(Base):
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
asset = relationship("Asset", back_populates="reviews")
|
||||
user = relationship("User") # <--- JAVÍTÁS: Hozzáadva
|
||||
user = relationship("User")
|
||||
|
||||
class AssetAssignment(Base):
|
||||
"""Jármű flotta-történetének nyilvántartása."""
|
||||
__tablename__ = "asset_assignments"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
@@ -90,7 +106,7 @@ class AssetAssignment(Base):
|
||||
status = Column(String(30), default="active")
|
||||
|
||||
asset = relationship("Asset", back_populates="assignments")
|
||||
organization = relationship("Organization") # <--- KRITIKUS JAVÍTÁS: Ez okozta a login hibát
|
||||
organization = relationship("Organization")
|
||||
|
||||
class AssetEvent(Base):
|
||||
__tablename__ = "asset_events"
|
||||
@@ -113,16 +129,13 @@ class AssetCost(Base):
|
||||
amount_local = Column(Numeric(18, 2), nullable=False)
|
||||
currency_local = Column(String(3), nullable=False)
|
||||
amount_eur = Column(Numeric(18, 2), nullable=True)
|
||||
net_amount_local = Column(Numeric(18, 2))
|
||||
vat_rate = Column(Numeric(5, 2))
|
||||
exchange_rate_used = Column(Numeric(18, 6))
|
||||
date = Column(DateTime(timezone=True), server_default=func.now())
|
||||
mileage_at_cost = Column(Integer)
|
||||
data = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
|
||||
asset = relationship("Asset", back_populates="costs")
|
||||
organization = relationship("Organization") # <--- JAVÍTÁS: Hozzáadva
|
||||
driver = relationship("User") # <--- JAVÍTÁS: Hozzáadva
|
||||
organization = relationship("Organization")
|
||||
driver = relationship("User")
|
||||
|
||||
class ExchangeRate(Base):
|
||||
__tablename__ = "exchange_rates"
|
||||
@@ -131,5 +144,4 @@ class ExchangeRate(Base):
|
||||
base_currency = Column(String(3), default="EUR")
|
||||
target_currency = Column(String(3), unique=True)
|
||||
rate = Column(Numeric(18, 6), nullable=False)
|
||||
rate_date = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
16
backend/app/models/audit.py
Normal file
16
backend/app/models/audit.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, JSON, ForeignKey, text
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base_class import Base
|
||||
|
||||
class AuditLog(Base):
|
||||
__tablename__ = "audit_logs"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="SET NULL"), nullable=True)
|
||||
action = Column(String(100), nullable=False) # pl. "LOGIN", "REGISTER", "DELETE_ASSET"
|
||||
resource_type = Column(String(50)) # pl. "User", "Asset", "Organization"
|
||||
resource_id = Column(String(100))
|
||||
details = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
ip_address = Column(String(45))
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
@@ -1,85 +1,68 @@
|
||||
import uuid
|
||||
import enum
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Enum, BigInteger
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Enum, BigInteger, UniqueConstraint
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base_class import Base
|
||||
|
||||
class UserRole(str, enum.Enum):
|
||||
superadmin = "superadmin"
|
||||
admin = "admin"
|
||||
user = "user"
|
||||
service = "service"
|
||||
fleet_manager = "fleet_manager"
|
||||
driver = "driver"
|
||||
superadmin = "superadmin"; admin = "admin"; user = "user"
|
||||
service = "service"; fleet_manager = "fleet_manager"; driver = "driver"
|
||||
|
||||
class Person(Base):
|
||||
__tablename__ = "persons"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
__tablename__ = "persons"; __table_args__ = {"schema": "data"}
|
||||
id = Column(BigInteger, primary_key=True, index=True)
|
||||
id_uuid = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
|
||||
address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True)
|
||||
|
||||
last_name = Column(String, nullable=False)
|
||||
first_name = Column(String, nullable=False)
|
||||
phone = Column(String, nullable=True)
|
||||
|
||||
last_name = Column(String, nullable=False); first_name = Column(String, nullable=False); phone = Column(String, nullable=True)
|
||||
mothers_last_name = Column(String); mothers_first_name = Column(String); birth_place = Column(String); birth_date = Column(DateTime)
|
||||
identity_docs = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
ice_contact = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
is_active = Column(Boolean, default=False, nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
users = relationship("User", back_populates="person")
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
__tablename__ = "users"; __table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String, unique=True, index=True, nullable=False)
|
||||
hashed_password = Column(String, nullable=True)
|
||||
role = Column(Enum(UserRole), default=UserRole.user)
|
||||
is_active = Column(Boolean, default=False)
|
||||
is_deleted = Column(Boolean, default=False)
|
||||
is_active = Column(Boolean, default=False); is_deleted = Column(Boolean, default=False)
|
||||
person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True)
|
||||
|
||||
# ÚJ MEZŐK HOZZÁADVA:
|
||||
preferred_language = Column(String(5), server_default="hu")
|
||||
region_code = Column(String(5), server_default="HU")
|
||||
|
||||
# RBAC & SCOPE
|
||||
scope_level = Column(String(30), server_default="individual")
|
||||
scope_id = Column(String(50))
|
||||
custom_permissions = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
|
||||
folder_slug = Column(String(12), unique=True, index=True)
|
||||
refresh_token_hash = Column(String(255), nullable=True)
|
||||
two_factor_secret = Column(String(100), nullable=True)
|
||||
two_factor_enabled = Column(Boolean, default=False)
|
||||
preferred_language = Column(String(5), server_default="hu"); region_code = Column(String(5), server_default="HU"); preferred_currency = Column(String(3), server_default="HUF")
|
||||
scope_level = Column(String(30), server_default="individual"); scope_id = Column(String(50)); custom_permissions = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
person = relationship("Person", back_populates="users"); wallet = relationship("Wallet", back_populates="user", uselist=False)
|
||||
stats = relationship("UserStats", back_populates="user", uselist=False); ownership_history = relationship("VehicleOwnership", back_populates="user")
|
||||
owned_organizations = relationship("Organization", back_populates="owner"); social_accounts = relationship("SocialAccount", back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
person = relationship("Person", back_populates="users")
|
||||
wallet = relationship("Wallet", back_populates="user", uselist=False)
|
||||
stats = relationship("UserStats", back_populates="user", uselist=False)
|
||||
ownership_history = relationship("VehicleOwnership", back_populates="user")
|
||||
owned_organizations = relationship("Organization", back_populates="owner")
|
||||
|
||||
# A Wallet és VerificationToken osztályok maradnak változatlanok...
|
||||
class Wallet(Base):
|
||||
__tablename__ = "wallets"
|
||||
__table_args__ = {"schema": "data"}
|
||||
__tablename__ = "wallets"; __table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"), unique=True)
|
||||
coin_balance = Column(Numeric(18, 2), default=0.00)
|
||||
credit_balance = Column(Numeric(18, 2), default=0.00)
|
||||
currency = Column(String(3), default="HUF")
|
||||
coin_balance = Column(Numeric(18, 2), default=0.00); credit_balance = Column(Numeric(18, 2), default=0.00); currency = Column(String(3), default="HUF")
|
||||
user = relationship("User", back_populates="wallet")
|
||||
|
||||
class VerificationToken(Base):
|
||||
__tablename__ = "verification_tokens"
|
||||
__table_args__ = {"schema": "data"}
|
||||
__tablename__ = "verification_tokens"; __table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
token = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="CASCADE"), nullable=False)
|
||||
token_type = Column(String(20), nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
expires_at = Column(DateTime(timezone=True), nullable=False)
|
||||
is_used = Column(Boolean, default=False)
|
||||
token_type = Column(String(20), nullable=False); created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
expires_at = Column(DateTime(timezone=True), nullable=False); is_used = Column(Boolean, default=False)
|
||||
|
||||
class SocialAccount(Base):
|
||||
__tablename__ = "social_accounts"
|
||||
__table_args__ = (UniqueConstraint('provider', 'social_id', name='uix_social_provider_id'), {"schema": "data"})
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="CASCADE"), nullable=False)
|
||||
provider = Column(String(50), nullable=False); social_id = Column(String(255), nullable=False, index=True); email = Column(String(255), nullable=False)
|
||||
extra_data = Column(JSON, server_default=text("'{}'::jsonb")); created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
user = relationship("User", back_populates="social_accounts")
|
||||
@@ -1,5 +1,4 @@
|
||||
import enum
|
||||
import uuid
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text
|
||||
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM
|
||||
from sqlalchemy.orm import relationship
|
||||
@@ -25,6 +24,9 @@ class Organization(Base):
|
||||
full_name = Column(String, nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
display_name = Column(String(50))
|
||||
|
||||
# --- BIZTONSÁGI BŐVÍTÉS (Mappa elszigetelés) ---
|
||||
folder_slug = Column(String(12), unique=True, index=True)
|
||||
|
||||
default_currency = Column(String(3), default="HUF")
|
||||
country_code = Column(String(2), default="HU")
|
||||
@@ -63,7 +65,7 @@ class Organization(Base):
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# String alapú hivatkozás a körkörös import ellen
|
||||
# Kapcsolatok
|
||||
assets = relationship("AssetAssignment", back_populates="organization", cascade="all, delete-orphan")
|
||||
members = relationship("OrganizationMember", back_populates="organization", cascade="all, delete-orphan")
|
||||
owner = relationship("User", back_populates="owned_organizations")
|
||||
@@ -75,8 +77,7 @@ class OrganizationMember(Base):
|
||||
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
||||
role = Column(String, default="driver")
|
||||
|
||||
permissions = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
|
||||
organization = relationship("Organization", back_populates="members")
|
||||
user = relationship("User") # Egyszerűsített string hivatkozás
|
||||
user = relationship("User")
|
||||
59
backend/app/models/service.py
Normal file
59
backend/app/models/service.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import uuid
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
||||
from geoalchemy2 import Geometry # PostGIS támogatás
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base_class import Base
|
||||
|
||||
class ServiceProfile(Base):
|
||||
"""
|
||||
Szerviz szolgáltató kiterjesztett adatai.
|
||||
Egy Organization-höz (org_type='service') kapcsolódik.
|
||||
"""
|
||||
__tablename__ = "service_profiles"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
organization_id = Column(Integer, ForeignKey("data.organizations.id"), unique=True)
|
||||
|
||||
# PostGIS GPS pont (SRID 4326 = WGS84 koordináták)
|
||||
location = Column(Geometry(geometry_type='POINT', srid=4326), index=True)
|
||||
|
||||
# Trust Engine (Bot Discovery=30, User Entry=50, Admin/Partner=100)
|
||||
trust_score = Column(Integer, default=30)
|
||||
is_verified = Column(Boolean, default=False)
|
||||
verification_log = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
|
||||
opening_hours = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
contact_phone = Column(String)
|
||||
website = Column(String)
|
||||
bio = Column(Text)
|
||||
|
||||
# Kapcsolatok
|
||||
organization = relationship("Organization")
|
||||
expertises = relationship("ServiceExpertise", back_populates="service")
|
||||
|
||||
class ExpertiseTag(Base):
|
||||
"""Szakmai szempontok taxonómiája."""
|
||||
__tablename__ = "expertise_tags"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
key = Column(String(50), unique=True, index=True) # pl. 'bmw_gs_specialist'
|
||||
name_hu = Column(String(100))
|
||||
category = Column(String(30)) # 'repair', 'fuel', 'food', 'emergency'
|
||||
|
||||
class ServiceExpertise(Base):
|
||||
"""Kapcsolótábla a szerviz és a szakterület között."""
|
||||
__tablename__ = "service_expertises"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
service_id = Column(Integer, ForeignKey("data.service_profiles.id"), primary_key=True)
|
||||
expertise_id = Column(Integer, ForeignKey("data.expertise_tags.id"), primary_key=True)
|
||||
|
||||
# Validációs szint (0-100% - Mennyire hiteles ez a szakértelem)
|
||||
validation_level = Column(Integer, default=0)
|
||||
|
||||
service = relationship("ServiceProfile", back_populates="expertises")
|
||||
expertise = relationship("ExpertiseTag")
|
||||
@@ -1,16 +1,18 @@
|
||||
from sqlalchemy import Column, String, JSON, Integer, Boolean, DateTime, func
|
||||
from sqlalchemy import Column, String, JSON, Boolean, DateTime, Integer, text
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base_class import Base
|
||||
|
||||
class SystemParameter(Base):
|
||||
"""
|
||||
Globális rendszerbeállítások (A meglévő data.system_parameters tábla alapján).
|
||||
Rendszerszintű dinamikus paraméterek tárolása.
|
||||
Szinkronban az admin.py és config.py elvárásaival.
|
||||
"""
|
||||
__tablename__ = "system_parameters"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
key = Column(String(50), unique=True, index=True, nullable=False)
|
||||
value = Column(JSON, nullable=False)
|
||||
# Az admin.py 'key' mezőt vár, nem 'key_name'-et!
|
||||
key = Column(String(50), primary_key=True, index=True)
|
||||
value = Column(JSON, server_default=text("'{}'::jsonb"), nullable=False)
|
||||
description = Column(String(255), nullable=True)
|
||||
is_active = Column(Boolean, default=True)
|
||||
description = Column(String, nullable=True)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,35 +1,99 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.models.asset import Asset, AssetTelemetry, AssetFinancials
|
||||
from sqlalchemy import select, distinct
|
||||
from app.models.asset import Asset, AssetCatalog, AssetTelemetry, AssetFinancials, AssetAssignment
|
||||
from app.models.gamification import UserStats, PointRule
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
async def create_new_vehicle(db: AsyncSession, user_id: int, vin: str, license_plate: str):
|
||||
# 1. Alap Asset létrehozása
|
||||
new_asset = Asset(
|
||||
vin=vin,
|
||||
license_plate=license_plate,
|
||||
name=f"Teszt Autó ({license_plate})"
|
||||
)
|
||||
db.add(new_asset)
|
||||
await db.flush() # Hogy legyen ID-ja
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 2. Modulok inicializálása (Digital Twin alapozás)
|
||||
db.add(AssetTelemetry(asset_id=new_asset.id, current_mileage=0))
|
||||
db.add(AssetFinancials(asset_id=new_asset.id))
|
||||
class AssetService:
|
||||
@staticmethod
|
||||
async def get_makes(db: AsyncSession):
|
||||
"""1. Szint: Márkák lekérdezése (pl. Audi, BMW)."""
|
||||
stmt = select(distinct(AssetCatalog.make)).order_by(AssetCatalog.make)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
# 3. GAMIFICATION: Pontszerzés (ASSET_REGISTER = 100 XP)
|
||||
# Megkeressük a szabályt
|
||||
rule_stmt = select(PointRule).where(PointRule.action_key == "ASSET_REGISTER")
|
||||
rule = (await db.execute(rule_stmt)).scalar_one_or_none()
|
||||
|
||||
if rule:
|
||||
# Frissítjük a felhasználó XP-jét
|
||||
stats_stmt = select(UserStats).where(UserStats.user_id == user_id)
|
||||
stats = (await db.execute(stats_stmt)).scalar_one_or_none()
|
||||
if stats:
|
||||
stats.total_xp += rule.points
|
||||
# Itt később jöhet a szintlépés ellenőrzése is!
|
||||
@staticmethod
|
||||
async def get_models(db: AsyncSession, make: str):
|
||||
"""2. Szint: Típusok szűrése márka alapján (pl. A4, A6)."""
|
||||
stmt = select(distinct(AssetCatalog.model)).where(AssetCatalog.make == make).order_by(AssetCatalog.model)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
await db.commit()
|
||||
return new_asset
|
||||
@staticmethod
|
||||
async def get_generations(db: AsyncSession, make: str, model: str):
|
||||
"""3. Szint: Generációk/Évjáratok (pl. B8 (2008-2015))."""
|
||||
stmt = select(distinct(AssetCatalog.generation)).where(
|
||||
AssetCatalog.make == make,
|
||||
AssetCatalog.model == model
|
||||
).order_by(AssetCatalog.generation)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
@staticmethod
|
||||
async def get_engines(db: AsyncSession, make: str, model: str, generation: str):
|
||||
"""4. Szint: Motorváltozatok (pl. 2.0 TDI)."""
|
||||
stmt = select(AssetCatalog).where(
|
||||
AssetCatalog.make == make,
|
||||
AssetCatalog.model == model,
|
||||
AssetCatalog.generation == generation
|
||||
).order_by(AssetCatalog.engine_variant)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
@staticmethod
|
||||
async def create_and_assign_vehicle(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
org_id: int,
|
||||
vin: str,
|
||||
license_plate: str,
|
||||
catalog_id: int = None
|
||||
):
|
||||
"""Jármű rögzítése, flottához rendelése és XP jóváírás (Atomic)."""
|
||||
try:
|
||||
# 1. Asset létrehozása közvetlen flotta-kötéssel
|
||||
new_asset = Asset(
|
||||
vin=vin,
|
||||
license_plate=license_plate,
|
||||
catalog_id=catalog_id,
|
||||
current_organization_id=org_id, # Izolációs pointer
|
||||
status="active",
|
||||
is_verified=False
|
||||
)
|
||||
db.add(new_asset)
|
||||
await db.flush()
|
||||
|
||||
# 2. Digitális Iker történetiség (Assignment)
|
||||
assignment = AssetAssignment(
|
||||
asset_id=new_asset.id,
|
||||
organization_id=org_id,
|
||||
status="active"
|
||||
)
|
||||
db.add(assignment)
|
||||
|
||||
# 3. Digitális Iker modulok indítása
|
||||
db.add(AssetTelemetry(asset_id=new_asset.id))
|
||||
db.add(AssetFinancials(asset_id=new_asset.id))
|
||||
|
||||
# 4. GAMIFICATION: XP jóváírás
|
||||
rule_stmt = select(PointRule).where(PointRule.action_key == "ASSET_REGISTER")
|
||||
rule = (await db.execute(rule_stmt)).scalar_one_or_none()
|
||||
|
||||
if rule:
|
||||
stats_stmt = select(UserStats).where(UserStats.user_id == user_id)
|
||||
stats = (await db.execute(stats_stmt)).scalar_one_or_none()
|
||||
if stats:
|
||||
stats.total_xp += rule.points
|
||||
logger.info(f"User {user_id} awarded {rule.points} XP for asset registration.")
|
||||
|
||||
# 5. Robot Scout Trigger (későbbi implementáció)
|
||||
# await RobotScout.trigger_vin_lookup(db, new_asset.id)
|
||||
|
||||
await db.commit()
|
||||
return new_asset
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Asset Creation Error: {str(e)}")
|
||||
raise e
|
||||
@@ -9,12 +9,13 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_
|
||||
from sqlalchemy.orm import joinedload
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from app.models.identity import User, Person, UserRole, VerificationToken, Wallet
|
||||
from app.models.gamification import UserStats
|
||||
from app.models.organization import Organization, OrganizationMember, OrgType
|
||||
from app.schemas.auth import UserLiteRegister, UserKYCComplete
|
||||
from app.core.security import get_password_hash, verify_password
|
||||
from app.core.security import get_password_hash, verify_password, generate_secure_slug
|
||||
from app.services.email_manager import email_manager
|
||||
from app.core.config import settings
|
||||
from app.services.config_service import config
|
||||
@@ -26,8 +27,23 @@ logger = logging.getLogger(__name__)
|
||||
class AuthService:
|
||||
@staticmethod
|
||||
async def register_lite(db: AsyncSession, user_in: UserLiteRegister):
|
||||
"""Step 1: Lite Regisztráció."""
|
||||
"""
|
||||
Step 1: Lite Regisztráció (Manuális).
|
||||
Létrehozza a Person és User rekordokat, de a fiók inaktív marad.
|
||||
A folder_slug itt még NEM generálódik le!
|
||||
"""
|
||||
try:
|
||||
# --- Dinamikus jelszóhossz ellenőrzés ---
|
||||
# Lekérjük az admin beállítást, minimum 8 karakter a hard limit.
|
||||
min_pass = await config.get_setting(db, "auth_min_password_length", default=8)
|
||||
min_len = max(int(min_pass), 8)
|
||||
|
||||
if len(user_in.password) < min_len:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"A jelszónak legalább {min_len} karakter hosszúnak kell lennie."
|
||||
)
|
||||
|
||||
new_person = Person(
|
||||
first_name=user_in.first_name,
|
||||
last_name=user_in.last_name,
|
||||
@@ -46,11 +62,13 @@ class AuthService:
|
||||
region_code=user_in.region_code,
|
||||
preferred_language=user_in.lang,
|
||||
timezone=user_in.timezone
|
||||
# folder_slug marad NULL a Step 2-ig
|
||||
)
|
||||
db.add(new_user)
|
||||
await db.flush()
|
||||
|
||||
reg_hours = await config.get_setting("auth_registration_hours", region_code=user_in.region_code, default=48)
|
||||
# Verifikációs token generálása
|
||||
reg_hours = await config.get_setting(db, "auth_registration_hours", region_code=user_in.region_code, default=48)
|
||||
token_val = uuid.uuid4()
|
||||
db.add(VerificationToken(
|
||||
token=token_val,
|
||||
@@ -59,6 +77,7 @@ class AuthService:
|
||||
expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reg_hours))
|
||||
))
|
||||
|
||||
# Email kiküldése
|
||||
verification_link = f"{settings.FRONTEND_BASE_URL}/verify?token={token_val}"
|
||||
await email_manager.send_email(
|
||||
recipient=user_in.email,
|
||||
@@ -67,9 +86,23 @@ class AuthService:
|
||||
lang=user_in.lang
|
||||
)
|
||||
|
||||
# Audit log a regisztrációról
|
||||
await security_service.log_event(
|
||||
db,
|
||||
user_id=new_user.id,
|
||||
action="USER_REGISTER_LITE",
|
||||
severity="info",
|
||||
target_type="User",
|
||||
target_id=str(new_user.id),
|
||||
new_data={"email": user_in.email, "method": "manual"}
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(new_user)
|
||||
return new_user
|
||||
except HTTPException:
|
||||
await db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Registration Error: {str(e)}")
|
||||
@@ -77,16 +110,27 @@ class AuthService:
|
||||
|
||||
@staticmethod
|
||||
async def complete_kyc(db: AsyncSession, user_id: int, kyc_in: UserKYCComplete):
|
||||
"""1.3. Fázis: Atomi Tranzakció & Shadow Identity."""
|
||||
"""
|
||||
Step 2: Atomi Tranzakció.
|
||||
Itt dől el minden: Adatok rögzítése, Shadow Identity ellenőrzés,
|
||||
Flotta és Wallet létrehozás, majd a fiók aktiválása.
|
||||
"""
|
||||
try:
|
||||
stmt = select(User).options(joinedload(User.person)).where(User.id == user_id)
|
||||
res = await db.execute(stmt)
|
||||
user = res.scalar_one_or_none()
|
||||
if not user: return None
|
||||
|
||||
# --- 1. BIZTONSÁG: User folder_slug generálása ---
|
||||
# Ha Google-lel jött vagy még nincs slugja, most kap egyet.
|
||||
if not user.folder_slug:
|
||||
user.folder_slug = generate_secure_slug(length=12)
|
||||
|
||||
# Pénznem beállítása
|
||||
if hasattr(kyc_in, 'preferred_currency') and kyc_in.preferred_currency:
|
||||
user.preferred_currency = kyc_in.preferred_currency
|
||||
|
||||
# --- 2. Shadow Identity keresése (Már létezik-e ez a fizikai személy?) ---
|
||||
identity_stmt = select(Person).where(and_(
|
||||
Person.mothers_last_name == kyc_in.mothers_last_name,
|
||||
Person.mothers_first_name == kyc_in.mothers_first_name,
|
||||
@@ -96,12 +140,15 @@ class AuthService:
|
||||
existing_person = (await db.execute(identity_stmt)).scalar_one_or_none()
|
||||
|
||||
if existing_person:
|
||||
# Ha találtunk egyezést, összekötjük a User-t a meglévő Person-nel
|
||||
user.person_id = existing_person.id
|
||||
active_person = existing_person
|
||||
logger.info(f"Shadow Identity linked: User {user_id} -> Person {existing_person.id}")
|
||||
else:
|
||||
# Ha nem, a saját (regisztrációkor létrehozott) Person-t töltjük fel
|
||||
active_person = user.person
|
||||
|
||||
# --- 3. Cím rögzítése GeoService segítségével ---
|
||||
addr_id = await GeoService.get_or_create_full_address(
|
||||
db,
|
||||
zip_code=kyc_in.address_zip,
|
||||
@@ -112,31 +159,40 @@ class AuthService:
|
||||
parcel_id=kyc_in.address_hrsz
|
||||
)
|
||||
|
||||
# --- 4. Személyes adatok frissítése ---
|
||||
active_person.mothers_last_name = kyc_in.mothers_last_name
|
||||
active_person.mothers_first_name = kyc_in.mothers_first_name
|
||||
active_person.birth_place = kyc_in.birth_place
|
||||
active_person.birth_date = kyc_in.birth_date
|
||||
active_person.phone = kyc_in.phone_number
|
||||
active_person.address_id = addr_id
|
||||
|
||||
# Dokumentumok és ICE kontakt mentése JSON-ként
|
||||
active_person.identity_docs = jsonable_encoder(kyc_in.identity_docs)
|
||||
active_person.ice_contact = jsonable_encoder(kyc_in.ice_contact)
|
||||
|
||||
# A Person most válik aktívvá
|
||||
active_person.is_active = True
|
||||
|
||||
# --- 5. EGYÉNI FLOTTA LÉTREHOZÁSA (A KYC szerves része) ---
|
||||
# Itt generáljuk a flotta mappáját is (folder_slug)
|
||||
new_org = Organization(
|
||||
full_name=f"{active_person.last_name} {active_person.first_name} Egyéni Flotta",
|
||||
name=f"{active_person.last_name} Flotta",
|
||||
folder_slug=generate_secure_slug(length=12), # FLOTTA SLUG
|
||||
org_type=OrgType.individual,
|
||||
owner_id=user.id,
|
||||
is_transferable=False,
|
||||
is_active=True,
|
||||
status="verified",
|
||||
language=user.preferred_language,
|
||||
default_currency=user.preferred_currency,
|
||||
default_currency=user.preferred_currency or "HUF",
|
||||
country_code=user.region_code
|
||||
)
|
||||
db.add(new_org)
|
||||
await db.flush()
|
||||
|
||||
# Flotta tagság (Owner)
|
||||
db.add(OrganizationMember(
|
||||
organization_id=new_org.id,
|
||||
user_id=user.id,
|
||||
@@ -144,15 +200,33 @@ class AuthService:
|
||||
permissions={"can_add_asset": True, "can_view_costs": True, "is_admin": True}
|
||||
))
|
||||
|
||||
# --- 6. PÉNZTÁRCA ÉS GAMIFICATION LÉTREHOZÁSA ---
|
||||
db.add(Wallet(
|
||||
user_id=user.id,
|
||||
coin_balance=0,
|
||||
credit_balance=0,
|
||||
currency=user.preferred_currency
|
||||
currency=user.preferred_currency or "HUF"
|
||||
))
|
||||
db.add(UserStats(user_id=user.id, total_xp=0, current_level=1))
|
||||
|
||||
# --- 7. AKTIVÁLÁS ÉS AUDIT ---
|
||||
user.is_active = True
|
||||
|
||||
await security_service.log_event(
|
||||
db,
|
||||
user_id=user.id,
|
||||
action="USER_KYC_COMPLETED",
|
||||
severity="info",
|
||||
target_type="User",
|
||||
target_id=str(user.id),
|
||||
new_data={
|
||||
"status": "active",
|
||||
"user_folder": user.folder_slug,
|
||||
"organization_id": new_org.id,
|
||||
"organization_folder": new_org.folder_slug,
|
||||
"wallet_created": True
|
||||
}
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
@@ -165,8 +239,7 @@ class AuthService:
|
||||
@staticmethod
|
||||
async def soft_delete_user(db: AsyncSession, user_id: int, reason: str, actor_id: int):
|
||||
"""
|
||||
Step 2 utáni Soft-Delete: Email felszabadítás és izoláció.
|
||||
Az email átnevezésre kerül, így az eredeti cím újra regisztrálható 'tiszta lappal'.
|
||||
Soft-Delete: Email felszabadítás és izoláció.
|
||||
"""
|
||||
stmt = select(User).where(User.id == user_id)
|
||||
user = (await db.execute(stmt)).scalar_one_or_none()
|
||||
@@ -175,12 +248,11 @@ class AuthService:
|
||||
return False
|
||||
|
||||
old_email = user.email
|
||||
# Email felszabadítása: deleted_ID_TIMESTAMP_EMAIL formátumban
|
||||
# Email átnevezése az egyediség megőrzése érdekében (újraregisztrációhoz)
|
||||
user.email = f"deleted_{user.id}_{datetime.now().strftime('%Y%m%d')}_{old_email}"
|
||||
user.is_deleted = True
|
||||
user.is_active = False
|
||||
|
||||
# Sentinel AuditLog bejegyzés
|
||||
await security_service.log_event(
|
||||
db,
|
||||
user_id=actor_id,
|
||||
@@ -231,7 +303,7 @@ class AuthService:
|
||||
user = (await db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
if user:
|
||||
reset_hours = await config.get_setting("auth_password_reset_hours", region_code=user.region_code, default=2)
|
||||
reset_hours = await config.get_setting(db, "auth_password_reset_hours", region_code=user.region_code, default=2)
|
||||
token_val = uuid.uuid4()
|
||||
db.add(VerificationToken(
|
||||
token=token_val,
|
||||
|
||||
61
backend/app/services/search_service.py
Normal file
61
backend/app/services/search_service.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from app.models.service import ServiceProfile, ExpertiseTag, ServiceExpertise
|
||||
from app.models.organization import Organization
|
||||
from geoalchemy2.functions import ST_Distance, ST_MakePoint
|
||||
|
||||
class SearchService:
|
||||
@staticmethod
|
||||
async def find_nearby_services(
|
||||
db: AsyncSession,
|
||||
lat: float,
|
||||
lon: float,
|
||||
expertise_key: str = None,
|
||||
radius_km: int = 50,
|
||||
is_premium: bool = False
|
||||
):
|
||||
"""
|
||||
Keresés távolság és szakértelem alapján.
|
||||
Premium: Trust Score + Valós távolság.
|
||||
Free: Trust Score + Légvonal.
|
||||
"""
|
||||
user_point = ST_MakePoint(lon, lat) # PostGIS pont létrehozása
|
||||
|
||||
# Alap lekérdezés: ServiceProfile + Organization adatok
|
||||
stmt = select(ServiceProfile, Organization).join(
|
||||
Organization, ServiceProfile.organization_id == Organization.id
|
||||
)
|
||||
|
||||
# 1. Sugár alapú szűrés (radius_km * 1000 méter)
|
||||
stmt = stmt.where(
|
||||
func.ST_DWithin(ServiceProfile.location, user_point, radius_km * 1000)
|
||||
)
|
||||
|
||||
# 2. Szakterület szűrése
|
||||
if expertise_key:
|
||||
stmt = stmt.join(ServiceProfile.expertises).join(ExpertiseTag).where(
|
||||
ExpertiseTag.key == expertise_key
|
||||
)
|
||||
|
||||
# 3. Távolság és Trust Score alapú sorrend
|
||||
# A ST_Distance méterben adja vissza az eredményt
|
||||
stmt = stmt.order_by(ST_Distance(ServiceProfile.location, user_point))
|
||||
|
||||
result = await db.execute(stmt.limit(50))
|
||||
rows = result.all()
|
||||
|
||||
# Rangsorolási logika alkalmazása
|
||||
results = []
|
||||
for s_prof, org in rows:
|
||||
results.append({
|
||||
"id": org.id,
|
||||
"name": org.full_name,
|
||||
"trust_score": s_prof.trust_score,
|
||||
"is_verified": s_prof.is_verified,
|
||||
"phone": s_prof.contact_phone,
|
||||
"website": s_prof.website,
|
||||
"is_premium_partner": s_prof.trust_score >= 90
|
||||
})
|
||||
|
||||
# Súlyozott rendezés: Prémium partnerek és Trust Score előre
|
||||
return sorted(results, key=lambda x: (not is_premium, -x['trust_score']))
|
||||
92
backend/app/services/social_auth_service.py
Normal file
92
backend/app/services/social_auth_service.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import uuid
|
||||
import logging
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.models.identity import User, Person, SocialAccount, UserRole
|
||||
from app.services.security_service import security_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SocialAuthService:
|
||||
@staticmethod
|
||||
async def get_or_create_social_user(
|
||||
db: AsyncSession,
|
||||
provider: str,
|
||||
social_id: str,
|
||||
email: str,
|
||||
first_name: str,
|
||||
last_name: str
|
||||
):
|
||||
"""
|
||||
Social Step 1: Csak alapregisztráció.
|
||||
Nincs slug generálás, nincs flotta. Megáll a KYC kapujában.
|
||||
"""
|
||||
# 1. Meglévő Social kapcsolat ellenőrzése
|
||||
stmt = select(SocialAccount).where(
|
||||
SocialAccount.provider == provider,
|
||||
SocialAccount.social_id == social_id
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
social_acc = result.scalar_one_or_none()
|
||||
|
||||
if social_acc:
|
||||
stmt = select(User).where(User.id == social_acc.user_id)
|
||||
user_result = await db.execute(stmt)
|
||||
return user_result.scalar_one_or_none()
|
||||
|
||||
# 2. Felhasználó keresése email alapján
|
||||
stmt = select(User).where(User.email == email)
|
||||
user_result = await db.execute(stmt)
|
||||
user = user_result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
try:
|
||||
# Person rekord létrehozása a Google-től kapott nevekkel
|
||||
new_person = Person(
|
||||
id_uuid=uuid.uuid4(),
|
||||
first_name=first_name or "Google",
|
||||
last_name=last_name or "User",
|
||||
is_active=False
|
||||
)
|
||||
db.add(new_person)
|
||||
await db.flush()
|
||||
|
||||
# User rekord (folder_slug nélkül!)
|
||||
user = User(
|
||||
email=email,
|
||||
hashed_password=None,
|
||||
person_id=new_person.id,
|
||||
role=UserRole.user,
|
||||
is_active=False,
|
||||
is_deleted=False,
|
||||
preferred_language="hu",
|
||||
region_code="HU"
|
||||
)
|
||||
db.add(user)
|
||||
await db.flush()
|
||||
|
||||
await security_service.log_event(
|
||||
db,
|
||||
user_id=user.id,
|
||||
action="USER_REGISTER_SOCIAL",
|
||||
severity="info",
|
||||
target_type="User",
|
||||
target_id=str(user.id),
|
||||
new_data={"email": email, "provider": provider}
|
||||
)
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Social Registration Error: {str(e)}")
|
||||
raise e
|
||||
|
||||
# 3. Összekötés
|
||||
new_social = SocialAccount(
|
||||
user_id=user.id,
|
||||
provider=provider,
|
||||
social_id=social_id,
|
||||
email=email
|
||||
)
|
||||
db.add(new_social)
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
return user
|
||||
35
backend/app/workers/catalog_filler.py
Normal file
35
backend/app/workers/catalog_filler.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# app/workers/catalog_filler.py
|
||||
import asyncio
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.db.session import SessionLocal
|
||||
from app.models.asset import AssetCatalog
|
||||
from sqlalchemy import select
|
||||
|
||||
class CatalogFiller:
|
||||
@staticmethod
|
||||
async def seed_initial_data():
|
||||
"""Alapértelmezett márkák és típusok feltöltése (Példa)."""
|
||||
initial_data = [
|
||||
{"make": "Audi", "model": "A4", "generation": "B8 (2008-2015)", "engine_variant": "2.0 TDI (150 LE)", "fuel_type": "Diesel"},
|
||||
{"make": "BMW", "model": "3 Series", "generation": "F30 (2012-2019)", "engine_variant": "320d (190 LE)", "fuel_type": "Diesel"},
|
||||
{"make": "Volkswagen", "model": "Passat", "generation": "B8 (2014-)", "engine_variant": "2.0 TDI (150 LE)", "fuel_type": "Diesel"}
|
||||
]
|
||||
|
||||
async with SessionLocal() as db:
|
||||
for item in initial_data:
|
||||
# Ellenőrizzük, létezik-e már
|
||||
stmt = select(AssetCatalog).where(
|
||||
AssetCatalog.make == item["make"],
|
||||
AssetCatalog.model == item["model"],
|
||||
AssetCatalog.engine_variant == item["engine_variant"]
|
||||
)
|
||||
exists = (await db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
if not exists:
|
||||
db.add(AssetCatalog(**item))
|
||||
|
||||
await db.commit()
|
||||
print("Catalog seeding complete.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(CatalogFiller.seed_initial_data())
|
||||
60
backend/app/workers/catalog_robot.py
Normal file
60
backend/app/workers/catalog_robot.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.db.session import SessionLocal
|
||||
from app.models.asset import AssetCatalog
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("Robot1-Catalog")
|
||||
|
||||
class CatalogScout:
|
||||
"""
|
||||
Robot 1: Járműkatalógus feltöltő.
|
||||
Stratégia: Magyarországi alapok -> Globális EU márkák -> Technikai mélység.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
async def get_initial_hu_data():
|
||||
"""
|
||||
Kezdeti adathalmaz (Példa).
|
||||
Élesben itt egy külső API vagy CSV feldolgozás helye van.
|
||||
"""
|
||||
return [
|
||||
# Suzuki - A magyar utak királya
|
||||
{"make": "Suzuki", "model": "Swift", "generation": "III (2005-2010)", "engine_variant": "1.3 (92 LE)", "year_from": 2005, "year_to": 2010, "fuel_type": "petrol"},
|
||||
{"make": "Suzuki", "model": "Vitara", "generation": "IV (2015-)", "engine_variant": "1.6 VVT (120 LE)", "year_from": 2015, "year_to": 2024, "fuel_type": "petrol"},
|
||||
# Opel - Astra népautó
|
||||
{"make": "Opel", "model": "Astra", "generation": "H (2004-2009)", "engine_variant": "1.4 Twinport (90 LE)", "year_from": 2004, "year_to": 2009, "fuel_type": "petrol"},
|
||||
{"make": "Opel", "model": "Astra", "generation": "J (2009-2015)", "engine_variant": "1.7 CDTI (110 LE)", "year_from": 2009, "year_to": 2015, "fuel_type": "diesel"},
|
||||
# Skoda - Családi/Flotta kedvenc
|
||||
{"make": "Skoda", "model": "Octavia", "generation": "II (2004-2013)", "engine_variant": "1.6 MPI (102 LE)", "year_from": 2004, "year_to": 2013, "fuel_type": "petrol"},
|
||||
{"make": "Skoda", "model": "Octavia", "generation": "III (2013-2020)", "engine_variant": "2.0 TDI (150 LE)", "year_from": 2013, "year_to": 2020, "fuel_type": "diesel"},
|
||||
# BMW - GS Motorosoknak
|
||||
{"make": "BMW", "model": "R 1200 GS", "generation": "K50 (2013-2018)", "engine_variant": "Adventure (125 LE)", "year_from": 2013, "year_to": 2018, "fuel_type": "petrol"}
|
||||
]
|
||||
|
||||
@classmethod
|
||||
async def run(cls):
|
||||
logger.info("🤖 Robot 1 indítása: Járműkatalógus feltöltés...")
|
||||
async with SessionLocal() as db:
|
||||
data = await cls.get_initial_hu_data()
|
||||
added_count = 0
|
||||
|
||||
for item in data:
|
||||
# Ellenőrizzük az egyediséget (Make + Model + Generation + Engine)
|
||||
stmt = select(AssetCatalog).where(
|
||||
AssetCatalog.make == item["make"],
|
||||
AssetCatalog.model == item["model"],
|
||||
AssetCatalog.engine_variant == item["engine_variant"]
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
if not result.scalar_one_or_none():
|
||||
db.add(AssetCatalog(**item))
|
||||
added_count += 1
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"✅ Robot 1 sikeresen rögzített {added_count} új katalógus elemet.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(CatalogScout.run())
|
||||
Reference in New Issue
Block a user