Files
CapaKraken/scripts/export-dev-seed.mjs
Hartmut 41eb722369 feat: user invite flow, deactivate/delete, favicon, dashboard loading fix, admin full-width
- Invite flow: admin can invite users by email with role selection; accept-invite page
  sets password and creates the account; 72-hour token expiry; E2E tests
- User deactivate/reactivate/delete: new tRPC procedures + UI buttons; deactivation
  revokes all active sessions immediately; delete cascades vacation/broadcast records;
  isActive field added via migration 20260402000000_user_isactive
- Auth: block login for inactive users with audit entry
- Favicon: SVG favicon + ICO/PNG fallbacks (16, 32, 180, 192, 512px); manifest updated
- Dashboard: GridLayout dynamic-import loading skeleton prevents blank dark area
  on first login before react-grid-layout chunk is cached
- Admin users: remove max-w-5xl constraint so table uses full page width
- Dev: docker container restart workflow documented in LEARNINGS.md; Prisma generate
  must run inside the container after schema changes (named node_modules volume)

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-02 20:19:26 +02:00

163 lines
6.0 KiB
JavaScript

#!/usr/bin/env node
/**
* export-dev-seed.mjs
*
* Dumps the current dev database into packages/db/prisma/dev-seed.sql.
* The dump is safe to commit: passwords, TOTP secrets, SMTP credentials,
* and webhook secrets are all sanitized before writing.
*
* Usage:
* node scripts/export-dev-seed.mjs
*
* Requirements:
* - The capakraken-postgres-1 Docker container must be running
* - DATABASE_URL must point to a local capakraken database
*/
import { execSync, spawnSync } from "node:child_process";
import { writeFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { loadWorkspaceEnv, resolveRealWorkspaceRoot } from "./load-env.mjs";
loadWorkspaceEnv();
const workspaceRoot = resolveRealWorkspaceRoot();
// ── Safety check ─────────────────────────────────────────────────────────────
const rawUrl = process.env["DATABASE_URL"];
if (!rawUrl) {
console.error("❌ DATABASE_URL is not set.");
process.exit(1);
}
let parsedUrl;
try {
parsedUrl = new URL(rawUrl);
} catch {
console.error("❌ DATABASE_URL is not a valid URL.");
process.exit(1);
}
const host = parsedUrl.hostname;
if (!["localhost", "127.0.0.1", "::1"].includes(host)) {
console.error(`❌ Refusing to export from non-local host: ${host}`);
console.error(" export-dev-seed is only for local development databases.");
process.exit(1);
}
// ── Docker container check ────────────────────────────────────────────────────
const CONTAINER = "capakraken-postgres-1";
const containerCheck = spawnSync("docker", ["inspect", "--format={{.State.Running}}", CONTAINER], {
encoding: "utf8",
});
if (containerCheck.stdout.trim() !== "true") {
console.error(`❌ Container ${CONTAINER} is not running.`);
console.error(" Start it with: docker compose up -d postgres");
process.exit(1);
}
// ── Tables to exclude entirely ────────────────────────────────────────────────
const EXCLUDE_TABLES = [
"_prisma_migrations",
"audit_logs",
"active_sessions",
"sessions",
"accounts",
"verification_tokens",
"invite_tokens",
"notifications",
"import_batches",
"staged_assignments",
"staged_availability_rules",
"staged_clients",
"staged_projects",
"staged_resources",
"staged_unresolved_records",
"staged_vacations",
];
const excludeFlags = EXCLUDE_TABLES.flatMap((t) => ["--exclude-table-data", `public.${t}`]);
// ── Run pg_dump inside the Docker container ───────────────────────────────────
const DB_USER = decodeURIComponent(parsedUrl.username) || "capakraken";
const DB_NAME = parsedUrl.pathname.replace(/^\/+/, "") || "capakraken";
const DB_PORT = parsedUrl.port || "5432";
console.log(`🔍 Exporting ${DB_USER}@${host}:${DB_PORT}/${DB_NAME}`);
const pgDumpArgs = [
"exec",
CONTAINER,
"pg_dump",
"-U", DB_USER,
"-d", DB_NAME,
"--data-only",
"--no-owner",
"--no-acl",
"--disable-triggers",
...excludeFlags,
];
const dump = spawnSync("docker", pgDumpArgs, { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
if (dump.status !== 0) {
console.error("❌ pg_dump failed:");
console.error(dump.stderr);
process.exit(1);
}
// ── Sanitize sensitive values ─────────────────────────────────────────────────
let sql = dump.stdout;
// Replace argon2id password hashes with a clearly invalid placeholder.
// The import script will update these with a real dev hash.
sql = sql.replace(/\$argon2id\$[^\t\n\\]*/g, "__DEV_PASSWORD_HASH__");
// Append sanitizing statements (TOTP, SMTP password, webhook secrets).
// These run after the COPY blocks so they don't require line-level parsing.
sql += `
-- ─── Sanitize secrets (applied after data load) ──────────────────────────────
UPDATE users SET "totpSecret" = NULL, "totpEnabled" = false;
UPDATE system_settings SET "smtpPassword" = NULL;
UPDATE webhooks SET secret = NULL;
`;
// ── Add header ────────────────────────────────────────────────────────────────
const header = `-- CapaKraken dev seed — exported ${new Date().toISOString()}
-- Source: ${DB_USER}@${host}:${DB_PORT}/${DB_NAME}
--
-- Excluded tables:
${EXCLUDE_TABLES.map((t) => `-- ${t}`).join("\n")}
--
-- Sanitized fields:
-- users.passwordHash → placeholder (import-dev-seed sets "Dev123456!")
-- users.totpSecret → NULL
-- users.totpEnabled → false
-- system_settings.smtpPassword → NULL
-- webhooks.secret → NULL
--
-- Import with:
-- node scripts/import-dev-seed.mjs
-- ─────────────────────────────────────────────────────────────────────────────
`;
// ── Write output ──────────────────────────────────────────────────────────────
const outPath = resolve(workspaceRoot, "packages/db/prisma/dev-seed.sql");
writeFileSync(outPath, header + sql, "utf8");
const lines = (header + sql).split("\n").length;
const sizeKb = Math.round(Buffer.byteLength(header + sql, "utf8") / 1024);
console.log(`✅ Written to packages/db/prisma/dev-seed.sql`);
console.log(` ${lines.toLocaleString()} lines · ${sizeKb.toLocaleString()} KB`);
console.log();
console.log("Next step: commit dev-seed.sql or share it with your team.");
console.log("Import it with: node scripts/import-dev-seed.mjs");