feat: complete Tier 2 onboarding - KYC, Private Fleet, and Wallet creation fully functional

This commit is contained in:
2026-02-06 23:43:01 +00:00
parent 9d06be4f87
commit 8020bbd394
9 changed files with 114 additions and 58 deletions

BIN
backend/app/api/__pycache__/deps.cpython-312.pyc Executable file → Normal file

Binary file not shown.

View File

@@ -1,39 +1,51 @@
from typing import Generator from typing import AsyncGenerator
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
from jose import JWTError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from app.db.session import SessionLocal from app.db.session import get_db
from app.core.security import decode_token from app.core.security import decode_token
from app.models.user import User from app.models.identity import User # Javítva identity-re
reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v2/auth/login") # Javítva v1-re
reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def get_db() -> Generator:
async with SessionLocal() as session:
yield session
async def get_current_user( async def get_current_user(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
token: str = Depends(reusable_oauth2), token: str = Depends(reusable_oauth2),
) -> User: ) -> User:
try: payload = decode_token(token)
payload = decode_token(token) if not payload:
user_id = payload.get("sub") raise HTTPException(
if not user_id: status_code=status.HTTP_401_UNAUTHORIZED,
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token error") detail="Érvénytelen vagy lejárt token."
except JWTError: )
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
user_id = payload.get("sub")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token azonosítási hiba."
)
# Felhasználó keresése az adatbázisban
res = await db.execute(select(User).where(User.id == int(user_id))) res = await db.execute(select(User).where(User.id == int(user_id)))
user = res.scalars().first() user = res.scalar_one_or_none()
if not user: if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Felhasználó nem található."
)
if not user.is_active: if user.is_deleted:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Fiók nem aktív.") raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Ez a fiók törölve lett."
)
return user # FONTOS: Itt NEM dobunk hibát, ha user.is_active == False,
# mert a Step 2 (KYC) kitöltéséhez be kell tudnia lépni inaktívként is!
return user

View File

