"""Migrate guest_users to regular users with role='guest' and drop guest tables. ⚠️ Guest-System umgebaut — Guests sind jetzt reguläre User mit role=guest This migration: 1. Creates User records for each guest (or links to existing users by email) 2. Creates UserTenant records with role='guest' and appropriate status 3. Drops guest_invitations and guest_users tables After this migration, guests authenticate via the normal login flow and are managed through the regular user system with role='guest' in user_tenants. Revision ID: 0113 Revises: 0112 """ from __future__ import annotations from alembic import op revision = "0113" down_revision = "0112" branch_labels = None depends_on = None def upgrade() -> None: # Migrate guest_users into users + user_tenants with role='guest' op.execute(""" DO $$ DECLARE guest_rec RECORD; existing_user_id UUID; new_user_id UUID; mapped_status TEXT; BEGIN FOR guest_rec IN SELECT * FROM guest_users LOOP -- Map guest status to user_tenants status mapped_status := CASE WHEN guest_rec.status = 'active' THEN 'active' WHEN guest_rec.status = 'invited' THEN 'invited' WHEN guest_rec.status = 'expired' THEN 'disabled' WHEN guest_rec.status = 'revoked' THEN 'disabled' ELSE 'disabled' END; -- Check if a user with this email already exists SELECT id INTO existing_user_id FROM users WHERE email = guest_rec.email LIMIT 1; IF existing_user_id IS NOT NULL THEN -- User already exists — just create the tenant membership if missing new_user_id := existing_user_id; -- Check if user_tenants entry already exists for this user+tenant IF NOT EXISTS ( SELECT 1 FROM user_tenants WHERE user_id = new_user_id AND tenant_id = guest_rec.tenant_id ) THEN INSERT INTO user_tenants (user_id, tenant_id, is_default, role, status, created_at, updated_at) VALUES ( new_user_id, guest_rec.tenant_id, false, 'guest', mapped_status, guest_rec.created_at, guest_rec.updated_at ); END IF; ELSE -- Create new user from guest record INSERT INTO users (id, email, name, password_hash, is_active, preferences, is_system_admin, created_at, updated_at) VALUES ( gen_random_uuid(), guest_rec.email, guest_rec.name, COALESCE(guest_rec.password_hash, ''), true, '{}'::jsonb, false, guest_rec.created_at, guest_rec.updated_at ) RETURNING id INTO new_user_id; -- Create user_tenants membership with guest role INSERT INTO user_tenants (user_id, tenant_id, is_default, role, status, created_at, updated_at) VALUES ( new_user_id, guest_rec.tenant_id, false, 'guest', mapped_status, guest_rec.created_at, guest_rec.updated_at ); END IF; END LOOP; END $$; """) # Drop guest tables op.execute("DROP TABLE IF EXISTS guest_invitations CASCADE") op.execute("DROP TABLE IF EXISTS guest_users CASCADE") def downgrade() -> None: # Recreate guest_users table (data is lost — this is a one-way migration) op.execute(""" CREATE TABLE IF NOT EXISTS guest_users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, password_hash VARCHAR(255), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, invited_by UUID REFERENCES users(id) ON DELETE SET NULL, status VARCHAR(20) NOT NULL DEFAULT 'invited', expires_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """) op.execute(""" CREATE TABLE IF NOT EXISTS guest_invitations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), guest_user_id UUID NOT NULL REFERENCES guest_users(id) ON DELETE CASCADE, token_hash VARCHAR(64) NOT NULL UNIQUE, expires_at TIMESTAMPTZ NOT NULL, used_at TIMESTAMPTZ, revoked_at TIMESTAMPTZ, created_by UUID REFERENCES users(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """)