feat: first-run setup wizard, CLI seed script, and installation docs
- /setup Server Component + SetupClient form + createFirstAdmin Server Action: zero-users guard (TOCTOU-safe), argon2 hash, ADMIN user creation, redirects to /auth/signin after setup - scripts/setup-admin.mjs: CLI alternative for headless/container setups - docs/installation.md: 7-section install guide (clone → configure → run → verify) Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env node
|
||||
// scripts/setup-admin.mjs
|
||||
// Usage: node scripts/setup-admin.mjs --email admin@example.com --name "Admin" --password secret123
|
||||
|
||||
import { loadWorkspaceEnv } from "./load-env.mjs";
|
||||
|
||||
// Load .env if DATABASE_URL is not already set
|
||||
if (!process.env.DATABASE_URL) {
|
||||
const loaded = loadWorkspaceEnv();
|
||||
if (loaded.length === 0 && !process.env.DATABASE_URL) {
|
||||
console.error("ERROR: DATABASE_URL is not set. Create a .env file or export DATABASE_URL before running this script.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse CLI args
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
function getArg(flag) {
|
||||
const index = args.indexOf(flag);
|
||||
if (index === -1 || index + 1 >= args.length) return null;
|
||||
return args[index + 1];
|
||||
}
|
||||
|
||||
const email = getArg("--email");
|
||||
const name = getArg("--name");
|
||||
const password = getArg("--password");
|
||||
|
||||
const missing = [];
|
||||
if (!email) missing.push("--email");
|
||||
if (!name) missing.push("--name");
|
||||
if (!password) missing.push("--password");
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.error(`ERROR: Missing required arguments: ${missing.join(", ")}`);
|
||||
console.error("Usage: node scripts/setup-admin.mjs --email admin@example.com --name \"Admin\" --password secret123");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!email.includes("@")) {
|
||||
console.error("ERROR: Invalid email address.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
console.error("ERROR: Password must be at least 8 characters.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { PrismaClient } = await import("@prisma/client");
|
||||
const { hash } = await import("@node-rs/argon2");
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
try {
|
||||
const count = await prisma.user.count();
|
||||
|
||||
if (count > 0) {
|
||||
console.log("Admin user already exists. Skipping.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const passwordHash = await hash(password);
|
||||
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
email: email.toLowerCase().trim(),
|
||||
name: name.trim(),
|
||||
passwordHash,
|
||||
systemRole: "ADMIN",
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Admin user created: ${email.toLowerCase().trim()}`);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.error(`ERROR: ${message}`);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
Reference in New Issue
Block a user