d4641e27aa
- /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>
45 lines
1.4 KiB
TypeScript
45 lines
1.4 KiB
TypeScript
"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;
|
|
}
|
|
}
|