"use server"; import { redirect } from "next/navigation"; import { AuthError } from "next-auth"; import { getLocale } from "next-intl/server"; import { signIn } from "@/server/auth"; import { clientIp } from "@/server/auth-selfservice"; import { signLoginTicket } from "@/server/login-ticket"; import { enforceTrialRateLimit } from "@/server/services/trial/abuse"; import { checkTrialStep, confirmTrialSignup, submitTrialSignup } from "@/server/services/trial/signup"; import type { FieldErrors } from "@/lib/trial/signup"; /** * L15 Testphase — public self-service signup (no session). Registered as PUBLIC in * scripts/check-module-guards.ts: every action checks the rate limit (per IP, per e-mail). * Business logic in src/server/services/trial/signup.ts. */ export type TrialSubmitState = { status: "sent" } | { status: "invalid"; errors: FieldErrors }; export type TrialConfirmState = { status: "idle" } | { status: "invalid" } | { status: "expired" } | { status: "rate_limited" }; async function localeOrDefault(): Promise { try { return await getLocale(); } catch { return "de"; } } /** Server-side validation of one wizard step (Weiter). Writes nothing. */ export async function checkTrialStepAction(step: string, values: unknown): Promise { const limit = enforceTrialRateLimit("trialStepCheck", { ip: await clientIp() }); if (!limit.allowed) return { _form: "rate_limited" }; return checkTrialStep(step, values); } /** Final submit: validates everything again, creates the pending signup and sends the confirmation mail. */ export async function submitTrialSignupAction(values: unknown): Promise { const ip = await clientIp(); const email = values && typeof values === "object" && typeof (values as { email?: unknown }).email === "string" ? (values as { email: string }).email : null; const limit = enforceTrialRateLimit("trialSignup", { ip, email }); if (!limit.allowed) return { status: "invalid", errors: { _form: "rate_limited" } }; return submitTrialSignup(values, { ip, locale: await localeOrDefault() }); } /** Confirmation (POST from /testen/bestaetigen — mail scanners opening the GET link consume nothing). */ export async function confirmTrialSignupAction(_prev: TrialConfirmState, fd: FormData): Promise { const limit = enforceTrialRateLimit("trialConfirm", { ip: await clientIp() }); if (!limit.allowed) return { status: "rate_limited" }; const token = String(fd.get("token") ?? ""); const result = await confirmTrialSignup(token); if (result.status !== "ok") return { status: result.status }; // Directly signed in via the existing login finalisation (login-ticket provider). If that is not // possible (e.g. MFA policy), the regular login page takes over. try { await signIn("login-ticket", { ticket: signLoginTicket(result.identityId, result.tenantSlug), redirectTo: "/dashboard?welcome=1" }); } catch (err) { if (err instanceof AuthError) redirect("/login?trial=ready"); throw err; // NEXT_REDIRECT of a successful sign-in } return { status: "idle" }; }