Iostream-Li's picture
Add files using upload-large-folder tool
6d1fe92 verified
#!/usr/bin/env node
// Idempotent production seed for the auth bootstrap.
//
// What it does (each step is independently idempotent and safe to re-run):
// 1. Inserts a set of initial invite codes into the `invite_codes` table.
// Sources, in priority order:
// a. --invite-codes <comma,list> CLI flag
// b. $INITIAL_INVITE_CODES (preferred for deploy)
// c. $INVITE_CODES (reuses the runtime env-derived list)
// Existing rows are left untouched (ON CONFLICT DO NOTHING).
//
// 2. Optionally creates an initial admin user when ALL of:
// $INITIAL_ADMIN_EMAIL, $INITIAL_ADMIN_USERNAME, $INITIAL_ADMIN_PASSWORD
// are set. If a user with the same email OR username already exists,
// the step is skipped (no overwrite). Password must be >= 8 chars.
//
// Usage:
// DATABASE_URL=... \
// INITIAL_INVITE_CODES="INVITE-A,INVITE-B" \
// INITIAL_ADMIN_EMAIL=admin@example.com \
// INITIAL_ADMIN_USERNAME=admin \
// INITIAL_ADMIN_PASSWORD='strong-password' \
// node artifacts/api-server/scripts/seed-prod.mjs
//
// Exit codes:
// 0 - success (including "nothing to do")
// 1 - misconfiguration / hard failure
import pg from "pg";
import bcrypt from "bcrypt";
import { ulid } from "ulid";
function parseArgs(argv) {
const out = {};
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
if (a === "--invite-codes") out.inviteCodes = argv[++i];
else if (a.startsWith("--invite-codes=")) out.inviteCodes = a.slice("--invite-codes=".length);
else if (a === "--help" || a === "-h") out.help = true;
}
return out;
}
function parseList(raw) {
return String(raw || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
function defaultPeriodEnd(start = new Date()) {
return new Date(start.getTime() + 30 * 24 * 60 * 60 * 1000);
}
function defaultSettingsBlob() {
return {
appearance: {
theme: "system",
density: "comfortable",
font_scale: 1.0,
code_theme: "github-dark",
},
language: "zh-CN",
default_model_id: "mdl_claude-sonnet-4-6",
send_on_enter: true,
show_token_count: true,
spellcheck: true,
voice: { enabled: false, voice_id: null, speed: 1.0 },
data_controls: {
chat_history_enabled: true,
improve_model_with_my_data: false,
auto_archive_after_days: 0,
},
beta_features: { streaming: true, code_interpreter: false },
notifications: { job_completed: true, weekly_digest: false },
long_term_memory: {
enabled: false,
auto_extract: true,
max_facts: 100,
max_tokens_per_turn: 2000,
},
};
}
async function main() {
const args = parseArgs(process.argv);
if (args.help) {
process.stdout.write(
"Usage: node artifacts/api-server/scripts/seed-prod.mjs [--invite-codes A,B,C]\n",
);
return;
}
const url = process.env.DATABASE_URL;
if (!url) {
console.error("seed-prod: DATABASE_URL is required");
process.exit(1);
}
const codes = parseList(
args.inviteCodes ||
process.env.INITIAL_INVITE_CODES ||
process.env.INVITE_CODES,
);
const adminEmail = String(process.env.INITIAL_ADMIN_EMAIL || "").trim().toLowerCase();
const adminUsername = String(process.env.INITIAL_ADMIN_USERNAME || "").trim().toLowerCase();
const adminPassword = String(process.env.INITIAL_ADMIN_PASSWORD || "");
const wantsAdmin = Boolean(adminEmail || adminUsername || adminPassword);
if (!codes.length && !wantsAdmin) {
console.warn(
"seed-prod: no INITIAL_INVITE_CODES / INVITE_CODES and no INITIAL_ADMIN_* set; nothing to do.",
);
return;
}
const pool = new pg.Pool({ connectionString: url });
const client = await pool.connect();
try {
// 1) invite codes
if (codes.length) {
let inserted = 0;
let skipped = 0;
for (const code of codes) {
const r = await client.query(
`INSERT INTO invite_codes (code) VALUES ($1)
ON CONFLICT (code) DO NOTHING
RETURNING code`,
[code],
);
if (r.rowCount === 1) inserted++;
else skipped++;
}
console.log(
`seed-prod: invite_codes — inserted=${inserted} skipped=${skipped} total=${codes.length}`,
);
} else {
console.log("seed-prod: no invite codes provided; skipping invite seed");
}
// 2) optional admin
if (wantsAdmin) {
if (!adminEmail || !adminUsername || !adminPassword) {
console.error(
"seed-prod: INITIAL_ADMIN_EMAIL, INITIAL_ADMIN_USERNAME and INITIAL_ADMIN_PASSWORD must all be set together.",
);
process.exit(1);
}
if (adminPassword.length < 8) {
console.error("seed-prod: INITIAL_ADMIN_PASSWORD must be at least 8 characters.");
process.exit(1);
}
const existing = await client.query(
`SELECT id, email, username FROM users WHERE email = $1 OR username = $2 LIMIT 1`,
[adminEmail, adminUsername],
);
if (existing.rowCount && existing.rowCount > 0) {
const row = existing.rows[0];
console.log(
`seed-prod: admin user already exists (id=${row.id} email=${row.email} username=${row.username}); not overwriting`,
);
} else {
const id = `usr_${ulid()}`;
const now = new Date();
const passwordHash = await bcrypt.hash(adminPassword, 12);
await client.query("BEGIN");
try {
await client.query(
`INSERT INTO users
(id, email, username, password_hash, display_name, role, plan,
tokens_limit, tokens_used, period_start, period_end, created_at)
VALUES ($1,$2,$3,$4,$5,'admin','free',5000000,0,$6,$7,$6)`,
[
id,
adminEmail,
adminUsername,
passwordHash,
adminUsername,
now,
defaultPeriodEnd(now),
],
);
await client.query(
`INSERT INTO user_settings (user_id, data) VALUES ($1, $2::jsonb)
ON CONFLICT (user_id) DO NOTHING`,
[id, JSON.stringify(defaultSettingsBlob())],
);
await client.query(
`INSERT INTO custom_instructions (user_id, about_me, response_style, enabled)
VALUES ($1, '', '', true)
ON CONFLICT (user_id) DO NOTHING`,
[id],
);
await client.query("COMMIT");
console.log(
`seed-prod: created admin user id=${id} email=${adminEmail} username=${adminUsername}`,
);
} catch (err) {
await client.query("ROLLBACK");
throw err;
}
}
} else {
console.log("seed-prod: no INITIAL_ADMIN_* provided; skipping admin user");
}
} finally {
client.release();
await pool.end();
}
}
main().catch((err) => {
console.error("seed-prod: failed:", err?.stack || err?.message || err);
process.exit(1);
});