@@ -2,16 +2,19 @@ from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text from sqlalchemy import text
from app.db.session import get_db from app.db.session import get_db
from app.services.auth_service import AuthService from app.services.auth_service import AuthService
from app.core.security import create_access_token from app.core.security import create_access_token
from app.schemas.auth import UserLiteRegister, Token, PasswordResetRequest from app.schemas.auth import UserLiteRegister, Token, PasswordResetRequest, UserKYCComplete
from app.api.deps import get_current_user # Ez kezeli a belépett felhasználót
from app.models.identity import User
router = APIRouter() router = APIRouter()
@router.post("/register-lite", response_model=Token, status_code=201) @router.post("/register-lite", response_model=Token, status_code=201)
async def register_lite(user_in: UserLiteRegister, db: AsyncSession = Depends(get_db)): async def register_lite(user_in: UserLiteRegister, db: AsyncSession = Depends(get_db)):
# Email csekkolás nyers SQL-el """Step 1: Alapszintű regisztráció és aktiváló e-mail küldése."""
check = await db.execute(text("SELECT id FROM data.users WHERE email = :e"), {"e": user_in.email}) check = await db.execute(text("SELECT id FROM data.users WHERE email = :e"), {"e": user_in.email})
if check.fetchone(): if check.fetchone():
raise HTTPException(status_code=400, detail="Ez az email cím már foglalt.") raise HTTPException(status_code=400, detail="Ez az email cím már foglalt.")
@@ -25,6 +28,7 @@ async def register_lite(user_in: UserLiteRegister, db: AsyncSession = Depends(ge
@router.post("/login", response_model=Token) @router.post("/login", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_db)): async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_db)):
"""Bejelentkezés az access_token megszerzéséhez."""
user = await AuthService.authenticate(db, form_data.username, form_data.password) user = await AuthService.authenticate(db, form_data.username, form_data.password)
if not user: if not user:
raise HTTPException(status_code=401, detail="Hibás e-mail vagy jelszó.") raise HTTPException(status_code=401, detail="Hibás e-mail vagy jelszó.")
@@ -32,15 +36,28 @@ async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: AsyncSessi
token = create_access_token(data={"sub": str(user.id)}) token = create_access_token(data={"sub": str(user.id)})
return {"access_token": token, "token_type": "bearer", "is_active": user.is_active} return {"access_token": token, "token_type": "bearer", "is_active": user.is_active}
@router.post("/forgot-password")
async def forgot_password(req: PasswordResetRequest, db: AsyncSession = Depends(get_db)):
await AuthService.initiate_password_reset(db, req.email)
return {"message": "Helyreállítási folyamat elindítva."}
@router.get("/verify-email") @router.get("/verify-email")
async def verify_email(token: str, db: AsyncSession = Depends(get_db)): async def verify_email(token: str, db: AsyncSession = Depends(get_db)):
"""Ezt hívja meg a frontend, amikor a user a levélben a gombra kattint.""" """E-mail megerősítése a kiküldött token alapján."""
success = await AuthService.verify_email(db, token) success = await AuthService.verify_email(db, token)
if not success: if not success:
raise HTTPException(status_code=400, detail="Érvénytelen vagy lejárt token.") raise HTTPException(status_code=400, detail="Érvénytelen vagy lejárt token.")
return {"message": "Email sikeresen megerősítve! Most már elvégezheti a KYC regisztrációt (Step 2)."} return {"message": "Email sikeresen megerősítve! het a Step 2 (KYC)."}
@router.post("/complete-kyc")
async def complete_kyc(
kyc_in: UserKYCComplete,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Step 2: Okmányok rögzítése, Privát Széf és Wallet aktiválása."""
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": "Gratulálunk! A Privát Széf és a Pénztárca aktiválva lett."}
@router.post("/forgot-password")
async def forgot_password(req: PasswordResetRequest, db: AsyncSession = Depends(get_db)):
"""Jelszó-visszaállító link küldése."""
await AuthService.initiate_password_reset(db, req.email)
return {"message": "Ha a cím létezik, elküldtük a helyreállítási linket."}

View File

@@ -1,8 +1,7 @@
# /opt/docker/dev/service_finder/backend/app/core/security.py
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
import bcrypt import bcrypt
from jose import jwt from jose import jwt, JWTError
from app.core.config import settings from app.core.config import settings
def verify_password(plain_password: str, hashed_password: str) -> bool: def verify_password(plain_password: str, hashed_password: str) -> bool:
@@ -17,4 +16,12 @@ def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta]
to_encode = data.copy() to_encode = data.copy()
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)) expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({"exp": expire}) to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
def decode_token(token: str) -> Optional[Dict[str, Any]]:
"""JWT token visszafejtése és ellenőrzése."""
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
return payload
except JWTError:
return None

BIN
backend/app/models/__pycache__/user.cpython-312.pyc Executable file → Normal file

Binary file not shown.

View File

@@ -1,13 +1,15 @@
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import uuid import uuid
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, text from sqlalchemy import select, text, cast, String
from app.models.identity import User, Person, UserRole, VerificationToken, Wallet from app.models.identity import User, Person, UserRole, VerificationToken, Wallet
from app.models.organization import Organization from app.models.organization import Organization
from app.schemas.auth import UserLiteRegister, UserKYCComplete 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
from app.services.email_manager import email_manager from app.services.email_manager import email_manager
from app.core.config import settings from app.core.config import settings
from sqlalchemy.orm import joinedload # <--- EZT ADD HOZZÁ AZ IMPORTOKHOZ!
class AuthService: class AuthService:
@staticmethod @staticmethod
@@ -94,47 +96,64 @@ class AuthService:
@staticmethod @staticmethod
async def complete_kyc(db: AsyncSession, user_id: int, kyc_in: UserKYCComplete): async def complete_kyc(db: AsyncSession, user_id: int, kyc_in: UserKYCComplete):
"""Step 2: KYC adatok, Telefon, Privát Flotta és Wallet aktiválása.""" """Step 2: KYC adatok rögzítése JSON-biztos dátumkezeléssel."""
try: try:
# 1. User és Person lekérése # 1. User és Person lekérése joinedload-dal (a korábbi hiba javítása)
stmt = select(User).where(User.id == user_id).join(User.person) stmt = (
select(User)
.options(joinedload(User.person))
.where(User.id == user_id)
)
result = await db.execute(stmt) result = await db.execute(stmt)
user = result.scalar_one_or_none() user = result.scalar_one_or_none()
if not user: if not user or not user.person:
return None return None
# 2. Személyes adatok rögzítése (tábla szinten) # 2. Előkészítjük a JSON-kompatibilis adatokat
# A mode='json' átalakítja a date objektumokat string-gé!
kyc_data_json = kyc_in.model_dump(mode='json')
p = user.person p = user.person
p.phone = kyc_in.phone_number p.phone = kyc_in.phone_number
p.birth_place = kyc_in.birth_place p.birth_place = kyc_in.birth_place
# A sima DATE oszlopba mehet a Python date objektum
p.birth_date = datetime.combine(kyc_in.birth_date, datetime.min.time()) p.birth_date = datetime.combine(kyc_in.birth_date, datetime.min.time())
p.mothers_name = kyc_in.mothers_name p.mothers_name = kyc_in.mothers_name
# JSONB mezők mentése Pydantic modellekből # A JSONB mezőkbe a már stringesített adatokat tesszük
p.identity_docs = {k: v.dict() for k, v in kyc_in.identity_docs.items()} p.identity_docs = kyc_data_json["identity_docs"]
p.ice_contact = kyc_in.ice_contact.dict() p.ice_contact = kyc_data_json["ice_contact"]
p.is_active = True p.is_active = True
# 3. PRIVÁT FLOTTA (Organization) automata generálása # 3. PRIVÁT FLOTTA (Organization)
new_org = Organization( # Megnézzük, létezik-e már (idempotencia)
name=f"{p.last_name} {p.first_name} - Privát Flotta", org_stmt = select(Organization).where(
owner_id=user.id, Organization.owner_id == user.id,
is_active=True, cast(Organization.org_type, String) == "individual"
org_type="individual"
) )
db.add(new_org) org_res = await db.execute(org_stmt)
await db.flush() existing_org = org_res.scalar_one_or_none()
if not existing_org:
new_org = Organization(
name=f"{p.last_name} {p.first_name} - Privát Flotta",
owner_id=user.id,
is_active=True,
org_type="individual",
is_verified=True,
is_transferable=True
)
db.add(new_org)
# 4. WALLET automata generálása # 4. WALLET
new_wallet = Wallet( wallet_stmt = select(Wallet).where(Wallet.user_id == user.id)
user_id=user.id, wallet_res = await db.execute(wallet_stmt)
coin_balance=0.00, if not wallet_res.scalar_one_or_none():
xp_balance=0 new_wallet = Wallet(user_id=user.id, coin_balance=0.0, xp_balance=0)
) db.add(new_wallet)
db.add(new_wallet)
# 5. USER TELJES AKTIVÁLÁSA # 5. USER AKTIVÁLÁSA
user.is_active = True user.is_active = True
await db.commit() await db.commit()
@@ -142,6 +161,7 @@ class AuthService:
return user return user
except Exception as e: except Exception as e:
await db.rollback() await db.rollback()
print(f"CRITICAL KYC ERROR: {str(e)}")
raise e raise e
@staticmethod @staticmethod