feat: complete auth life-cycle with Step 2 KYC and Private Fleet generation
This commit is contained in:
Binary file not shown.
@@ -1,7 +1,8 @@
|
|||||||
from pydantic import BaseModel, EmailStr, Field
|
from pydantic import BaseModel, EmailStr, Field
|
||||||
from typing import Optional
|
from typing import Optional, Dict
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
|
# --- STEP 1: LITE REGISTRATION ---
|
||||||
class UserLiteRegister(BaseModel):
|
class UserLiteRegister(BaseModel):
|
||||||
email: EmailStr
|
email: EmailStr
|
||||||
password: str = Field(..., min_length=8)
|
password: str = Field(..., min_length=8)
|
||||||
@@ -13,6 +14,26 @@ class UserLogin(BaseModel):
|
|||||||
email: EmailStr
|
email: EmailStr
|
||||||
password: str
|
password: str
|
||||||
|
|
||||||
|
# --- STEP 2: KYC & ONBOARDING ---
|
||||||
|
class ICEContact(BaseModel):
|
||||||
|
name: str
|
||||||
|
phone: str
|
||||||
|
relationship: Optional[str] = None
|
||||||
|
|
||||||
|
class DocumentDetail(BaseModel):
|
||||||
|
number: str
|
||||||
|
expiry_date: date
|
||||||
|
|
||||||
|
class UserKYCComplete(BaseModel):
|
||||||
|
phone_number: str
|
||||||
|
birth_place: str
|
||||||
|
birth_date: date
|
||||||
|
mothers_name: str
|
||||||
|
# Rugalmas okmánytár, pl: {"id_card": {"number": "123", "expiry_date": "2030-01-01"}}
|
||||||
|
identity_docs: Dict[str, DocumentDetail]
|
||||||
|
ice_contact: ICEContact
|
||||||
|
|
||||||
|
# --- COMMON & SECURITY ---
|
||||||
class PasswordResetRequest(BaseModel):
|
class PasswordResetRequest(BaseModel):
|
||||||
email: EmailStr
|
email: EmailStr
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -2,9 +2,9 @@ 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
|
||||||
from app.models.identity import User, Person, UserRole, VerificationToken
|
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
|
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
|
||||||
@@ -12,9 +12,9 @@ from app.core.config import settings
|
|||||||
class AuthService:
|
class AuthService:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def register_lite(db: AsyncSession, user_in: UserLiteRegister):
|
async def register_lite(db: AsyncSession, user_in: UserLiteRegister):
|
||||||
"""Step 1: Lite regisztráció kormányozható token élettartammal."""
|
"""Step 1: Lite regisztráció + Token generálás + Email."""
|
||||||
try:
|
try:
|
||||||
# 1. Person shell
|
# 1. Person alap létrehozása
|
||||||
new_person = Person(
|
new_person = Person(
|
||||||
first_name=user_in.first_name,
|
first_name=user_in.first_name,
|
||||||
last_name=user_in.last_name,
|
last_name=user_in.last_name,
|
||||||
@@ -23,7 +23,7 @@ class AuthService:
|
|||||||
db.add(new_person)
|
db.add(new_person)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
|
|
||||||
# 2. User fiók
|
# 2. User technikai fiók
|
||||||
new_user = User(
|
new_user = User(
|
||||||
email=user_in.email,
|
email=user_in.email,
|
||||||
hashed_password=get_password_hash(user_in.password),
|
hashed_password=get_password_hash(user_in.password),
|
||||||
@@ -35,10 +35,8 @@ class AuthService:
|
|||||||
db.add(new_user)
|
db.add(new_user)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
|
|
||||||
# 3. Biztonsági Token (Beállítható élettartam)
|
# 3. Kormányozható Token generálása
|
||||||
# Default: 48 óra, ha nincs megadva a settingsben
|
|
||||||
expire_hours = getattr(settings, "REGISTRATION_TOKEN_EXPIRE_HOURS", 48)
|
expire_hours = getattr(settings, "REGISTRATION_TOKEN_EXPIRE_HOURS", 48)
|
||||||
|
|
||||||
token_val = uuid.uuid4()
|
token_val = uuid.uuid4()
|
||||||
new_token = VerificationToken(
|
new_token = VerificationToken(
|
||||||
token=token_val,
|
token=token_val,
|
||||||
@@ -49,9 +47,8 @@ class AuthService:
|
|||||||
db.add(new_token)
|
db.add(new_token)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
|
|
||||||
# 4. Email küldés
|
# 4. Email küldés gombbal
|
||||||
verification_link = f"{settings.FRONTEND_BASE_URL}/verify?token={token_val}"
|
verification_link = f"{settings.FRONTEND_BASE_URL}/verify?token={token_val}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await email_manager.send_email(
|
await email_manager.send_email(
|
||||||
recipient=user_in.email,
|
recipient=user_in.email,
|
||||||
@@ -62,7 +59,7 @@ class AuthService:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
except Exception as email_err:
|
except Exception as email_err:
|
||||||
print(f"CRITICAL: Email sending failed: {str(email_err)}")
|
print(f"CRITICAL: Email failed: {str(email_err)}")
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(new_user)
|
await db.refresh(new_user)
|
||||||
@@ -73,11 +70,9 @@ class AuthService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def verify_email(db: AsyncSession, token_str: str):
|
async def verify_email(db: AsyncSession, token_str: str):
|
||||||
"""Token ellenőrzése és regisztráció megerősítése."""
|
"""Token ellenőrzése (Email megerősítés)."""
|
||||||
try:
|
try:
|
||||||
# Token UUID-vá alakítása az összehasonlításhoz
|
|
||||||
token_uuid = uuid.UUID(token_str)
|
token_uuid = uuid.UUID(token_str)
|
||||||
|
|
||||||
stmt = select(VerificationToken).where(
|
stmt = select(VerificationToken).where(
|
||||||
VerificationToken.token == token_uuid,
|
VerificationToken.token == token_uuid,
|
||||||
VerificationToken.is_used == False,
|
VerificationToken.is_used == False,
|
||||||
@@ -89,18 +84,7 @@ class AuthService:
|
|||||||
if not token_obj:
|
if not token_obj:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Token elhasználása
|
|
||||||
token_obj.is_used = True
|
token_obj.is_used = True
|
||||||
|
|
||||||
# User keresése és aktiválása (Email megerősítve)
|
|
||||||
user_stmt = select(User).where(User.id == token_obj.user_id)
|
|
||||||
user_res = await db.execute(user_stmt)
|
|
||||||
user = user_res.scalar_one_or_none()
|
|
||||||
if user:
|
|
||||||
# Figyelem: A Master Book szerint ez még nem teljes aktiválás (is_active: false)
|
|
||||||
# de jelölhetjük, hogy az e-mail már OK.
|
|
||||||
pass
|
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -108,19 +92,70 @@ class AuthService:
|
|||||||
await db.rollback()
|
await db.rollback()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@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."""
|
||||||
|
try:
|
||||||
|
# 1. User és Person lekérése
|
||||||
|
stmt = select(User).where(User.id == user_id).join(User.person)
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 2. Személyes adatok rögzítése (tábla szinten)
|
||||||
|
p = user.person
|
||||||
|
p.phone = kyc_in.phone_number
|
||||||
|
p.birth_place = kyc_in.birth_place
|
||||||
|
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()
|
||||||
|
p.is_active = True
|
||||||
|
|
||||||
|
# 3. PRIVÁT FLOTTA (Organization) automata generálása
|
||||||
|
new_org = Organization(
|
||||||
|
name=f"{p.last_name} {p.first_name} - Privát Flotta",
|
||||||
|
owner_id=user.id,
|
||||||
|
is_active=True,
|
||||||
|
org_type="individual"
|
||||||
|
)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
db.add(new_wallet)
|
||||||
|
|
||||||
|
# 5. USER TELJES AKTIVÁLÁSA
|
||||||
|
user.is_active = True
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(user)
|
||||||
|
return user
|
||||||
|
except Exception as e:
|
||||||
|
await db.rollback()
|
||||||
|
raise e
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def authenticate(db: AsyncSession, email: str, password: str):
|
async def authenticate(db: AsyncSession, email: str, password: str):
|
||||||
stmt = select(User).where(User.email == email, User.is_deleted == False)
|
stmt = select(User).where(User.email == email, User.is_deleted == False)
|
||||||
res = await db.execute(stmt)
|
res = await db.execute(stmt)
|
||||||
user = res.scalar_one_or_none()
|
user = res.scalar_one_or_none()
|
||||||
|
|
||||||
if not user or not user.hashed_password or not verify_password(password, user.hashed_password):
|
if not user or not user.hashed_password or not verify_password(password, user.hashed_password):
|
||||||
return None
|
return None
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def initiate_password_reset(db: AsyncSession, email: str):
|
async def initiate_password_reset(db: AsyncSession, email: str):
|
||||||
"""Jelszó-emlékeztető kormányozható élettartammal."""
|
"""Jelszó-visszaállítás indítása."""
|
||||||
stmt = select(User).where(User.email == email, User.is_deleted == False)
|
stmt = select(User).where(User.email == email, User.is_deleted == False)
|
||||||
res = await db.execute(stmt)
|
res = await db.execute(stmt)
|
||||||
user = res.scalar_one_or_none()
|
user = res.scalar_one_or_none()
|
||||||
@@ -137,7 +172,6 @@ class AuthService:
|
|||||||
db.add(new_token)
|
db.add(new_token)
|
||||||
|
|
||||||
reset_link = f"{settings.FRONTEND_BASE_URL}/reset-password?token={token_val}"
|
reset_link = f"{settings.FRONTEND_BASE_URL}/reset-password?token={token_val}"
|
||||||
|
|
||||||
await email_manager.send_email(
|
await email_manager.send_email(
|
||||||
recipient=email,
|
recipient=email,
|
||||||
template_key="password_reset",
|
template_key="password_reset",
|
||||||
|
|||||||
@@ -78,3 +78,26 @@ Minden regisztrációnál létrejön:
|
|||||||
...
|
...
|
||||||
- **Hibrid Validálás:** Ha a VIES API nem ad eredményt, a rendszer céges dokumentum feltöltését kéri.
|
- **Hibrid Validálás:** Ha a VIES API nem ad eredményt, a rendszer céges dokumentum feltöltését kéri.
|
||||||
- **Ellenőrzés:** Adminisztrátori jóváhagyás vagy AI-alapú dokumentum-validálás után válik `Verified` státuszúvá.
|
- **Ellenőrzés:** Adminisztrátori jóváhagyás vagy AI-alapú dokumentum-validálás után válik `Verified` státuszúvá.
|
||||||
|
|
||||||
|
# 🆔 Identitás Validációs és Bizalmi Protokoll
|
||||||
|
|
||||||
|
A rendszer a fokozatos adatszolgáltatás és a "Tier-based Access Control" (szintezett hozzáférés) elvét alkalmazza.
|
||||||
|
|
||||||
|
## 1. Bizalmi Szintek (Trust Tiers)
|
||||||
|
|
||||||
|
| Szint | Megnevezés | Követelmény | Jogosultságok |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **Tier 0** | Anonymous | Nincs | Csak publikus adatok megtekintése. |
|
||||||
|
| **Tier 1** | Verified Email | Step 1 sikeres | Belépés, saját profil megtekintése. |
|
||||||
|
| **Tier 2** | KYC Submitted | Step 2 (Személyi adatok + Telefon) | **Privát Széf/Flotta aktiválása**, Wallet használat. |
|
||||||
|
| **Tier 3** | AI/OCR Verified | Okmánykép AI általi ellenőrzése | Harmadik fél szolgáltatásainak igénybevétele. |
|
||||||
|
|
||||||
|
## 2. Kötelező Adatkör (Step 2 - Tier 2)
|
||||||
|
A "Privát Széf" aktiválásához az alábbi adatok megadása kötelező:
|
||||||
|
- **Kapcsolat:** Valós telefonszám (nemzetközi formátum).
|
||||||
|
- **Személyi:** Születési hely, idő, anyja neve.
|
||||||
|
- **Okmány:** Típus, sorszám és **lejárati dátum**.
|
||||||
|
- **Biztonság:** ICE (In Case of Emergency) név és telefonszám.
|
||||||
|
|
||||||
|
## 3. Adattárolási Stratégia
|
||||||
|
- A rugalmas okmányadatokat és vészhelyzeti kapcsolatokat a `persons.identity_docs` és `persons.ice_contact` JSONB mezőkben tároljuk a kereshetőség és bővíthetőség érdekében.
|
||||||
Reference in New Issue
Block a user