feat: Asset Catalog system, PostGIS integration and RobotScout V1
This commit is contained in:
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
|
||||
]
|
||||
Reference in New Issue
Block a user