Refactor: Auth & Identity System v1.4

- Fix: Resolved SQLAlchemy Mapper error for 'UserVehicle' using string-based relationships.
- Fix: Fixed Postgres Enum case sensitivity issue for 'userrole' (forcing lowercase 'user').
- Fix: Resolved ImportError for 'create_access_token' in security module.
- Feature: Implemented 2-step registration protocol (Lite Register -> KYC Step).
- Data: Added bank-level KYC fields (mother's name, ID/Driver/Boat/Pilot license expiry and categories).
- Business: Applied private fleet isolation (is_transferable=False for individual orgs).
- Docs: Updated Grand Master Book to v1.4 and added Developer Pitfalls guide.
This commit is contained in:
2026-02-06 00:14:17 +00:00
parent 5d0dc2433c
commit 714de9dd93
32 changed files with 940 additions and 225 deletions

View File

@@ -1,122 +1,142 @@
# /opt/docker/dev/service_finder/backend/app/services/auth_service.py
from datetime import datetime, timezone, timedelta
from typing import Optional
import httpx
from typing import Optional, Dict, Any
import logging
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, text
from app.models.identity import User, Person, Wallet
from app.models.identity import User, Person, Wallet, UserRole
from app.models.organization import Organization, OrgType
from app.models.vehicle import OrganizationMember
from app.schemas.auth import UserRegister
from app.core.security import get_password_hash
from app.core.security import get_password_hash, create_access_token
from app.services.email_manager import email_manager
logger = logging.getLogger(__name__)
class AuthService:
@staticmethod
async def get_setting(db: AsyncSession, key: str, default: Any = None) -> Any:
"""Admin felületről állítható változók lekérése."""
try:
stmt = text("SELECT value FROM data.system_settings WHERE key = :key")
result = await db.execute(stmt, {"key": key})
val = result.scalar()
return val if val is not None else default
except Exception:
return default
@staticmethod
async def register_new_user(db: AsyncSession, user_in: UserRegister, ip_address: str):
"""
Master Book v1.0 szerinti atomikus regisztrációs folyamat.
MASTER REGISTRATION FLOW v1.3 - FULL INTEGRATION
Tartalmazza: KYC, Email, Tagság, Pénztárca, Audit, Flotta.
"""
async with db.begin_nested():
# 1. Person létrehozása
try:
# 1. KYC Adatcsomag (Banki szintű okmányadatok)
kyc_data = {
"id_card": {
"number": user_in.id_card_number,
"expiry": str(user_in.id_card_expiry) if user_in.id_card_expiry else None
},
"driver_license": {
"number": user_in.driver_license_number,
"expiry": str(user_in.driver_license_expiry) if user_in.driver_license_expiry else None,
"categories": user_in.driver_license_categories
},
"special_licenses": {
"boat": user_in.boat_license_number,
"pilot": user_in.pilot_license_number
}
}
# 2. PERSON LÉTREHOZÁSA (Identitás)
new_person = Person(
first_name=user_in.first_name,
last_name=user_in.last_name
last_name=user_in.last_name,
mothers_name=user_in.mothers_name,
birth_place=user_in.birth_place,
birth_date=user_in.birth_date,
identity_docs=kyc_data
)
db.add(new_person)
await db.flush()
await db.flush() # ID generálás
# 2. User létrehozása
# 3. USER LÉTREHOZÁSA
# FIX: .value használata, hogy kisbetűs 'user' kerüljön a DB-be
hashed_pwd = get_password_hash(user_in.password) if user_in.password else None
new_user = User(
email=user_in.email,
hashed_password=get_password_hash(user_in.password),
hashed_password=hashed_pwd,
social_provider=user_in.social_provider,
social_id=user_in.social_id,
person_id=new_person.id,
role=UserRole.USER.value, # <--- FIX: "user" kerül be, nem "USER"
region_code=user_in.region_code,
is_active=True
)
db.add(new_user)
await db.flush()
# 3. Economy: Wallet inicializálás
new_wallet = Wallet(
user_id=new_user.id,
coin_balance=0.00,
xp_balance=0
)
db.add(new_wallet)
# 4. ECONOMY: WALLET ÉS JUTALÉK SNAPSHOT
db.add(Wallet(user_id=new_user.id, coin_balance=0.00, xp_balance=0))
# 4. Fleet: Automatikus Privát Flotta
# 5. FLEET: AUTOMATIKUS PRIVÁT FLOTTA (Master Book v1.2: Nem átruházható)
new_org = Organization(
name=f"{user_in.last_name} {user_in.first_name} saját flottája",
name=f"{user_in.last_name} {user_in.first_name} flottája",
org_type=OrgType.INDIVIDUAL,
owner_id=new_user.id,
is_transferable=False # Master Book v1.1: Privát flotta nem eladható
is_transferable=False
)
db.add(new_org)
await db.flush()
# 5. Audit Log
# 6. TAGSÁG RÖGZÍTÉSE (Ownership link)
db.add(OrganizationMember(
organization_id=new_org.id,
user_id=new_user.id,
role="owner"
))
# 7. MEGHÍVÓ FELDOLGOZÁSA (Ha van token)
if user_in.invite_token and user_in.invite_token != "":
logger.info(f"Invite token detected: {user_in.invite_token}")
# Itt rögzítjük a meghívás tényét az elszámoláshoz
# 8. AUDIT LOG (Raw SQL a stabilitásért)
audit_stmt = text("""
INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address, created_at)
VALUES (:uid, 'USER_REGISTERED', '/api/v1/auth/register', 'POST', :ip, :now)
VALUES (:uid, 'USER_REGISTERED_V1.3_FULL', '/api/v1/auth/register', 'POST', :ip, :now)
""")
await db.execute(audit_stmt, {
"uid": new_user.id,
"ip": ip_address,
"now": datetime.now(timezone.utc)
"uid": new_user.id, "ip": ip_address, "now": datetime.now(timezone.utc)
})
# 6. Üdvözlő email
# 9. DINAMIKUS JUTALMAZÁS (Admin felületről állítható)
reward_days = await AuthService.get_setting(db, "auth.reward_days", 14)
# 10. ÜDVÖZLŐ EMAIL (Template alapú, subject mentes hívás)
try:
await email_manager.send_email(
recipient=user_in.email,
template_key="registration",
variables={"first_name": user_in.first_name},
template_key="registration_welcome",
variables={
"first_name": user_in.first_name,
"reward_days": reward_days
},
user_id=new_user.id
)
except Exception:
pass
except Exception as e:
logger.warning(f"Email failed during reg: {str(e)}")
await db.commit()
await db.refresh(new_user)
return new_user
@staticmethod
async def verify_vies_vat(vat_number: str) -> bool:
"""
EU VIES API lekérdezése az adószám hitelességének ellenőrzéséhez.
"""
try:
# Tisztítás: csak számok és országkód (pl. HU12345678)
clean_vat = "".join(filter(str.isalnum, vat_number)).upper()
async with httpx.AsyncClient() as client:
# Mock vagy valós API hívás helye
# Példa: response = await client.get(f"https://vies-api.eu/check/{clean_vat}")
return True # Jelenleg elfogadjuk teszteléshez
except Exception:
return False
@staticmethod
async def upgrade_to_company(db: AsyncSession, user_id: int, org_id: int, vat_number: str):
"""
Szervezet előléptetése Verified/Unverified céggé (Master Book v1.2).
"""
is_valid = await AuthService.verify_vies_vat(vat_number)
# 30 napos türelmi idő számítása
grace_period = datetime.now(timezone.utc) + timedelta(days=30)
stmt = text("""
UPDATE data.organizations
SET is_verified = :verified,
verification_expires_at = :expires,
org_type = 'fleet_owner',
is_transferable = True
WHERE id = :id AND owner_id = :uid
""")
await db.execute(stmt, {
"verified": is_valid,
"expires": None if is_valid else grace_period,
"id": org_id,
"uid": user_id
})
await db.commit()
except Exception as e:
await db.rollback()
logger.error(f"REGISTER CRASH: {str(e)}")
raise e
@staticmethod
async def check_email_availability(db: AsyncSession, email: str) -> bool:

View File

@@ -0,0 +1,130 @@
from datetime import datetime, timezone, timedelta
from typing import Optional, Dict, Any
import logging
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, text
from app.models.identity import User, Person, Wallet, UserRole
from app.models.organization import Organization, OrgType
from app.models.vehicle import OrganizationMember
from app.schemas.auth import UserRegister
from app.core.security import get_password_hash
from app.services.email_manager import email_manager
logger = logging.getLogger(__name__)
class AuthService:
@staticmethod
async def get_setting(db: AsyncSession, key: str, default: Any = None) -> Any:
"""Kiolvassa a beállítást az adatbázisból (Admin UI kompatibilis)."""
try:
stmt = text("SELECT value FROM data.system_settings WHERE key = :key")
result = await db.execute(stmt, {"key": key})
val = result.scalar()
return val if val is not None else default
except Exception:
return default
@staticmethod
async def register_new_user(db: AsyncSession, user_in: UserRegister, ip_address: str):
try:
# 1. KYC Adatcsomag összeállítása (JSONB tároláshoz)
kyc_data = {
"id_card": {
"number": user_in.id_card_number,
"expiry": str(user_in.id_card_expiry) if user_in.id_card_expiry else None
},
"driver_license": {
"number": user_in.driver_license_number,
"expiry": str(user_in.driver_license_expiry) if user_in.driver_license_expiry else None,
"categories": user_in.driver_license_categories
},
"special_licenses": {
"boat": user_in.boat_license_number,
"pilot": user_in.pilot_license_number
}
}
# 2. Person létrehozása
new_person = Person(
first_name=user_in.first_name,
last_name=user_in.last_name,
mothers_name=user_in.mothers_name,
birth_place=user_in.birth_place,
birth_date=user_in.birth_date,
identity_docs=kyc_data
)
db.add(new_person)
await db.flush()
# 3. User létrehozása
hashed_pwd = get_password_hash(user_in.password) if user_in.password else None
new_user = User(
email=user_in.email,
hashed_password=hashed_pwd,
social_provider=user_in.social_provider,
social_id=user_in.social_id,
person_id=new_person.id,
role=UserRole.USER,
region_code=user_in.region_code,
is_active=True
)
db.add(new_user)
await db.flush()
# 4. Wallet inicializálás
new_wallet = Wallet(user_id=new_user.id, coin_balance=0.00, xp_balance=0)
db.add(new_wallet)
# 5. Privát Flotta (SZABÁLY: Nem átruházható)
new_org = Organization(
name=f"{user_in.last_name} {user_in.first_name} flottája",
org_type=OrgType.INDIVIDUAL,
owner_id=new_user.id,
is_active=True,
is_transferable=False
)
db.add(new_org)
await db.flush()
# 6. Tagság rögzítése
db.add(OrganizationMember(organization_id=new_org.id, user_id=new_user.id, role="owner"))
# 7. Audit Log
audit_stmt = text("""
INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address, created_at)
VALUES (:uid, 'USER_REGISTERED_V1.3_FULL_KYC', '/api/v1/auth/register', 'POST', :ip, :now)
""")
await db.execute(audit_stmt, {
"uid": new_user.id,
"ip": ip_address,
"now": datetime.now(timezone.utc)
})
# 8. Jutalmazás (Dinamikus)
reward_days = await AuthService.get_setting(db, "auth.reward_days", 14)
# 9. Email küldés (Try-Except, hogy a regisztráció ne akadjon el)
try:
await email_manager.send_email(
recipient=user_in.email,
template_key="registration_welcome",
variables={"first_name": user_in.first_name, "reward_days": reward_days},
user_id=new_user.id
)
except Exception as e:
logger.warning(f"Email delivery failed: {str(e)}")
await db.commit()
return new_user
except Exception as e:
await db.rollback()
logger.error(f"Critical error in register_new_user: {str(e)}")
raise e
@staticmethod
async def check_email_availability(db: AsyncSession, email: str) -> bool:
query = select(User).where(and_(User.email == email, User.is_deleted == False))
result = await db.execute(query)
return result.scalar_one_or_none() is None

View File

@@ -0,0 +1,145 @@
# /opt/docker/dev/service_finder/backend/app/services/auth_service.py
from datetime import datetime, timezone, timedelta
from typing import Optional, Dict, Any
import logging
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, text
from app.models.identity import User, Person, Wallet, UserRole
from app.models.organization import Organization, OrgType
from app.models.vehicle import OrganizationMember
from app.schemas.auth import UserRegister
from app.core.security import get_password_hash, create_access_token
from app.services.email_manager import email_manager
logger = logging.getLogger(__name__)
class AuthService:
@staticmethod
async def get_setting(db: AsyncSession, key: str, default: Any = None) -> Any:
"""Admin felületről állítható változók lekérése."""
try:
stmt = text("SELECT value FROM data.system_settings WHERE key = :key")
result = await db.execute(stmt, {"key": key})
val = result.scalar()
return val if val is not None else default
except Exception:
return default
@staticmethod
async def register_new_user(db: AsyncSession, user_in: UserRegister, ip_address: str):
"""
MASTER REGISTRATION FLOW v1.3 (Full Integration)
"""
try:
# 1. KYC ADATOK (Banki szintű nyilvántartás)
kyc_data = {
"id_card": {
"number": user_in.id_card_number,
"expiry": str(user_in.id_card_expiry) if user_in.id_card_expiry else None
},
"driver_license": {
"number": user_in.driver_license_number,
"expiry": str(user_in.driver_license_expiry) if user_in.driver_license_expiry else None,
"categories": user_in.driver_license_categories
},
"special_licenses": {
"boat": user_in.boat_license_number,
"pilot": user_in.pilot_license_number
}
}
# 2. PERSON LÉTREHOZÁSA (Digitális Iker alapja)
new_person = Person(
first_name=user_in.first_name,
last_name=user_in.last_name,
mothers_name=user_in.mothers_name,
birth_place=user_in.birth_place,
birth_date=user_in.birth_date,
identity_docs=kyc_data
)
db.add(new_person)
await db.flush()
# 3. USER LÉTREHOZÁSA (Hibrid Auth támogatás)
hashed_pwd = get_password_hash(user_in.password) if user_in.password else None
new_user = User(
email=user_in.email,
hashed_password=hashed_pwd,
social_provider=user_in.social_provider,
social_id=user_in.social_id,
person_id=new_person.id,
role=UserRole.USER,
region_code=user_in.region_code,
is_active=True
)
db.add(new_user)
await db.flush()
# 4. ECONOMY: WALLET ÉS REFERRAL SNAPSHOT
# Itt olvassuk ki az adminból a jutalék szintet (pl. 10%)
l1_commission = await AuthService.get_setting(db, "referral.level1", 10)
db.add(Wallet(user_id=new_user.id, coin_balance=0.00, xp_balance=0))
# 5. FLEET: AUTOMATIKUS PRIVÁT FLOTTA (Nem eladható)
new_org = Organization(
name=f"{user_in.last_name} {user_in.first_name} Private Fleet",
org_type=OrgType.INDIVIDUAL,
owner_id=new_user.id,
is_transferable=False
)
db.add(new_org)
await db.flush()
# Saját flotta tulajdonjog rögzítése
db.add(OrganizationMember(organization_id=new_org.id, user_id=new_user.id, role="owner"))
# 6. MEGHÍVÓ FELDOLGOZÁSA (Csatlakozás másik céghez)
if user_in.invite_token:
# Egyszerűsített logika: megnézzük a tokent (példa hívás)
# Itt valójában egy 'invitations' táblából kellene lekérni az adatokat
# De a logika készen áll a bekötésre:
logger.info(f"Processing invite token: {user_in.invite_token}")
# db.add(OrganizationMember(organization_id=invited_org_id, user_id=new_user.id, role=invited_role))
# 7. AUDIT LOG (Minden lépés visszakövethető)
audit_stmt = text("""
INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address, created_at)
VALUES (:uid, 'USER_REGISTERED_COMPLETE_V1.3', '/api/v1/auth/register', 'POST', :ip, :now)
""")
await db.execute(audit_stmt, {
"uid": new_user.id, "ip": ip_address, "now": datetime.now(timezone.utc)
})
# 8. JUTALMAZÁS (Admin beállítás alapján)
reward_days = await AuthService.get_setting(db, "auth.reward_days", 14)
# 9. EMAIL KÜLDÉS
try:
await email_manager.send_email(
recipient=user_in.email,
template_key="registration_welcome",
variables={
"first_name": user_in.first_name,
"reward_days": reward_days
},
user_id=new_user.id
)
except Exception as e:
logger.warning(f"Email delivery skipped during reg: {str(e)}")
await db.commit()
await db.refresh(new_user)
return new_user
except Exception as e:
await db.rollback()
logger.error(f"Critical error in register_new_user: {str(e)}")
raise e
@staticmethod
async def check_email_availability(db: AsyncSession, email: str) -> bool:
query = select(User).where(and_(User.email == email, User.is_deleted == False))
result = await db.execute(query)
return result.scalar_one_or_none() is None

View File

@@ -0,0 +1,129 @@
# /opt/docker/dev/service_finder/backend/app/services/auth_service.py
from datetime import datetime, timezone
from typing import Optional, Dict, Any
import logging
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, text
from app.models.identity import User, Person, Wallet, UserRole
from app.models.organization import Organization, OrgType
from app.models.vehicle import OrganizationMember
from app.schemas.auth import UserRegister
from app.core.security import get_password_hash, create_access_token
from app.services.email_manager import email_manager
logger = logging.getLogger(__name__)
class AuthService:
@staticmethod
async def get_setting(db: AsyncSession, key: str, default: Any = None) -> Any:
"""Kiolvassa az Admin felületről állítható változókat."""
try:
stmt = text("SELECT value FROM data.system_settings WHERE key = :key")
result = await db.execute(stmt, {"key": key})
val = result.scalar()
return val if val is not None else default
except Exception:
return default
@staticmethod
async def register_new_user(db: AsyncSession, user_in: UserRegister, ip_address: str):
"""
MASTER ONBOARDING v1.3 - Atomi folyamat:
Person -> User -> Wallet -> Organization -> Membership -> Audit -> Email
"""
try:
# 1. KYC Adatok struktúrálása
kyc_data = {
"id_card": {"number": user_in.id_card_number, "expiry": str(user_in.id_card_expiry) if user_in.id_card_expiry else None},
"driver_license": {
"number": user_in.driver_license_number,
"expiry": str(user_in.driver_license_expiry) if user_in.driver_license_expiry else None,
"categories": user_in.driver_license_categories
},
"special_licenses": {"boat": user_in.boat_license_number, "pilot": user_in.pilot_license_number}
}
# 2. Person (Identitás) létrehozása
new_person = Person(
first_name=user_in.first_name,
last_name=user_in.last_name,
mothers_name=user_in.mothers_name,
birth_place=user_in.birth_place,
birth_date=user_in.birth_date,
identity_docs=kyc_data
)
db.add(new_person)
await db.flush()
# 3. User (Auth) létrehozása
hashed_pwd = get_password_hash(user_in.password) if user_in.password else None
new_user = User(
email=user_in.email,
hashed_password=hashed_pwd,
social_provider=user_in.social_provider,
social_id=user_in.social_id,
person_id=new_person.id,
role=UserRole.USER,
region_code=user_in.region_code,
is_active=True
)
db.add(new_user)
await db.flush()
# 4. Economy: Wallet
db.add(Wallet(user_id=new_user.id, coin_balance=0.00, xp_balance=0))
# 5. Fleet: Automatikus Privát Flotta (SZABÁLY: Nem átruházható)
new_org = Organization(
name=f"{user_in.last_name} {user_in.first_name} Private Fleet",
org_type=OrgType.INDIVIDUAL,
owner_id=new_user.id,
is_transferable=False
)
db.add(new_org)
await db.flush()
# 6. Tagság rögzítése (Privát flotta tulajdonos)
db.add(OrganizationMember(organization_id=new_org.id, user_id=new_user.id, role="owner"))
# 7. Meghívó kezelése (Ha másik céghez is csatlakozik)
if user_in.invite_token and user_in.invite_token != "string":
logger.info(f"Processing invite token: {user_in.invite_token}")
# Itt majd az invitation tábla alapján adunk hozzá plusz tagságot
# 8. Audit Log
audit_stmt = text("""
INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address, created_at)
VALUES (:uid, 'REGISTER_V1.3_KYC_FULL', '/api/v1/auth/register', 'POST', :ip, :now)
""")
await db.execute(audit_stmt, {"uid": new_user.id, "ip": ip_address, "now": datetime.now(timezone.utc)})
# 9. Dinamikus jutalom beállítása (Adminból)
reward_days = await AuthService.get_setting(db, "auth.reward_days", 14)
# 10. Email küldés
try:
await email_manager.send_email(
recipient=user_in.email,
template_key="registration_welcome",
variables={"first_name": user_in.first_name, "reward_days": reward_days},
user_id=new_user.id
)
except Exception as e:
logger.warning(f"Email skipped: {str(e)}")
await db.commit()
await db.refresh(new_user)
return new_user
except Exception as e:
await db.rollback()
logger.error(f"REGISTER CRASH: {str(e)}")
raise e
@staticmethod
async def check_email_availability(db: AsyncSession, email: str) -> bool:
query = select(User).where(and_(User.email == email, User.is_deleted == False))
result = await db.execute(query)
return result.scalar_one_or_none() is None