Files
CapaKraken/apps/web/src/app/setup/actions.ts
T
Hartmut 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
security: reject common/weak passwords on every set-password path (#31)
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>
2026-04-18 14:09:38 +02:00

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;
}
}