From 9d06be4f8728b84c85731f39510596a99fc0a495 Mon Sep 17 00:00:00 2001 From: Kincses Date: Fri, 6 Feb 2026 22:38:44 +0000 Subject: [PATCH] feat: complete auth life-cycle with Step 2 KYC and Private Fleet generation --- .../schemas/__pycache__/auth.cpython-312.pyc | Bin 1360 -> 2260 bytes backend/app/schemas/auth.py | 23 ++++- .../__pycache__/auth_service.cpython-312.pyc | Bin 7654 -> 9844 bytes backend/app/services/auth_service.py | 90 ++++++++++++------ docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md | 25 ++++- 5 files changed, 108 insertions(+), 30 deletions(-) diff --git a/backend/app/schemas/__pycache__/auth.cpython-312.pyc b/backend/app/schemas/__pycache__/auth.cpython-312.pyc index ff49eba2b9f46100c74d1bba76e5f54f5eedef94..04b8240429b303ffd372ace2427762a966dae0a5 100644 GIT binary patch literal 2260 zcma)7-ESL35a0W<&*!fscD_i3lt#2Y5gIBaq)HVJB!z~w4hoG#{lMvRw~bHShr4$M z{1zzz_0c>ve*&odCrF4Yc`z%XBJtEWq+Er>6Ek~uVk{Rp>1JntJ6_MsZ)Wbta@ip8 z{ONz=-B1bn9XG>A8z^T#f%1g7#Fcy!NKGk_n{uEu6$vrfR|Bo71-WKU>=i#B=uKTB zGWn3W>Jf1@Dm_)QyP5@o<^au)poTzoKno*iQJ@B(#Sye5&=R2K5wtAO3ZT^yv?9f$jO9B#dhxc+_`Ind{r_#EU}P=jw*%B&Nhw z*G{ODMUzQLn|RJXh2;sMq$#n??s^H`q5EE(P;ir?!`ENvU;?fgffrgn4fm54*V}d+e;F~C z8xK7eCst?&loxz^sLFSCd6B_QQD`|3e#0zq-4bS&#SO~}BDdpH&`Xx}sAKzhfn_m_ zmRBvy4#Oz1F*z}ySSqZ7M@pP9HUrxCFmtSin>vbF6c{+0LottH0flhkHPkK* zMH!U11Y!_NzcPJvFEd1&$-ce|C$I1oYe@L&Dou^%sDl3qm^$!Z3Y$xHW zpjm6TqL4OtiBUhJsO7cI{CGqn4iV8Z+{uuI@vvS;3t3N$^s?KK48b-b6v7}|SecPej$TpY(UJ9b5MOmD+cG+_w zV_pL3iJX`hJ%>VHi>iRuel_(XS9$PRBu8*Pp1P}eied@g_-zo7x|!J%b+$iUKT+%b zDIo>&TW$VWIsWYDg*TtoRmLt>*$$0qvO^zrC`=h_ z)`qE#hj!7YV=cM{_6)@v@WvRjXwjqXjC(wp>>W;~$=(T`MR#6s-&IYGYxkqiX~;{q zIUm`atPNvAqlP&J zUxTh|Lv=Vz0VEq`II?*Tobkf_toJb`T*ulKW;&Z8<2YW9iu%vZKsvqA>2`p(&k?M~(UxIyIF!?tC``r@& delta 598 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 HP#gI_ab?_k delta 2148 zcmZuxYfM~46rS0=`?$-#cUg7|TW(pfZYf)6YguSTX`!%)k5s7X8YMT}0k+*ooqGun zsc3Bs@$myEu^MY?6MvLcqX|E3qCd1-5~F`0Gyyj@YC_aRf2`O<8l!Q}UAI2s&F*); zbIx~X=FT}YxB9<3=={Rrup$1uL|#a_0`txwzC6G4(c>5?NHM3K8CTM!b4%Kt@gzMu zx2C-rU(%QHC;hrFrYkdnWI*S(bX6vp3}R$JJxHlIhZOr215AKWGBm&5j|J9%8|$OE zV1Jm7%GuPktfg{U?z;z7Lih|(@aB?>!7 zg8(581*5De1=C1+Tpgn#M5_T@Y{eRO)q=n>N;Fy1DD|@6t%tFP-7j{TD?t=sFN&Yz zI2*K`_gYa+}Q|9rW|@6DTb$w z2)$-JZ+yXILT4;97WSMoTw$HDQY<3=LWDghMOeYGCH$Eal;E`L~jMx4Tm;S9GEcmhotPoN3>o^b+4P2;UB=c<~` zFQ^iQ)|%F6{?c%Y_d$k?61p_6=HJtJmc))eG%)QvPH1XmN+OizD6euQ-Ora$5C#Vm zUDU!UwS#Y(7$cO>yMS9#>d?4K6dHvz|7t`J0Kmr5Rt^OVnd~OR;~FV=)nPd;)1h3J z4CUxhmfKRVw161Cmfj1{4sa0QJ`PbE)!U?x?Q!4TToCzebV$`QTEVJQN>sWR#628T zXiMqNLwngtcVk^2WDf9({eZTfMMbkDhBiXsi=j6{Z?~<6n&$d8*oSU)wrEB>8s-u= z?BQEx6sod0AWy?_I0f zJJ-AJt6uZPR(!ElU-Mkomo6V4&{O%1*MB}bA7zbe_VBVjyzZ+i?nHszTg3nh?%OIl zkz@Pq4Oegzu|d!7*=P7t&*GyPpF14x7SOdFxT{0BRu=#shxoO&PCs|9J8(A=t~+-D zkK=BGaJ@|celLiwcVNz!kd6i&1+lxsxa8Q;-DX^h;qDgm(jF7={U)woYBNE$!)W79 z>F72uvg#-C@dx>O^gzMJE3FWIZSZ104ADmb;J{Efhp4$!CjHzuL-ZJjg0Wm4)y&;e zVR%(~!i_k`)bIfQmVFkE;EKhc!s~|kJzUdA(&}`6v592T`Gr$tS|i%hy!78|>|YmH zn*AspGDl$22G+ErW_A~+QGqr>WH&$)0Cd*xs({QX(`!1V&a381hp zz4xi!s{hmGQ~*f~Adcc3JwV$4pr@naKXwN;h9&{@zC8@=2te1Xb=z?-tF7zAQ%tV= z0&lRUNSCDrxf-^L5}SZTC3%dj+?A`gBZ%q^!o|aczvk_7pr29?nq`Q~C8nQ`!`D z!8C4mqd{ti@)TTmB*VGvNNTiHaed21K&mebwy|J6l*uXMY0^PYv)EmA^@&xKxQUu? ZqNcCW{WsD6o2dR*Az{Fkn+Q;U^G^|=9=QMj diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py index 64098ff..7805f84 100644 --- a/backend/app/services/auth_service.py +++ b/backend/app/services/auth_service.py @@ -2,9 +2,9 @@ from datetime import datetime, timedelta, timezone import uuid from sqlalchemy.ext.asyncio import AsyncSession 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.schemas.auth import UserLiteRegister +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 @@ -12,9 +12,9 @@ from app.core.config import settings class AuthService: @staticmethod 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: - # 1. Person shell + # 1. Person alap létrehozása new_person = Person( first_name=user_in.first_name, last_name=user_in.last_name, @@ -23,7 +23,7 @@ class AuthService: db.add(new_person) await db.flush() - # 2. User fiók + # 2. User technikai fiók new_user = User( email=user_in.email, hashed_password=get_password_hash(user_in.password), @@ -35,10 +35,8 @@ class AuthService: db.add(new_user) await db.flush() - # 3. Biztonsági Token (Beállítható élettartam) - # Default: 48 óra, ha nincs megadva a settingsben + # 3. Kormányozható Token generálása expire_hours = getattr(settings, "REGISTRATION_TOKEN_EXPIRE_HOURS", 48) - token_val = uuid.uuid4() new_token = VerificationToken( token=token_val, @@ -49,9 +47,8 @@ class AuthService: db.add(new_token) 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}" - try: await email_manager.send_email( recipient=user_in.email, @@ -62,7 +59,7 @@ class AuthService: } ) 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.refresh(new_user) @@ -73,11 +70,9 @@ class AuthService: @staticmethod 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: - # Token UUID-vá alakítása az összehasonlításhoz token_uuid = uuid.UUID(token_str) - stmt = select(VerificationToken).where( VerificationToken.token == token_uuid, VerificationToken.is_used == False, @@ -89,18 +84,7 @@ class AuthService: if not token_obj: return False - # Token elhasználása 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() return True except Exception as e: @@ -108,19 +92,70 @@ class AuthService: await db.rollback() 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 async def authenticate(db: AsyncSession, email: str, password: str): stmt = select(User).where(User.email == email, User.is_deleted == False) res = await db.execute(stmt) user = res.scalar_one_or_none() - if not user or not user.hashed_password or not verify_password(password, user.hashed_password): return None return user @staticmethod 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) res = await db.execute(stmt) user = res.scalar_one_or_none() @@ -137,7 +172,6 @@ class AuthService: db.add(new_token) reset_link = f"{settings.FRONTEND_BASE_URL}/reset-password?token={token_val}" - await email_manager.send_email( recipient=email, template_key="password_reset", diff --git a/docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md b/docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md index 27ef3c3..bd0bb7a 100644 --- a/docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md +++ b/docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md @@ -77,4 +77,27 @@ Minden regisztrációnál létrejön: ## IV. CÉGES AZONOSÍTÁS ÉS VERIFIKÁCIÓ ... - **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á. \ No newline at end of file +- **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. \ No newline at end of file