raqim / scripts /seed-admin.mjs
RAQIM Deploy
Deploy RAQIM 2026-05-02 19:53
6ec2824
/**
* 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);