/** * Seeds the admin user into the SQLite database. * Run once after first startup: * node scripts/seed-admin.mjs * * Uses only node:sqlite (built-in) — no external dependencies. * Runs pending migrations before inserting so it is safe to call * before or after the server first starts. */ import { DatabaseSync } from "node:sqlite"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import { createHash } from "crypto"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const DB_PATH = process.env.DB_PATH || path.join(__dirname, "../artifacts/api-server/raqim.db"); const MIGRATIONS_DIR = process.env.MIGRATIONS_DIR || path.join(__dirname, "../lib/db/drizzle"); const ADMIN_EMAIL = (process.env.ADMIN_EMAIL || "admin@raqim.app").toLowerCase(); const ADMIN_NAME = process.env.ADMIN_NAME || "Admin"; // bcrypt hash for "Admin1234!" cost=12 // Override via ADMIN_PASSWORD_HASH env var. const ADMIN_PASSWORD_HASH = process.env.ADMIN_PASSWORD_HASH || "$2b$12$6PwSkmxfTjxyQYCXBcKvA.CqVVHL.b.Kz29McI5txRPlwutiMp4ZS"; console.log("[seed] DB:", DB_PATH); console.log("[seed] Email:", ADMIN_EMAIL); const db = new DatabaseSync(DB_PATH); db.exec("PRAGMA journal_mode = WAL"); db.exec("PRAGMA foreign_keys = ON"); // ── Run pending migrations so tables exist ──────────────────────────────── db.exec(`CREATE TABLE IF NOT EXISTS __drizzle_migrations ( id INTEGER PRIMARY KEY AUTOINCREMENT, hash TEXT NOT NULL UNIQUE, applied_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) )`); if (fs.existsSync(MIGRATIONS_DIR)) { const sqlFiles = fs .readdirSync(MIGRATIONS_DIR) .filter((f) => f.endsWith(".sql")) .sort(); for (const file of sqlFiles) { const hash = file.replace(".sql", ""); const already = db .prepare("SELECT 1 FROM __drizzle_migrations WHERE hash = ?") .get(hash); if (already) continue; const sql = fs.readFileSync(path.join(MIGRATIONS_DIR, file), "utf-8"); const stmts = sql .split("--> statement-breakpoint") .map((s) => s.trim()) .filter(Boolean); for (const stmt of stmts) db.exec(stmt); db.prepare("INSERT INTO __drizzle_migrations (hash) VALUES (?)").run(hash); console.log("[seed] Applied migration:", file); } } /** * Derives a stable UUID v4-shaped ID from the admin email. * Same email → same UUID every time, across container restarts. * This prevents FK violations when the DB is wiped and re-seeded. */ function emailToUUID(email) { const h = createHash("sha256").update("raqim:admin:" + email).digest("hex"); return [ h.slice(0, 8), h.slice(8, 12), "4" + h.slice(13, 16), (parseInt(h[16], 16) & 0x3 | 0x8).toString(16) + h.slice(17, 20), h.slice(20, 32), ].join("-"); } const ADMIN_ID = process.env.ADMIN_ID || emailToUUID(ADMIN_EMAIL); console.log("[seed] Admin UUID:", ADMIN_ID); // ── Insert admin user ───────────────────────────────────────────────────── const existing = db .prepare("SELECT id FROM users WHERE email = ?") .get(ADMIN_EMAIL); if (existing) { // If the admin exists but with a DIFFERENT id (old random UUID), // update it to the deterministic one so future tokens work correctly. if (existing.id !== ADMIN_ID) { console.log("[seed] Updating admin UUID from", existing.id, "→", ADMIN_ID); db.prepare("UPDATE users SET id = ?, updated_at = unixepoch() * 1000 WHERE email = ?") .run(ADMIN_ID, ADMIN_EMAIL); console.log("[seed] ✓ Admin UUID updated."); } else { console.log("[seed] Admin already exists with correct UUID — skipping."); } process.exit(0); } db.prepare(` INSERT INTO users (id, email, password_hash, display_name, role, status, created_at, updated_at) VALUES (?, ?, ?, ?, 'admin', 'active', unixepoch() * 1000, unixepoch() * 1000) `).run(ADMIN_ID, ADMIN_EMAIL, ADMIN_PASSWORD_HASH, ADMIN_NAME); console.log("[seed] ✓ Admin user created:", ADMIN_EMAIL, "id:", ADMIN_ID);