Add non-destructive dev seed on startup
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
"use server";
|
||||
|
||||
import { hash } from "bcryptjs";
|
||||
import { z } from "zod";
|
||||
|
||||
import { signIn } from "@/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
// =====================================================================
|
||||
// /invite/[token] · Server Action
|
||||
// ---------------------------------------------------------------------
|
||||
// acceptInviteAction:
|
||||
// 1. Token erneut gegen DB prüfen (kein Trust auf Client-State)
|
||||
// 2. Passwort hashen + User-Daten setzen (DSGVO-Timestamps)
|
||||
// 3. Token löschen (kein Replay möglich)
|
||||
// 4. Direkt einloggen + Redirect auf /dashboard
|
||||
// =====================================================================
|
||||
|
||||
const inviteSchema = z
|
||||
.object({
|
||||
token: z.string().min(1),
|
||||
password: z.string().min(8, "Mindestens 8 Zeichen.").max(72, "Maximal 72 Zeichen."),
|
||||
confirmPassword: z.string().min(1, "Passwort-Bestätigung fehlt."),
|
||||
acceptPrivacyPolicy: z.literal("on", {
|
||||
error: "Bitte die Datenschutzerklärung akzeptieren.",
|
||||
}),
|
||||
directoryOptIn: z.enum(["on", "off"]).optional(),
|
||||
})
|
||||
.refine((d) => d.password === d.confirmPassword, {
|
||||
message: "Passwörter stimmen nicht überein.",
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
|
||||
const PRIVACY_POLICY_VERSION = "2026-05-01";
|
||||
|
||||
export type InviteState = {
|
||||
errors?: {
|
||||
password?: string[];
|
||||
confirmPassword?: string[];
|
||||
acceptPrivacyPolicy?: string[];
|
||||
directoryOptIn?: string[];
|
||||
_form?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export async function acceptInviteAction(
|
||||
_prev: InviteState,
|
||||
formData: FormData,
|
||||
): Promise<InviteState> {
|
||||
const parsed = inviteSchema.safeParse({
|
||||
token: formData.get("token"),
|
||||
password: formData.get("password"),
|
||||
confirmPassword: formData.get("confirmPassword"),
|
||||
acceptPrivacyPolicy: formData.get("acceptPrivacyPolicy"),
|
||||
directoryOptIn: formData.get("directoryOptIn") ?? "off",
|
||||
});
|
||||
|
||||
if (!parsed.success) {
|
||||
return { errors: parsed.error.flatten().fieldErrors };
|
||||
}
|
||||
|
||||
const { token, password, directoryOptIn } = parsed.data;
|
||||
const now = new Date();
|
||||
|
||||
// ── 1. Token erneut DB-seitig validieren ──────────────────────────
|
||||
const verificationToken = await prisma.verificationToken.findUnique({
|
||||
where: { token },
|
||||
});
|
||||
|
||||
if (!verificationToken) {
|
||||
return { errors: { _form: ["Einladungslink ist ungültig."] } };
|
||||
}
|
||||
if (verificationToken.expires < now) {
|
||||
return {
|
||||
errors: {
|
||||
_form: [
|
||||
"Dieser Einladungslink ist abgelaufen. Bitte wende dich an deinen Administrator.",
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const userId = verificationToken.identifier;
|
||||
|
||||
// ── 2. User validieren (existiert und hat noch kein Passwort) ──────
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { id: true, email: true, passwordHash: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return { errors: { _form: ["Benutzer nicht gefunden."] } };
|
||||
}
|
||||
if (user.passwordHash !== "") {
|
||||
// Invite wurde bereits eingelöst
|
||||
return {
|
||||
errors: {
|
||||
_form: [
|
||||
"Dieser Einladungslink wurde bereits verwendet. Bitte melde dich an.",
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ── 3. Passwort hashen ─────────────────────────────────────────────
|
||||
const passwordHash = await hash(password, 12);
|
||||
|
||||
// ── 4. User updaten + Token löschen (atomar) ──────────────────────
|
||||
await prisma.$transaction([
|
||||
prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
passwordHash,
|
||||
privacyPolicyAcceptedAt: now,
|
||||
privacyPolicyVersion: PRIVACY_POLICY_VERSION,
|
||||
directoryOptInAt: directoryOptIn === "on" ? now : null,
|
||||
emailVerifiedAt: now,
|
||||
lastLoginAt: now,
|
||||
},
|
||||
}),
|
||||
prisma.verificationToken.delete({
|
||||
where: { token },
|
||||
}),
|
||||
]);
|
||||
|
||||
// ── 5. Direkt einloggen → wirft NEXT_REDIRECT ─────────────────────
|
||||
await signIn("credentials", {
|
||||
email: user.email,
|
||||
password,
|
||||
redirectTo: "/dashboard",
|
||||
});
|
||||
|
||||
// Unreachable – signIn redirected.
|
||||
return {};
|
||||
}
|
||||
Reference in New Issue
Block a user