BEGIN; ALTER TABLE app_user ADD COLUMN IF NOT EXISTS role TEXT NOT NULL DEFAULT 'USER'; CREATE INDEX IF NOT EXISTS idx_app_user_role ON app_user(role); DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_app_user_role') THEN ALTER TABLE app_user ADD CONSTRAINT chk_app_user_role CHECK (role IN ('USER','ADMIN','SUPER_ADMIN')); END IF; END $$; UPDATE app_user SET role = 'SUPER_ADMIN' WHERE role <> 'SUPER_ADMIN' AND COALESCE(is_super_admin, false) = true; UPDATE app_user SET role = 'ADMIN' WHERE role = 'USER' AND COALESCE(is_admin, false) = true; DO $$ BEGIN IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'app_user' AND column_name = 'is_admin' ) THEN UPDATE app_user SET is_admin = (role IN ('ADMIN','SUPER_ADMIN')) WHERE is_admin IS DISTINCT FROM (role IN ('ADMIN','SUPER_ADMIN')); END IF; IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'app_user' AND column_name = 'is_super_admin' ) THEN UPDATE app_user SET is_super_admin = (role = 'SUPER_ADMIN') WHERE is_super_admin IS DISTINCT FROM (role = 'SUPER_ADMIN'); END IF; END $$; CREATE TABLE IF NOT EXISTS admin_role_audit ( id BIGSERIAL PRIMARY KEY, actor_user_id TEXT NOT NULL, target_user_id TEXT NOT NULL, old_role TEXT NOT NULL, new_role TEXT NOT NULL, changed_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE OR REPLACE FUNCTION prevent_super_admin_delete() RETURNS trigger AS $$ BEGIN IF OLD.role = 'SUPER_ADMIN' OR OLD.is_super_admin THEN RAISE EXCEPTION 'cannot delete super admin user'; END IF; RETURN OLD; END; $$ LANGUAGE plpgsql; DROP VIEW IF EXISTS admin_user_metrics; CREATE OR REPLACE VIEW admin_user_metrics AS WITH session_stats AS ( SELECT user_id, MIN(created_at) AS first_session_at, MAX(COALESCE(last_seen_at, created_at)) AS last_login_at FROM app_session GROUP BY user_id ), run_stats AS ( SELECT user_id, COUNT(*) AS runs_count, MAX(CASE WHEN status = 'RUNNING' THEN run_id END) AS active_run_id, MAX(CASE WHEN status = 'RUNNING' THEN status END) AS active_run_status, MIN(created_at) AS first_run_at FROM strategy_run GROUP BY user_id ), broker_stats AS ( SELECT user_id, BOOL_OR(connected) AS broker_connected FROM user_broker GROUP BY user_id ) SELECT u.id AS user_id, u.username, u.role, (u.role IN ('ADMIN','SUPER_ADMIN')) AS is_admin, COALESCE(session_stats.first_session_at, run_stats.first_run_at) AS created_at, session_stats.last_login_at, COALESCE(run_stats.runs_count, 0) AS runs_count, run_stats.active_run_id, run_stats.active_run_status, COALESCE(broker_stats.broker_connected, FALSE) AS broker_connected FROM app_user u LEFT JOIN session_stats ON session_stats.user_id = u.id LEFT JOIN run_stats ON run_stats.user_id = u.id LEFT JOIN broker_stats ON broker_stats.user_id = u.id; COMMIT;