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:
2026-04-02 20:45:15 +02:00
parent 41eb722369
commit d4641e27aa
5 changed files with 452 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
"use server";
import { prisma } from "@capakraken/db";
import { SystemRole } from "@capakraken/db";
export type SetupResult =
| { success: true }
| { error: "alreadySetup" | "emailTaken" | "validation"; message?: string };
export async function createFirstAdmin(formData: {
name: string;
email: string;
password: string;
}): Promise<SetupResult> {
// Validate
if (!formData.name.trim()) return { error: "validation", message: "Name is required." };
if (!formData.email.includes("@")) return { error: "validation", message: "Valid email required." };
if (formData.password.length < 8) return { error: "validation", message: "Password must be at least 8 characters." };
// TOCTOU guard — check again inside the action
const count = await prisma.user.count();
if (count > 0) return { error: "alreadySetup" };
const { hash } = await import("@node-rs/argon2");
const passwordHash = await hash(formData.password);
try {
await prisma.user.create({
data: {
email: formData.email.toLowerCase().trim(),
name: formData.name.trim(),
passwordHash,
systemRole: SystemRole.ADMIN,
isActive: true,
},
});
return { success: true };
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
if (message.includes("Unique constraint") || message.includes("unique")) {
return { error: "emailTaken" };
}
throw err;
}
}