RABC fejlesztése és beélesítése hibák kijavításával
This commit is contained in:
306
backend/app/scripts/migrate_legacy_garages.py
Normal file
306
backend/app/scripts/migrate_legacy_garages.py
Normal file
@@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
P0 CRITICAL - The Great Garage & Vehicle Migration (Legacy Data Cleanup)
|
||||
|
||||
Migrates legacy person_id-based ownership to the new organization_id / organization_members
|
||||
RBAC structure. Ensures every user has a personal garage (INDIVIDUAL org), is bound as OWNER
|
||||
in organization_members, and orphaned vehicles are rescued into their owner's garage.
|
||||
|
||||
TASKS:
|
||||
A) Ensure Personal Garages exist for every user who lacks one
|
||||
B) Bind users to their garages via organization_members (OWNER role) + update scope_id
|
||||
C) Rescue orphaned vehicles (owner_person_id set but no current_organization_id/owner_org_id)
|
||||
|
||||
Usage:
|
||||
docker exec sf_api python3 /app/app/scripts/migrate_legacy_garages.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import asyncpg
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# asyncpg needs plain "postgresql://" scheme, not "postgresql+asyncpg://"
|
||||
_raw_url = os.getenv(
|
||||
"DATABASE_URL",
|
||||
"postgresql://service_finder_app:AppSafePass_2026@db:5432/service_finder"
|
||||
)
|
||||
DATABASE_URL = _raw_url.replace("+asyncpg", "")
|
||||
|
||||
|
||||
async def main():
|
||||
print("=" * 70)
|
||||
print("🔧 P0 MIGRATION: THE GREAT GARAGE & VEHICLE MIGRATION")
|
||||
print("=" * 70)
|
||||
print(f"Started at: {datetime.now(timezone.utc).isoformat()}")
|
||||
print()
|
||||
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
|
||||
try:
|
||||
# ── TASK A: ENSURE PERSONAL GARAGES EXIST ──
|
||||
print("📋 TASK A: Ensure Personal Garages Exist")
|
||||
print("-" * 50)
|
||||
|
||||
# Find all users who do NOT have an INDIVIDUAL-type organization where they are owner
|
||||
users_without_garage = await conn.fetch("""
|
||||
SELECT u.id AS user_id,
|
||||
u.email,
|
||||
p.id AS person_id,
|
||||
p.first_name,
|
||||
p.last_name
|
||||
FROM identity.users u
|
||||
JOIN identity.persons p ON p.id = u.person_id
|
||||
WHERE u.is_deleted = false
|
||||
AND u.is_active = true
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM fleet.organizations o
|
||||
WHERE o.owner_id = u.id
|
||||
AND o.org_type = 'individual'
|
||||
AND o.is_deleted = false
|
||||
)
|
||||
ORDER BY u.id
|
||||
""")
|
||||
|
||||
print(f" Found {len(users_without_garage)} users without a personal garage.")
|
||||
garages_created = 0
|
||||
|
||||
for row in users_without_garage:
|
||||
user_id = row['user_id']
|
||||
person_id = row['person_id']
|
||||
first_name = row['first_name'] or "Felhasználó"
|
||||
last_name = row['last_name'] or ""
|
||||
|
||||
garage_name = f"{first_name} Garázsa"
|
||||
folder_slug = f"personal-{user_id}-{person_id}"
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Direct INSERT with all NOT NULL columns explicitly provided
|
||||
await conn.execute("""
|
||||
INSERT INTO fleet.organizations
|
||||
(full_name, name, display_name, folder_slug, org_type,
|
||||
owner_id, status, is_active, is_deleted,
|
||||
subscription_plan, base_asset_limit, purchased_extra_slots,
|
||||
notification_settings, external_integration_config,
|
||||
is_ownership_transferable, is_verified,
|
||||
first_registered_at, current_lifecycle_started_at,
|
||||
lifecycle_index, created_at,
|
||||
visual_settings, aliases, tags, settings)
|
||||
VALUES
|
||||
($1, $2, $3, $4, 'individual',
|
||||
$5, 'active', true, false,
|
||||
'FREE', 5, 0,
|
||||
$6::json, '{}'::json,
|
||||
true, false,
|
||||
$7, $8,
|
||||
1, $9,
|
||||
$10::jsonb, '[]'::jsonb, '[]'::jsonb, $11::jsonb)
|
||||
""",
|
||||
garage_name, # $1 full_name
|
||||
garage_name, # $2 name
|
||||
garage_name, # $3 display_name
|
||||
folder_slug, # $4 folder_slug
|
||||
user_id, # $5 owner_id
|
||||
json.dumps({"notify_owner": True, "alert_days_before": [30, 15, 7, 1]}), # $6 notification_settings
|
||||
now, # $7 first_registered_at
|
||||
now, # $8 current_lifecycle_started_at
|
||||
now, # $9 created_at
|
||||
json.dumps({"theme": "default", "primary_color": None, "wall_logo_url": None}), # $10 visual_settings
|
||||
json.dumps({"invite_expiry_days": 7, "max_members": 50, "allow_public_join": False}), # $11 settings
|
||||
)
|
||||
|
||||
garages_created += 1
|
||||
print(f" ✅ Created personal garage '{garage_name}' for user_id={user_id} ({row['email']})")
|
||||
|
||||
print(f"\n 📊 Task A Result: {garages_created} personal garages created.")
|
||||
print()
|
||||
|
||||
# ── TASK B: BIND USERS TO GARAGES (RBAC) ──
|
||||
print("📋 TASK B: Bind Users to Garages (RBAC)")
|
||||
print("-" * 50)
|
||||
|
||||
# Get all active users with their personal garages
|
||||
users_and_garages = await conn.fetch("""
|
||||
SELECT u.id AS user_id,
|
||||
u.email,
|
||||
u.scope_id,
|
||||
o.id AS org_id,
|
||||
o.name AS org_name
|
||||
FROM identity.users u
|
||||
JOIN fleet.organizations o ON o.owner_id = u.id
|
||||
AND o.org_type = 'individual'
|
||||
AND o.is_deleted = false
|
||||
WHERE u.is_deleted = false
|
||||
AND u.is_active = true
|
||||
ORDER BY u.id
|
||||
""")
|
||||
|
||||
members_added = 0
|
||||
scopes_updated = 0
|
||||
|
||||
for row in users_and_garages:
|
||||
user_id = row['user_id']
|
||||
org_id = row['org_id']
|
||||
scope_id = row['scope_id']
|
||||
|
||||
# Check if user is already a member of this org
|
||||
existing_member = await conn.fetchrow("""
|
||||
SELECT id FROM fleet.organization_members
|
||||
WHERE organization_id = $1 AND user_id = $2
|
||||
""", org_id, user_id)
|
||||
|
||||
if not existing_member:
|
||||
await conn.execute("""
|
||||
INSERT INTO fleet.organization_members
|
||||
(organization_id, user_id, role, status,
|
||||
is_permanent, is_verified, created_at)
|
||||
VALUES
|
||||
($1, $2, 'OWNER', 'active',
|
||||
true, true, NOW())
|
||||
""", org_id, user_id)
|
||||
members_added += 1
|
||||
print(f" ✅ Added user_id={user_id} as OWNER of org_id={org_id} ({row['org_name']})")
|
||||
else:
|
||||
# Ensure role is OWNER
|
||||
await conn.execute("""
|
||||
UPDATE fleet.organization_members
|
||||
SET role = 'OWNER', is_permanent = true, is_verified = true, status = 'active'
|
||||
WHERE organization_id = $1 AND user_id = $2
|
||||
""", org_id, user_id)
|
||||
print(f" ℹ️ Updated membership for user_id={user_id} in org_id={org_id} to OWNER")
|
||||
|
||||
# Update scope_id if NULL
|
||||
if scope_id is None or scope_id == "":
|
||||
await conn.execute("""
|
||||
UPDATE identity.users
|
||||
SET scope_id = $1::text
|
||||
WHERE id = $2
|
||||
""", str(org_id), user_id)
|
||||
scopes_updated += 1
|
||||
print(f" ✅ Updated scope_id for user_id={user_id} to '{org_id}'")
|
||||
|
||||
print(f"\n 📊 Task B Result: {members_added} members added, {scopes_updated} scope_ids updated.")
|
||||
print()
|
||||
|
||||
# ── TASK C: RESCUE ORPHANED VEHICLES ──
|
||||
print("📋 TASK C: Rescue Orphaned Vehicles")
|
||||
print("-" * 50)
|
||||
|
||||
# Find orphaned vehicles: have owner_person_id but no current_organization_id or owner_org_id
|
||||
orphaned_vehicles = await conn.fetch("""
|
||||
SELECT a.id AS asset_id,
|
||||
a.license_plate,
|
||||
a.vin,
|
||||
a.owner_person_id,
|
||||
a.current_organization_id,
|
||||
a.owner_org_id,
|
||||
a.brand,
|
||||
a.model
|
||||
FROM vehicle.assets a
|
||||
WHERE a.owner_person_id IS NOT NULL
|
||||
AND (a.current_organization_id IS NULL OR a.owner_org_id IS NULL)
|
||||
AND a.status != 'deleted'
|
||||
ORDER BY a.owner_person_id, a.id
|
||||
""")
|
||||
|
||||
print(f" Found {len(orphaned_vehicles)} orphaned vehicles.")
|
||||
|
||||
vehicles_rescued = 0
|
||||
vehicles_skipped = 0
|
||||
|
||||
for vehicle in orphaned_vehicles:
|
||||
owner_person_id = vehicle['owner_person_id']
|
||||
asset_id = vehicle['asset_id']
|
||||
plate = vehicle['license_plate'] or vehicle['vin'] or str(asset_id)
|
||||
|
||||
# Find the user who owns this person record
|
||||
user_row = await conn.fetchrow("""
|
||||
SELECT u.id AS user_id
|
||||
FROM identity.users u
|
||||
WHERE u.person_id = $1
|
||||
AND u.is_deleted = false
|
||||
AND u.is_active = true
|
||||
LIMIT 1
|
||||
""", owner_person_id)
|
||||
|
||||
if not user_row:
|
||||
print(f" ⚠️ No active user found for person_id={owner_person_id}, skipping vehicle {plate}")
|
||||
vehicles_skipped += 1
|
||||
continue
|
||||
|
||||
user_id = user_row['user_id']
|
||||
|
||||
# Find the user's personal garage
|
||||
garage = await conn.fetchrow("""
|
||||
SELECT o.id AS org_id
|
||||
FROM fleet.organizations o
|
||||
WHERE o.owner_id = $1
|
||||
AND o.org_type = 'individual'
|
||||
AND o.is_deleted = false
|
||||
LIMIT 1
|
||||
""", user_id)
|
||||
|
||||
if not garage:
|
||||
print(f" ⚠️ No personal garage found for user_id={user_id}, skipping vehicle {plate}")
|
||||
vehicles_skipped += 1
|
||||
continue
|
||||
|
||||
org_id = garage['org_id']
|
||||
|
||||
# Update the vehicle's organization fields
|
||||
updates = []
|
||||
params = []
|
||||
param_idx = 1
|
||||
|
||||
if vehicle['current_organization_id'] is None:
|
||||
updates.append(f"current_organization_id = ${param_idx}")
|
||||
params.append(org_id)
|
||||
param_idx += 1
|
||||
|
||||
if vehicle['owner_org_id'] is None:
|
||||
updates.append(f"owner_org_id = ${param_idx}")
|
||||
params.append(org_id)
|
||||
param_idx += 1
|
||||
|
||||
if updates:
|
||||
params.append(asset_id)
|
||||
update_sql = f"""
|
||||
UPDATE vehicle.assets
|
||||
SET {', '.join(updates)}
|
||||
WHERE id = ${param_idx}::uuid
|
||||
"""
|
||||
await conn.execute(update_sql, *params)
|
||||
vehicles_rescued += 1
|
||||
print(f" ✅ Rescued vehicle {plate} → org_id={org_id} (user_id={user_id})")
|
||||
|
||||
print(f"\n 📊 Task C Result: {vehicles_rescued} vehicles rescued, {vehicles_skipped} skipped.")
|
||||
print()
|
||||
|
||||
# ── FINAL REPORT ──
|
||||
print("=" * 70)
|
||||
print("📊 FINAL MIGRATION REPORT")
|
||||
print("=" * 70)
|
||||
print(f" 🏗️ Task A - New Garages Created: {garages_created}")
|
||||
print(f" 👥 Task B - Members Added: {members_added}")
|
||||
print(f" 🆔 Task B - Scope IDs Updated: {scopes_updated}")
|
||||
print(f" 🚗 Task C - Vehicles Rescued: {vehicles_rescued}")
|
||||
print(f" ⏭️ Task C - Vehicles Skipped: {vehicles_skipped}")
|
||||
print()
|
||||
print(f"Completed at: {datetime.now(timezone.utc).isoformat()}")
|
||||
print("=" * 70)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ FATAL ERROR: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user