645 lines
28 KiB
Python
645 lines
28 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
P0 SYSTEM RESET — Deep User Purge & Private Garage Restitution
|
||
|
||
CRITICAL DIRECTIVE: The Architect has ordered a massive system reset.
|
||
We must delete all dummy/test users EXCEPT for explicitly protected IDs
|
||
and Admin/Superadmin roles. Afterward, we must ensure every surviving user
|
||
has a correctly named Private Garage according to the new naming convention.
|
||
|
||
NEW NAMING CONVENTION:
|
||
"{last_name} {first_name} - Privát Garázs (#{user_id})"
|
||
Example: "Gyöngyössy Zsolt - Privát Garázs (#28)"
|
||
|
||
Phases:
|
||
1. THE GREAT USER PURGE — Delete unprotected users and cascade safely
|
||
2. RESTORE & RENAME GARAGES — Ensure every survivor has a correctly named private garage
|
||
3. VERIFICATION — Report results
|
||
|
||
Usage:
|
||
docker compose exec sf_api python3 /app/scripts/system_reset.py
|
||
"""
|
||
|
||
import asyncio
|
||
import logging
|
||
import sys
|
||
from datetime import datetime, timezone
|
||
|
||
from sqlalchemy import text
|
||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||
|
||
sys.path.insert(0, '/app')
|
||
from app.core.config import settings
|
||
|
||
# ──────────────────────────────────────────────
|
||
# Configuration
|
||
# ──────────────────────────────────────────────
|
||
|
||
# Protected User IDs (never delete these)
|
||
PROTECTED_USER_IDS = (100, 88, 86, 85, 7929, 28, 2, 1)
|
||
|
||
# Protected Roles (never delete users with these roles)
|
||
PROTECTED_ROLES = ('SUPERADMIN', 'ADMIN')
|
||
|
||
# Private free subscription tier ID (from system.subscription_tiers)
|
||
PRIVATE_FREE_TIER_ID = 13 # private_free_v1
|
||
|
||
DATABASE_URL = settings.DATABASE_URL
|
||
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||
handlers=[logging.StreamHandler(sys.stdout)],
|
||
)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# =====================================================================
|
||
# PHASE 1: THE GREAT USER PURGE
|
||
# =====================================================================
|
||
|
||
async def phase_1_purge_users(db: AsyncSession) -> dict:
|
||
"""
|
||
Find all users NOT in the Protected List AND NOT in Protected Roles.
|
||
Delete them safely with cascade.
|
||
"""
|
||
logger.info("\n" + "=" * 60)
|
||
logger.info("PHASE 1: THE GREAT USER PURGE")
|
||
logger.info("=" * 60)
|
||
|
||
# 1. Identify users to delete
|
||
protected_ids_str = ", ".join(str(x) for x in PROTECTED_USER_IDS)
|
||
protected_roles_str = ", ".join(f"'{r}'" for r in PROTECTED_ROLES)
|
||
|
||
find_targets_sql = f"""
|
||
SELECT id, email, role
|
||
FROM identity.users
|
||
WHERE id NOT IN ({protected_ids_str})
|
||
AND role NOT IN ({protected_roles_str})
|
||
AND is_deleted = false
|
||
ORDER BY id
|
||
"""
|
||
result = await db.execute(text(find_targets_sql))
|
||
targets = result.fetchall()
|
||
|
||
if not targets:
|
||
logger.info("✅ No unprotected users found. Nothing to purge.")
|
||
return {"deleted_users": 0, "deleted_persons": 0, "target_ids": []}
|
||
|
||
target_ids = [row[0] for row in targets]
|
||
target_ids_str = ", ".join(str(x) for x in target_ids)
|
||
|
||
logger.info(f"🔍 Found {len(targets)} unprotected users to purge:")
|
||
for uid, email, role in targets:
|
||
logger.info(f" - ID={uid}: {email} (role={role})")
|
||
|
||
# 2. Collect all person_ids linked to these users (before deletion)
|
||
person_sql = f"""
|
||
SELECT DISTINCT person_id FROM identity.users
|
||
WHERE id IN ({target_ids_str})
|
||
AND person_id IS NOT NULL
|
||
"""
|
||
person_result = await db.execute(text(person_sql))
|
||
person_ids = [row[0] for row in person_result.fetchall()]
|
||
person_ids_str = ", ".join(str(x) for x in person_ids) if person_ids else "0"
|
||
|
||
logger.info(f" Associated person IDs to handle: {person_ids}")
|
||
|
||
# 3. Delete dependent records in safe order
|
||
|
||
# 3a. Delete gamification records (CASCADE for some, manual for others)
|
||
logger.info("\n🗑️ Deleting gamification records...")
|
||
for table in [
|
||
"gamification.points_ledger",
|
||
"gamification.user_badges",
|
||
"gamification.user_scores",
|
||
"gamification.user_stats",
|
||
"gamification.user_contributions",
|
||
]:
|
||
try:
|
||
del_sql = f"DELETE FROM {table} WHERE user_id IN ({target_ids_str})"
|
||
r = await db.execute(text(del_sql))
|
||
logger.info(f" ✅ {table}: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ {table}: {e}")
|
||
|
||
# 3b. Delete vehicle ratings
|
||
logger.info("\n🗑️ Deleting vehicle_user_ratings...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM vehicle.vehicle_user_ratings WHERE user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ vehicle.vehicle_user_ratings: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ vehicle.vehicle_user_ratings: {e}")
|
||
|
||
# 3c. Delete service requests
|
||
logger.info("\n🗑️ Deleting service_requests...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM marketplace.service_requests WHERE user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ marketplace.service_requests: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ marketplace.service_requests: {e}")
|
||
|
||
# 3d. NULL out service_reviews
|
||
logger.info("\n🗑️ NULLing service_reviews...")
|
||
try:
|
||
r = await db.execute(text(f"UPDATE marketplace.service_reviews SET user_id = NULL WHERE user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ marketplace.service_reviews: {r.rowcount} rows NULLed")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ marketplace.service_reviews: {e}")
|
||
|
||
# 3e. Delete social accounts
|
||
logger.info("\n🗑️ Deleting social_accounts...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM identity.social_accounts WHERE user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ identity.social_accounts: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ identity.social_accounts: {e}")
|
||
|
||
# 3f. Delete verification tokens
|
||
logger.info("\n🗑️ Deleting verification_tokens...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM identity.verification_tokens WHERE user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ identity.verification_tokens: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ identity.verification_tokens: {e}")
|
||
|
||
# 3g. Delete user_device_links
|
||
logger.info("\n🗑️ Deleting user_device_links...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM identity.user_device_links WHERE user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ identity.user_device_links: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ identity.user_device_links: {e}")
|
||
|
||
# 3h. Delete user_trust_profiles
|
||
logger.info("\n🗑️ Deleting user_trust_profiles...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM identity.user_trust_profiles WHERE user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ identity.user_trust_profiles: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ identity.user_trust_profiles: {e}")
|
||
|
||
# 3i. Delete internal_notifications
|
||
logger.info("\n🗑️ Deleting internal_notifications...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM system.internal_notifications WHERE user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ system.internal_notifications: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ system.internal_notifications: {e}")
|
||
|
||
# 3j. Delete user_subscriptions
|
||
logger.info("\n🗑️ Deleting user_subscriptions...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM finance.user_subscriptions WHERE user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ finance.user_subscriptions: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ finance.user_subscriptions: {e}")
|
||
|
||
# 3k. Delete withdrawal_requests
|
||
logger.info("\n🗑️ Deleting withdrawal_requests...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM finance.withdrawal_requests WHERE user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ finance.withdrawal_requests: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ finance.withdrawal_requests: {e}")
|
||
|
||
# 3l. Delete payment_intents
|
||
logger.info("\n🗑️ Deleting payment_intents...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM finance.payment_intents WHERE payer_id IN ({target_ids_str}) OR beneficiary_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ finance.payment_intents: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ finance.payment_intents: {e}")
|
||
|
||
# 3m. Delete wallets
|
||
logger.info("\n🗑️ Deleting wallets...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM identity.wallets WHERE user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ identity.wallets: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ identity.wallets: {e}")
|
||
|
||
# 3n. Delete audit_logs (SET NULL handled by DB, but clean up)
|
||
logger.info("\n🗑️ NULLing audit_logs...")
|
||
try:
|
||
r = await db.execute(text(f"UPDATE audit.operational_logs SET user_id = NULL WHERE user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ audit.operational_logs: {r.rowcount} rows NULLed")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ audit.operational_logs: {e}")
|
||
|
||
# 3o. Handle organizations owned by these users
|
||
logger.info("\n🗑️ Handling organizations owned by purged users...")
|
||
org_sql = f"""
|
||
SELECT id, name FROM fleet.organizations
|
||
WHERE owner_id IN ({target_ids_str})
|
||
"""
|
||
org_result = await db.execute(text(org_sql))
|
||
owned_orgs = org_result.fetchall()
|
||
|
||
for org_id, org_name in owned_orgs:
|
||
logger.info(f" 🏪 Soft-deleting org ID={org_id}: {org_name}")
|
||
# First NULL out owner_id to break FK constraint to identity.users
|
||
await db.execute(text("""
|
||
UPDATE fleet.organizations
|
||
SET owner_id = NULL, is_deleted = true, is_active = false,
|
||
status = 'deleted', last_deactivated_at = :now
|
||
WHERE id = :oid
|
||
"""), {"now": datetime.now(timezone.utc), "oid": org_id})
|
||
|
||
# Delete org members
|
||
await db.execute(text(f"DELETE FROM fleet.organization_members WHERE organization_id = :oid"), {"oid": org_id})
|
||
# Delete branches
|
||
await db.execute(text(f"DELETE FROM fleet.branches WHERE organization_id = :oid"), {"oid": org_id})
|
||
|
||
# 3p. Delete organization_members for these users
|
||
logger.info("\n🗑️ Deleting organization_members for purged users...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM fleet.organization_members WHERE user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ fleet.organization_members: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ fleet.organization_members: {e}")
|
||
|
||
# 3q. Delete org_sales_assignments
|
||
logger.info("\n🗑️ Deleting org_sales_assignments...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM fleet.org_sales_assignments WHERE agent_user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ fleet.org_sales_assignments: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ fleet.org_sales_assignments: {e}")
|
||
|
||
# 3r. Delete ratings authored by these users
|
||
logger.info("\n🗑️ Deleting marketplace ratings...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM marketplace.ratings WHERE author_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ marketplace.ratings: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ marketplace.ratings: {e}")
|
||
|
||
# 3s. Delete documents uploaded by these users
|
||
logger.info("\n🗑️ NULLing documents...")
|
||
try:
|
||
r = await db.execute(text(f"UPDATE system.documents SET uploaded_by = NULL WHERE uploaded_by IN ({target_ids_str})"))
|
||
logger.info(f" ✅ system.documents: {r.rowcount} rows NULLed")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ system.documents: {e}")
|
||
|
||
# 3t. Delete pending_actions
|
||
logger.info("\n🗑️ Deleting pending_actions...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM system.pending_actions WHERE requester_id IN ({target_ids_str}) OR approver_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ system.pending_actions: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ system.pending_actions: {e}")
|
||
|
||
# 3u. Delete vehicle_logbook entries
|
||
logger.info("\n🗑️ Deleting vehicle_logbook...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM vehicle.vehicle_logbook WHERE driver_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ vehicle.vehicle_logbook: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ vehicle.vehicle_logbook: {e}")
|
||
|
||
# 3v. Delete vehicle_ownership_history
|
||
logger.info("\n🗑️ Deleting vehicle_ownership_history...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM vehicle.vehicle_ownership_history WHERE user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ vehicle.vehicle_ownership_history: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ vehicle.vehicle_ownership_history: {e}")
|
||
|
||
# 3w. Delete vehicle_transfer_requests
|
||
logger.info("\n🗑️ Deleting vehicle_transfer_requests...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM vehicle.vehicle_transfer_requests WHERE requester_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ vehicle.vehicle_transfer_requests: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ vehicle.vehicle_transfer_requests: {e}")
|
||
|
||
# 3x. Delete asset_inspections
|
||
logger.info("\n🗑️ Deleting asset_inspections...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM vehicle.asset_inspections WHERE inspector_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ vehicle.asset_inspections: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ vehicle.asset_inspections: {e}")
|
||
|
||
# 3y. Delete asset_reviews
|
||
logger.info("\n🗑️ Deleting asset_reviews...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM vehicle.asset_reviews WHERE user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ vehicle.asset_reviews: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ vehicle.asset_reviews: {e}")
|
||
|
||
# 3z. Delete financial_ledger entries
|
||
logger.info("\n🗑️ Deleting financial_ledger...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM audit.financial_ledger WHERE user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ audit.financial_ledger: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ audit.financial_ledger: {e}")
|
||
|
||
# 3za. Delete security_audit_logs
|
||
logger.info("\n🗑️ Deleting security_audit_logs...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM audit.security_audit_logs WHERE actor_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ audit.security_audit_logs: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ audit.security_audit_logs: {e}")
|
||
|
||
# 3zb. Delete audit_logs
|
||
logger.info("\n🗑️ Deleting audit_logs...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM audit.audit_logs WHERE user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ audit.audit_logs: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ audit.audit_logs: {e}")
|
||
|
||
# 3zc. Delete service_providers added by these users
|
||
logger.info("\n🗑️ NULLing service_providers.added_by_user_id...")
|
||
try:
|
||
r = await db.execute(text(f"UPDATE marketplace.service_providers SET added_by_user_id = NULL WHERE added_by_user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ marketplace.service_providers: {r.rowcount} rows NULLed")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ marketplace.service_providers: {e}")
|
||
|
||
# 3zd. Delete votes
|
||
logger.info("\n🗑️ Deleting marketplace.votes...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM marketplace.votes WHERE user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ marketplace.votes: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ marketplace.votes: {e}")
|
||
|
||
# 3ze. Delete user_scores_deprecated
|
||
logger.info("\n🗑️ Deleting user_scores_deprecated...")
|
||
try:
|
||
r = await db.execute(text(f"DELETE FROM system.user_scores_deprecated WHERE user_id IN ({target_ids_str})"))
|
||
logger.info(f" ✅ system.user_scores_deprecated: {r.rowcount} rows deleted")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ system.user_scores_deprecated: {e}")
|
||
|
||
# 4. Before deleting users, NULL out person.user_id references
|
||
# (identity.persons.user_id -> identity.users.id has NO ACTION on delete)
|
||
logger.info("\n🔗 NULLing person.user_id references...")
|
||
try:
|
||
r = await db.execute(text(f"""
|
||
UPDATE identity.persons
|
||
SET user_id = NULL
|
||
WHERE user_id IN ({target_ids_str})
|
||
"""))
|
||
logger.info(f" ✅ identity.persons.user_id: {r.rowcount} rows NULLed")
|
||
except Exception as e:
|
||
logger.warning(f" ⚠️ identity.persons.user_id: {e}")
|
||
|
||
# 5. Finally, delete the users themselves
|
||
logger.info("\n💀 Deleting users...")
|
||
r = await db.execute(text(f"DELETE FROM identity.users WHERE id IN ({target_ids_str}) RETURNING id, email"))
|
||
deleted_users = r.fetchall()
|
||
logger.info(f" ✅ Deleted {len(deleted_users)} users:")
|
||
for uid, email in deleted_users:
|
||
logger.info(f" - ID={uid}: {email}")
|
||
|
||
# 5. Handle orphaned persons (soft-delete them)
|
||
if person_ids:
|
||
logger.info("\n👤 Handling orphaned persons...")
|
||
for pid in person_ids:
|
||
# Check if person is still referenced by any remaining user
|
||
check_sql = text("SELECT COUNT(*) FROM identity.users WHERE person_id = :pid AND is_deleted = false")
|
||
ref_count = (await db.execute(check_sql, {"pid": pid})).scalar()
|
||
if ref_count == 0:
|
||
logger.info(f" 👤 Soft-deleting orphaned person ID={pid}")
|
||
await db.execute(text("""
|
||
UPDATE identity.persons
|
||
SET is_active = false, is_ghost = true, deleted_at = :now
|
||
WHERE id = :pid
|
||
"""), {"now": datetime.now(timezone.utc), "pid": pid})
|
||
else:
|
||
logger.info(f" ℹ️ Person ID={pid} still referenced by {ref_count} active user(s), keeping")
|
||
|
||
await db.commit()
|
||
|
||
return {
|
||
"deleted_users": len(deleted_users),
|
||
"target_ids": target_ids,
|
||
}
|
||
|
||
|
||
# =====================================================================
|
||
# PHASE 2: RESTORE & RENAME GARAGES
|
||
# =====================================================================
|
||
|
||
async def phase_2_restore_garages(db: AsyncSession) -> dict:
|
||
"""
|
||
After the purge, iterate through ALL surviving users (Protected List + Admins).
|
||
For each user:
|
||
- If they have NO private garage: Create one with the new naming convention.
|
||
- If they HAVE a private garage but wrong name: Rename it.
|
||
"""
|
||
logger.info("\n" + "=" * 60)
|
||
logger.info("PHASE 2: RESTORE & RENAME GARAGES")
|
||
logger.info("=" * 60)
|
||
|
||
# Get all surviving users (active, not deleted)
|
||
protected_ids_str = ", ".join(str(x) for x in PROTECTED_USER_IDS)
|
||
protected_roles_str = ", ".join(f"'{r}'" for r in PROTECTED_ROLES)
|
||
|
||
survivors_sql = f"""
|
||
SELECT u.id, u.email, u.role, u.person_id,
|
||
p.last_name, p.first_name
|
||
FROM identity.users u
|
||
LEFT JOIN identity.persons p ON p.id = u.person_id
|
||
WHERE u.is_deleted = false
|
||
AND (u.id IN ({protected_ids_str})
|
||
OR u.role IN ({protected_roles_str}))
|
||
ORDER BY u.id
|
||
"""
|
||
result = await db.execute(text(survivors_sql))
|
||
survivors = result.fetchall()
|
||
|
||
logger.info(f"🔍 Found {len(survivors)} surviving users to process:")
|
||
|
||
garages_created = 0
|
||
garages_renamed = 0
|
||
garages_ok = 0
|
||
skipped_no_person = 0
|
||
|
||
for row in survivors:
|
||
user_id = row[0]
|
||
email = row[1]
|
||
role = row[2]
|
||
person_id = row[3]
|
||
last_name = row[4] or "Unknown"
|
||
first_name = row[5] or "User"
|
||
|
||
logger.info(f"\n--- Processing User ID={user_id}: {email} (role={role}) ---")
|
||
|
||
if not person_id:
|
||
logger.warning(f" ⚠️ User ID={user_id} has no person record! Skipping garage creation.")
|
||
skipped_no_person += 1
|
||
continue
|
||
|
||
# Generate the new naming convention
|
||
new_garage_name = f"{last_name} {first_name} - Privát Garázs (#{user_id})"
|
||
|
||
# Find existing private garages owned by this user
|
||
org_sql = text("""
|
||
SELECT id, name, full_name, org_type
|
||
FROM fleet.organizations
|
||
WHERE owner_id = :uid
|
||
AND org_type = 'individual'
|
||
AND is_deleted = false
|
||
ORDER BY id
|
||
""")
|
||
org_result = await db.execute(org_sql, {"uid": user_id})
|
||
existing_orgs = org_result.fetchall()
|
||
|
||
if not existing_orgs:
|
||
# No private garage exists — create one
|
||
logger.info(f" 🏗️ No private garage found. Creating: '{new_garage_name}'")
|
||
|
||
# Generate a secure slug
|
||
import hashlib, uuid
|
||
folder_slug = hashlib.md5(f"{user_id}-{uuid.uuid4()}".encode()).hexdigest()[:12]
|
||
|
||
# Create the organization
|
||
insert_org_sql = text("""
|
||
INSERT INTO fleet.organizations
|
||
(full_name, name, display_name, folder_slug, org_type,
|
||
owner_id, is_active, status, country_code,
|
||
first_registered_at, current_lifecycle_started_at,
|
||
created_at, subscription_plan, base_asset_limit,
|
||
purchased_extra_slots, notification_settings,
|
||
external_integration_config, is_ownership_transferable,
|
||
subscription_tier_id)
|
||
VALUES
|
||
(:fname, :fname, :fname, :slug, 'individual',
|
||
:owner, true, 'verified', 'HU',
|
||
:now, :now, :now, 'FREE', 1, 0,
|
||
'{}'::jsonb, '{}'::jsonb, true,
|
||
:tier_id)
|
||
RETURNING id
|
||
""")
|
||
org_insert_result = await db.execute(insert_org_sql, {
|
||
"fname": new_garage_name,
|
||
"slug": folder_slug,
|
||
"owner": user_id,
|
||
"now": datetime.now(timezone.utc),
|
||
"tier_id": PRIVATE_FREE_TIER_ID,
|
||
})
|
||
new_org_id = org_insert_result.scalar()
|
||
logger.info(f" ✅ Created organization ID={new_org_id}")
|
||
|
||
# Create a branch (must provide UUID id since raw SQL bypasses ORM default)
|
||
branch_id = uuid.uuid4()
|
||
await db.execute(text("""
|
||
INSERT INTO fleet.branches
|
||
(id, organization_id, name, is_main, status, branch_rating, is_deleted, created_at)
|
||
VALUES (:bid, :oid, 'Home Base', true, 'active', 0.0, false, :now)
|
||
"""), {"bid": branch_id, "oid": new_org_id, "now": datetime.now(timezone.utc)})
|
||
|
||
# Create organization member (OWNER)
|
||
await db.execute(text("""
|
||
INSERT INTO fleet.organization_members
|
||
(organization_id, user_id, person_id, role,
|
||
is_permanent, is_verified, status, created_at)
|
||
VALUES (:oid, :uid, :pid, 'OWNER',
|
||
true, true, 'active', :now)
|
||
"""), {"oid": new_org_id, "uid": user_id, "pid": person_id, "now": datetime.now(timezone.utc)})
|
||
|
||
# Update user's scope_id
|
||
await db.execute(text("""
|
||
UPDATE identity.users
|
||
SET scope_id = :scope
|
||
WHERE id = :uid AND (scope_id IS NULL OR scope_id = '')
|
||
"""), {"scope": str(new_org_id), "uid": user_id})
|
||
|
||
garages_created += 1
|
||
|
||
else:
|
||
# Garage exists — check/rename
|
||
for org_id, org_name, org_full_name, org_type in existing_orgs:
|
||
if org_full_name != new_garage_name:
|
||
logger.info(f" 🔄 Renaming org ID={org_id}: '{org_full_name}' -> '{new_garage_name}'")
|
||
await db.execute(text("""
|
||
UPDATE fleet.organizations
|
||
SET full_name = :new_name, name = :new_name
|
||
WHERE id = :oid
|
||
"""), {"new_name": new_garage_name, "oid": org_id})
|
||
garages_renamed += 1
|
||
else:
|
||
logger.info(f" ✅ Org ID={org_id} already has correct name: '{org_full_name}'")
|
||
garages_ok += 1
|
||
|
||
await db.commit()
|
||
|
||
return {
|
||
"survivors_count": len(survivors),
|
||
"garages_created": garages_created,
|
||
"garages_renamed": garages_renamed,
|
||
"garages_ok": garages_ok,
|
||
"skipped_no_person": skipped_no_person,
|
||
}
|
||
|
||
|
||
# =====================================================================
|
||
# MAIN
|
||
# =====================================================================
|
||
|
||
async def system_reset():
|
||
"""Main execution: Phase 1 (Purge) + Phase 2 (Restore)."""
|
||
engine = create_async_engine(DATABASE_URL, echo=False)
|
||
|
||
async with AsyncSession(engine) as db:
|
||
try:
|
||
# ── Phase 1: Purge ──
|
||
purge_result = await phase_1_purge_users(db)
|
||
|
||
# ── Phase 2: Restore ──
|
||
restore_result = await phase_2_restore_garages(db)
|
||
|
||
# ── Final Report ──
|
||
logger.info("\n" + "=" * 60)
|
||
logger.info("📋 FINAL SYSTEM RESET REPORT")
|
||
logger.info("=" * 60)
|
||
|
||
logger.info(f"\n💀 PHASE 1 — USER PURGE:")
|
||
logger.info(f" Users deleted: {purge_result['deleted_users']}")
|
||
|
||
logger.info(f"\n🏪 PHASE 2 — GARAGE RESTITUTION:")
|
||
logger.info(f" Survivors processed: {restore_result['survivors_count']}")
|
||
logger.info(f" Garages created: {restore_result['garages_created']}")
|
||
logger.info(f" Garages renamed: {restore_result['garages_renamed']}")
|
||
logger.info(f" Garages already correct: {restore_result['garages_ok']}")
|
||
logger.info(f" Skipped (no person record): {restore_result['skipped_no_person']}")
|
||
|
||
logger.info(f"\n✅ System reset completed successfully!")
|
||
|
||
return {
|
||
"phase_1": purge_result,
|
||
"phase_2": restore_result,
|
||
}
|
||
|
||
except Exception as e:
|
||
await db.rollback()
|
||
logger.error(f"💥 Fatal error during system reset: {e}", exc_info=True)
|
||
sys.exit(1)
|
||
|
||
|
||
async def main():
|
||
logger.info("🚀 P0 SYSTEM RESET — Deep User Purge & Private Garage Restitution")
|
||
logger.info(f" Protected User IDs: {PROTECTED_USER_IDS}")
|
||
logger.info(f" Protected Roles: {PROTECTED_ROLES}")
|
||
logger.info(f" Database: {DATABASE_URL}")
|
||
logger.info("")
|
||
|
||
try:
|
||
await system_reset()
|
||
except Exception as e:
|
||
logger.error(f"💥 Fatal error: {e}")
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(main())
|