#!/usr/bin/env python3 """ Hard Delete Cleanup Script — Cascade törlés User és kapcsolódó rekordokra. Használat: docker compose exec sf_api python3 /app/backend/app/scripts/cleanup_user.py --email %test% Az --email paraméter részleges egyezést is támogat (SQL LIKE, pl. %test%@%. A szkript tranzakcióban törli: - User rekordot - Person rekordot (ha a User volt az egyetlen kapcsolódó) - Organization rekordot (ha a User volt az egyetlen owner) - Session tokeneket, SocialAccount-okat, Wallet-eket, stb. """ import argparse import logging import sys from sqlalchemy import select, delete, text from sqlalchemy.orm import joinedload # Projekt importok from app.database import async_session_factory from app.models.identity import User, Person, UserRole from app.models.identity.identity import ( SocialAccount, Wallet, ActiveVoucher, VerificationToken, UserDeviceLink, UserTrustProfile, OneTimePassword, ) from app.models.gamification.gamification import UserStats from app.models.marketplace.service_request import ServiceRequest from app.models.social import ServiceReview from app.models.vehicle.asset import VehicleOwnership logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) logger = logging.getLogger("cleanup_user") async def cleanup_user(email_pattern: str) -> None: """ Megkeresi a megadott email mintára illeszkedő usereket, és cascade hard delete-et hajt végre rajtuk. """ async with async_session_factory() as db: # 1. Keresés részleges email egyezéssel stmt = ( select(User) .where(User.email.ilike(email_pattern)) .options( joinedload(User.person), joinedload(User.stats), joinedload(User.wallet), joinedload(User.trust_profile), joinedload(User.ownership_history), joinedload(User.social_accounts), joinedload(User.service_reviews), joinedload(User.service_requests), ) ) result = await db.execute(stmt) users = result.unique().scalars().all() if not users: logger.warning("❌ Nem található user a '%s' mintára.", email_pattern) return logger.info("🔍 Talált userek (%d db):", len(users)) for u in users: logger.info(" - ID=%d, Email=%s, Role=%s", u.id, u.email, u.role.value if hasattr(u.role, 'value') else u.role) # 2. Tranzakció indítása async with db.begin(): for user in users: user_id = user.id email = user.email logger.info("🗑️ Törlés: User ID=%d, Email=%s", user_id, email) # --- Cascade törlés --- # UserDeviceLink await db.execute( delete(UserDeviceLink).where(UserDeviceLink.user_id == user_id) ) logger.info(" ✓ UserDeviceLink törölve") # VerificationToken await db.execute( delete(VerificationToken).where(VerificationToken.user_id == user_id) ) logger.info(" ✓ VerificationToken törölve") # OneTimePassword await db.execute( delete(OneTimePassword).where(OneTimePassword.email == email) ) logger.info(" ✓ OneTimePassword törölve") # SocialAccount (cascade all, delete-orphan) await db.execute( delete(SocialAccount).where(SocialAccount.user_id == user_id) ) logger.info(" ✓ SocialAccount törölve") # UserTrustProfile (cascade all, delete-orphan) await db.execute( delete(UserTrustProfile).where(UserTrustProfile.user_id == user_id) ) logger.info(" ✓ UserTrustProfile törölve") # UserStats (gamification) await db.execute( delete(UserStats).where(UserStats.user_id == user_id) ) logger.info(" ✓ UserStats törölve") # Wallet + ActiveVoucher (cascade) wallet_stmt = select(Wallet).where(Wallet.user_id == user_id) wallet_result = await db.execute(wallet_stmt) wallet = wallet_result.scalar_one_or_none() if wallet: await db.execute( delete(ActiveVoucher).where(ActiveVoucher.wallet_id == wallet.id) ) await db.execute( delete(Wallet).where(Wallet.id == wallet.id) ) logger.info(" ✓ Wallet + ActiveVoucher törölve") # ServiceReview await db.execute( delete(ServiceReview).where(ServiceReview.user_id == user_id) ) logger.info(" ✓ ServiceReview törölve") # ServiceRequest await db.execute( delete(ServiceRequest).where(ServiceRequest.user_id == user_id) ) logger.info(" ✓ ServiceRequest törölve") # VehicleOwnership await db.execute( delete(VehicleOwnership).where(VehicleOwnership.user_id == user_id) ) logger.info(" ✓ VehicleOwnership törölve") # Person rekord kezelése person = user.person if person: # Ellenőrizzük, hogy más User nem hivatkozik-e erre a Person-ra other_users_stmt = ( select(User) .where(User.person_id == person.id, User.id != user_id) .limit(1) ) other_result = await db.execute(other_users_stmt) other_user = other_result.scalar_one_or_none() if not other_user: # Nincs más User ehhez a Person-hoz -> törölhető await db.execute( delete(Person).where(Person.id == person.id) ) logger.info(" ✓ Person ID=%d törölve (egyedüli kapcsolat)", person.id) else: logger.info( " ⚠️ Person ID=%d MEGTARTVA (más User is hivatkozik rá: ID=%d)", person.id, other_user.id ) # Organization kezelése (ha a User volt az egyetlen owner) from app.models.identity.organization import Organization org_stmt = ( select(Organization) .where(Organization.owner_id == user_id) ) org_result = await db.execute(org_stmt) owned_orgs = org_result.scalars().all() for org in owned_orgs: # Ellenőrizzük, hogy más User nem owner-e other_owner_stmt = ( select(User) .where(User.id != user_id, User.id.isnot(None)) .limit(1) ) # Egyszerűbb: csak töröljük, ha nincs más owner # (a gyakorlatban az Organization-nak lehet több tagja) from app.models.identity.organization import OrganizationMember member_stmt = ( select(OrganizationMember) .where( OrganizationMember.organization_id == org.id, OrganizationMember.user_id != user_id, ) .limit(1) ) member_result = await db.execute(member_stmt) other_member = member_result.scalar_one_or_none() if not other_member: await db.execute( delete(Organization).where(Organization.id == org.id) ) logger.info(" ✓ Organization ID=%d törölve (egyedüli owner)", org.id) else: logger.info( " ⚠️ Organization ID=%d MEGTARTVA (van más tag)", org.id ) # VÉGÜL: Maga a User rekord törlése await db.execute( delete(User).where(User.id == user_id) ) logger.info(" ✅ User ID=%d (%s) törölve.", user_id, email) logger.info("🎉 Tranzakció sikeresen végrehajtva.") def main(): parser = argparse.ArgumentParser( description="Hard Delete Cleanup — User és kapcsolódó rekordok cascade törlése." ) parser.add_argument( "--email", required=True, help="Email minta (SQL LIKE, pl. %%test%% vagy teszt%%).", ) args = parser.parse_args() import asyncio asyncio.run(cleanup_user(args.email)) if __name__ == "__main__": main()