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.security import OAuth2PasswordBearer
from jose import JWTError
from sqlalchemy.ext.asyncio import AsyncSession
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.models.user import User
from app.models.identity import User # Javítva identity-re
reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v2/auth/login")
async def get_db() -> Generator:
async with SessionLocal() as session:
yield session
# Javítva v1-re
reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def get_current_user(
db: AsyncSession = Depends(get_db),
token: str = Depends(reusable_oauth2),
) -> User:
try:
payload = decode_token(token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Érvénytelen vagy lejárt token."
)
user_id = payload.get("sub")
if not user_id:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token error")
except JWTError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
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)))
user = res.scalars().first()
user = res.scalar_one_or_none()
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:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Fiók nem aktív.")
if user.is_deleted:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Ez a fiók törölve lett."
)
# 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 sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
from app.db.session import get_db
from app.services.auth_service import AuthService
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.post("/register-lite", response_model=Token, status_code=201)
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})
if check.fetchone():
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)
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)
if not user:
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)})
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")
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)
if not success:
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 typing import Optional, Dict, Any
import bcrypt
from jose import jwt
from jose import jwt, JWTError
from app.core.config import settings
def verify_password(plain_password: str, hashed_password: str) -> bool:
@@ -18,3 +17,11 @@ def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta]
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({"exp": expire})
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
import uuid
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.organization import Organization
from app.schemas.auth import UserLiteRegister, UserKYCComplete
from app.core.security import get_password_hash, verify_password
from app.services.email_manager import email_manager
from app.core.config import settings
from sqlalchemy.orm import joinedload # <--- EZT ADD HOZZÁ AZ IMPORTOKHOZ!
class AuthService:
@staticmethod
@@ -94,47 +96,64 @@ class AuthService:
@staticmethod
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:
# 1. User és Person lekérése
stmt = select(User).where(User.id == user_id).join(User.person)
# 1. User és Person lekérése joinedload-dal (a korábbi hiba javítása)
stmt = (
select(User)
.options(joinedload(User.person))
.where(User.id == user_id)
)
result = await db.execute(stmt)
user = result.scalar_one_or_none()
if not user:
if not user or not user.person:
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.phone = kyc_in.phone_number
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.mothers_name = kyc_in.mothers_name
# JSONB mezők mentése Pydantic modellekből
p.identity_docs = {k: v.dict() for k, v in kyc_in.identity_docs.items()}
p.ice_contact = kyc_in.ice_contact.dict()
# A JSONB mezőkbe a már stringesített adatokat tesszük
p.identity_docs = kyc_data_json["identity_docs"]
p.ice_contact = kyc_data_json["ice_contact"]
p.is_active = True
# 3. PRIVÁT FLOTTA (Organization) automata generálása
# 3. PRIVÁT FLOTTA (Organization)
# Megnézzük, létezik-e már (idempotencia)
org_stmt = select(Organization).where(
Organization.owner_id == user.id,
cast(Organization.org_type, String) == "individual"
)
org_res = await db.execute(org_stmt)
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"
org_type="individual",
is_verified=True,
is_transferable=True
)
db.add(new_org)
await db.flush()
# 4. WALLET automata generálása
new_wallet = Wallet(
user_id=user.id,
coin_balance=0.00,
xp_balance=0
)
# 4. WALLET
wallet_stmt = select(Wallet).where(Wallet.user_id == user.id)
wallet_res = await db.execute(wallet_stmt)
if not wallet_res.scalar_one_or_none():
new_wallet = Wallet(user_id=user.id, coin_balance=0.0, xp_balance=0)
db.add(new_wallet)
# 5. USER TELJES AKTIVÁLÁSA
# 5. USER AKTIVÁLÁSA
user.is_active = True
await db.commit()
@@ -142,6 +161,7 @@ class AuthService:
return user
except Exception as e:
await db.rollback()
print(f"CRITICAL KYC ERROR: {str(e)}")
raise e
@staticmethod