/** * Idempotent migration for the admin dashboard: adds the Better Auth admin * plugin columns (user.role/banned/ban_reason/ban_expires, session.impersonated_by) * and the admin_audit_log table. Safe to run multiple times. * * Run: npx tsx scripts/migrate-admin.ts */ import { config } from "dotenv" config({ path: ".env.local" }) process.env.DATABASE_SSL = process.env.DATABASE_SSL ?? "disable" const SQL = ` ALTER TABLE "user" ADD COLUMN IF NOT EXISTS "role" text DEFAULT 'user'; ALTER TABLE "user" ADD COLUMN IF NOT EXISTS "banned" boolean DEFAULT false; ALTER TABLE "user" ADD COLUMN IF NOT EXISTS "ban_reason" text; ALTER TABLE "user" ADD COLUMN IF NOT EXISTS "ban_expires" timestamp; ALTER TABLE "session" ADD COLUMN IF NOT EXISTS "impersonated_by" text; CREATE TABLE IF NOT EXISTS "admin_audit_log" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "admin_id" text, "action" text NOT NULL, "target_user_id" text, "metadata" jsonb NOT NULL DEFAULT '{}'::jsonb, "ip_address" text, "created_at" timestamptz NOT NULL DEFAULT now() ); DO $$ BEGIN ALTER TABLE "admin_audit_log" ADD CONSTRAINT "admin_audit_log_admin_id_user_id_fk" FOREIGN KEY ("admin_id") REFERENCES "user"("id") ON DELETE SET NULL; EXCEPTION WHEN duplicate_object THEN NULL; END $$; CREATE INDEX IF NOT EXISTS "admin_audit_log_created_at_idx" ON "admin_audit_log" ("created_at" DESC); CREATE INDEX IF NOT EXISTS "admin_audit_log_target_idx" ON "admin_audit_log" ("target_user_id"); ` async function main() { const { pool } = await import("../lib/db") await pool.query(SQL) const cols = await pool.query( `select table_name, column_name from information_schema.columns where (table_name='user' and column_name in ('role','banned','ban_reason','ban_expires')) or (table_name='session' and column_name='impersonated_by') order by table_name, column_name` ) console.log("Applied admin migration. New columns present:") for (const r of cols.rows) console.log(` ${r.table_name}.${r.column_name}`) const t = await pool.query(`select to_regclass('public.admin_audit_log') as t`) console.log(` admin_audit_log table: ${t.rows[0].t ? "OK" : "MISSING"}`) await pool.end() } main().catch((e) => { console.error("Migration failed:", e) process.exit(1) })