← back to Grant

scripts/seed-admin-user.mjs

80 lines

#!/usr/bin/env node
/**
 * Seed an admin-level user account for the Grant app.
 *
 * Idempotent: upserts on email. Ties the row to an organization (defaults to
 * the oldest org, matching resolveOrgId's deterministic fallback in lib/auth.ts).
 *
 * Usage:
 *   node scripts/seed-admin-user.mjs
 *   ADMIN_EMAIL=admin@studentdebtcrisis.org ADMIN_NAME="Site Admin" ADMIN_ROLE=admin node scripts/seed-admin-user.mjs
 *   ORG_ID=<uuid> node scripts/seed-admin-user.mjs
 *
 * Reads DATABASE_URL from the environment (or .env.local).
 */
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import pg from 'pg';

const __dirname = dirname(fileURLToPath(import.meta.url));

// Minimal .env.local loader (only if DATABASE_URL not already set).
if (!process.env.DATABASE_URL) {
  try {
    const env = readFileSync(join(__dirname, '..', '.env.local'), 'utf8');
    for (const line of env.split('\n')) {
      const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
      if (m && !process.env[m[1]]) process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
    }
  } catch { /* no .env.local — rely on real env */ }
}

const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@studentdebtcrisis.org';
const ADMIN_NAME  = process.env.ADMIN_NAME  || 'Site Admin';
const ADMIN_ROLE  = process.env.ADMIN_ROLE  || 'admin'; // owner | admin | member

if (!['owner', 'admin', 'member'].includes(ADMIN_ROLE)) {
  console.error(`[seed-admin] ADMIN_ROLE must be owner|admin|member, got "${ADMIN_ROLE}"`);
  process.exit(1);
}
if (!process.env.DATABASE_URL) {
  console.error('[seed-admin] DATABASE_URL not set');
  process.exit(1);
}

const { Pool } = pg;
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

const run = async () => {
  // Resolve target org: explicit ORG_ID, else oldest org.
  let orgId = process.env.ORG_ID;
  if (!orgId) {
    const r = await pool.query('SELECT id FROM organizations ORDER BY created_at ASC LIMIT 1');
    if (r.rows.length === 0) {
      console.error('[seed-admin] No organizations exist yet — create the org first.');
      process.exit(1);
    }
    orgId = r.rows[0].id;
  }

  const res = await pool.query(
    `INSERT INTO users (email, display_name, role, org_id, last_login_at)
       VALUES ($1, $2, $3, $4, NOW())
     ON CONFLICT (email) DO UPDATE
       SET display_name = EXCLUDED.display_name,
           role         = EXCLUDED.role,
           org_id       = EXCLUDED.org_id,
           updated_at   = NOW()
     RETURNING id, email, display_name, role, org_id`,
    [ADMIN_EMAIL, ADMIN_NAME, ADMIN_ROLE, orgId],
  );
  const u = res.rows[0];
  console.log(`[seed-admin] ✓ ${u.role} account ready: ${u.email} (${u.display_name})`);
  console.log(`[seed-admin]   id=${u.id} org_id=${u.org_id}`);
};

run()
  .catch((e) => { console.error('[seed-admin] FAILED:', e.message); process.exit(1); })
  .finally(() => pool.end());