e01074926e
CI / Architecture Guardrails (pull_request) Successful in 6m31s
CI / Typecheck (pull_request) Failing after 6m9s
CI / Build (pull_request) Has been skipped
CI / E2E Tests (pull_request) Has been skipped
CI / Fresh-Linux Docker Deploy (pull_request) Has been skipped
CI / Assistant Split Regression (pull_request) Successful in 7m23s
CI / Lint (pull_request) Successful in 6m54s
CI / Unit Tests (pull_request) Successful in 9m28s
CI / Release Images (pull_request) Has been skipped
Adds a synchronous policy check that blocks (1) the curated >=12-char common-password list (rockyou top, predictable seasonal, admin defaults), (2) trivial patterns (single-char repeat, short-pattern repeat, keyboard or numeric sequences), and (3) passwords containing the user's email local-part or any name component. Wired into all five password-mutation sites: first-admin setup, admin createUser/setUserPassword, invite acceptance, and password-reset. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
64 lines
1.8 KiB
TypeScript
64 lines
1.8 KiB
TypeScript
"use server";
|
|
import { prisma } from "@capakraken/db";
|
|
import { SystemRole } from "@capakraken/db";
|
|
import {
|
|
PASSWORD_MAX_LENGTH,
|
|
PASSWORD_MIN_LENGTH,
|
|
PASSWORD_POLICY_MESSAGE,
|
|
checkPasswordPolicy,
|
|
} from "@capakraken/shared";
|
|
|
|
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 < PASSWORD_MIN_LENGTH ||
|
|
formData.password.length > PASSWORD_MAX_LENGTH
|
|
) {
|
|
return { error: "validation", message: PASSWORD_POLICY_MESSAGE };
|
|
}
|
|
const policy = checkPasswordPolicy(formData.password, {
|
|
email: formData.email,
|
|
name: formData.name,
|
|
});
|
|
if (!policy.ok) {
|
|
return { error: "validation", message: policy.reason };
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|