# /opt/docker/dev/service_finder/backend/app/services/auth_service.py import logging import uuid from datetime import datetime, timedelta, timezone from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, and_, update from sqlalchemy.orm import joinedload from fastapi.encoders import jsonable_encoder from fastapi import HTTPException, status from app.models.identity import User, Person, UserRole, VerificationToken, Wallet from app.models.gamification import UserStats from app.models.organization import Organization, OrganizationMember, OrgType, Branch from app.schemas.auth import UserLiteRegister, UserKYCComplete from app.core.security import get_password_hash, verify_password, generate_secure_slug from app.services.email_manager import email_manager from app.core.config import settings from app.services.config_service import config from app.services.geo_service import GeoService from app.services.security_service import security_service from app.services.gamification_service import GamificationService logger = logging.getLogger(__name__) class AuthService: @staticmethod async def register_lite(db: AsyncSession, user_in: UserLiteRegister): """ 1. FÁZIS: Lite regisztráció dinamikus korlátokkal és Sentinel naplózással. """ try: # Paraméterek lekérése az admin felületről min_pass = await config.get_setting(db, "auth_min_password_length", default=8) default_role_name = await config.get_setting(db, "auth_default_role", default="user") reg_token_hours = await config.get_setting(db, "auth_registration_hours", region_code=user_in.region_code, default=48) if len(user_in.password) < int(min_pass): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"A jelszónak legalább {min_pass} karakter hosszúnak kell lennie." ) new_person = Person( first_name=user_in.first_name, last_name=user_in.last_name, is_active=False ) db.add(new_person) await db.flush() # Szerepkör dinamikus feloldása assigned_role = UserRole[default_role_name] if default_role_name in UserRole.__members__ else UserRole.user new_user = User( email=user_in.email, hashed_password=get_password_hash(user_in.password), person_id=new_person.id, role=assigned_role, is_active=False, is_deleted=False, region_code=user_in.region_code, preferred_language=user_in.lang, timezone=user_in.timezone ) db.add(new_user) await db.flush() # Verifikációs token token_val = uuid.uuid4() db.add(VerificationToken( token=token_val, user_id=new_user.id, token_type="registration", expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reg_token_hours)) )) # Email küldés a beállított template alapján verification_link = f"{settings.FRONTEND_BASE_URL}/verify?token={token_val}" await email_manager.send_email( recipient=user_in.email, template_key="reg", variables={"first_name": user_in.first_name, "link": verification_link}, lang=user_in.lang ) # Sentinel Audit Log await security_service.log_event( db, user_id=new_user.id, action="USER_REGISTER_LITE", severity="info", target_type="User", target_id=str(new_user.id), new_data={"email": user_in.email} ) await db.commit() return new_user except Exception as e: await db.rollback() logger.error(f"Lite Reg Error: {e}") raise e @staticmethod async def complete_kyc(db: AsyncSession, user_id: int, kyc_in: UserKYCComplete): """ 2. FÁZIS: Teljes profil és Gamification inicializálás. """ try: stmt = select(User).options(joinedload(User.person)).where(User.id == user_id) user = (await db.execute(stmt)).scalar_one_or_none() if not user: return None # Dinamikus beállítások (Soha ne legyen kódba vésve!) org_tpl = await config.get_setting(db, "org_naming_template", default="{last_name} Flotta") base_cur = await config.get_setting(db, "finance_default_currency", region_code=user.region_code, default="HUF") kyc_reward = await config.get_setting(db, "gamification_kyc_bonus", default=500) # Címkezelés (GeoService hívás) addr_id = await GeoService.get_or_create_full_address( db, zip_code=kyc_in.address_zip, city=kyc_in.address_city, street_name=kyc_in.address_street_name, street_type=kyc_in.address_street_type, house_number=kyc_in.address_house_number, parcel_id=kyc_in.address_hrsz ) # Person adatok dúsítása p = user.person p.mothers_last_name = kyc_in.mothers_last_name p.mothers_first_name = kyc_in.mothers_first_name p.birth_place = kyc_in.birth_place p.birth_date = kyc_in.birth_date p.phone = kyc_in.phone_number p.address_id = addr_id p.identity_docs = jsonable_encoder(kyc_in.identity_docs) p.is_active = True # Dinamikus szervezet generálás org_full_name = org_tpl.format(last_name=p.last_name, first_name=p.first_name) new_org = Organization( full_name=org_full_name, name=f"{p.last_name} Széfe", folder_slug=generate_secure_slug(12), org_type=OrgType.individual, owner_id=user.id, is_active=True, status="verified", country_code=user.region_code ) db.add(new_org) await db.flush() # Infrastruktúra elemek db.add(Branch(organization_id=new_org.id, address_id=addr_id, name="Home Base", is_main=True)) db.add(OrganizationMember(organization_id=new_org.id, user_id=user.id, role="OWNER")) db.add(Wallet(user_id=user.id, currency=kyc_in.preferred_currency or base_cur)) db.add(UserStats(user_id=user.id)) user.is_active = True user.folder_slug = generate_secure_slug(12) # Gamification XP jóváírás await GamificationService.award_points(db, user_id=user.id, amount=int(kyc_reward), reason="KYC_VERIFICATION") await db.commit() return user except Exception as e: await db.rollback() logger.error(f"KYC Error: {e}") raise e @staticmethod async def authenticate(db: AsyncSession, email: str, password: str): """ Felhasználó hitelesítése. """ stmt = select(User).where(and_(User.email == email, User.is_deleted == False)) res = await db.execute(stmt) user = res.scalar_one_or_none() if user and verify_password(password, user.hashed_password): return user return None @staticmethod async def verify_email(db: AsyncSession, token_str: str): """ Email megerősítés. """ try: token_uuid = uuid.UUID(token_str) stmt = select(VerificationToken).where(and_( VerificationToken.token == token_uuid, VerificationToken.is_used == False, VerificationToken.expires_at > datetime.now(timezone.utc) )) token = (await db.execute(stmt)).scalar_one_or_none() if not token: return False token.is_used = True # Itt aktiválhatnánk a júzert, ha a Lite regnél még nem tennénk meg await db.commit() return True except: return False @staticmethod async def initiate_password_reset(db: AsyncSession, email: str): """ Elfelejtett jelszó folyamat indítása. """ stmt = select(User).where(and_(User.email == email, User.is_deleted == False)) user = (await db.execute(stmt)).scalar_one_or_none() if user: # Dinamikus lejárat az adminból reset_h = await config.get_setting(db, "auth_password_reset_hours", region_code=user.region_code, default=2) token_val = uuid.uuid4() db.add(VerificationToken( token=token_val, user_id=user.id, token_type="password_reset", expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reset_h)) )) link = f"{settings.FRONTEND_BASE_URL}/reset-password?token={token_val}" await email_manager.send_email( recipient=email, template_key="pwd_reset", variables={"link": link}, lang=user.preferred_language ) await db.commit() return "success" return "not_found" @staticmethod async def reset_password(db: AsyncSession, email: str, token_str: str, new_password: str): """ Jelszó tényleges megváltoztatása token alapján. """ try: token_uuid = uuid.UUID(token_str) stmt = select(VerificationToken).join(User).where(and_( User.email == email, VerificationToken.token == token_uuid, VerificationToken.token_type == "password_reset", VerificationToken.is_used == False, VerificationToken.expires_at > datetime.now(timezone.utc) )) token_rec = (await db.execute(stmt)).scalar_one_or_none() if not token_rec: return False user_stmt = select(User).where(User.id == token_rec.user_id) user = (await db.execute(user_stmt)).scalar_one() user.hashed_password = get_password_hash(new_password) token_rec.is_used = True await db.commit() return True except: return False @staticmethod async def soft_delete_user(db: AsyncSession, user_id: int, reason: str, actor_id: int): """ Felhasználó törlése (Soft-Delete) auditálással. """ stmt = select(User).where(User.id == user_id) user = (await db.execute(stmt)).scalar_one_or_none() if not user or user.is_deleted: return False old_email = user.email user.email = f"deleted_{user.id}_{datetime.now().strftime('%Y%m%d')}_{old_email}" user.is_deleted = True user.is_active = False await security_service.log_event( db, user_id=actor_id, action="USER_SOFT_DELETE", severity="warning", target_type="User", target_id=str(user_id), new_data={"reason": reason} ) await db.commit() return True