63 lines
1.8 KiB
TypeScript
63 lines
1.8 KiB
TypeScript
"use server";
|
|
|
|
import { AuthError } from "next-auth";
|
|
import { z } from "zod";
|
|
|
|
import { safeCallbackUrl } from "@/lib/safe-redirect";
|
|
import { signIn } from "@/auth";
|
|
|
|
// =====================================================================
|
|
// /login · Server Action
|
|
// ---------------------------------------------------------------------
|
|
// Wir kapseln `signIn("credentials", …)` damit:
|
|
// • das Formular `useActionState` mit typisiertem Fehler-State nutzen kann
|
|
// • der Redirect-Trigger (NEXT_REDIRECT) sauber durchgereicht wird
|
|
// • generische Fehler ("Login fehlgeschlagen") weder Username-Existenz
|
|
// noch Lockout-Status leaken (kein User-Enumeration).
|
|
// =====================================================================
|
|
|
|
const loginSchema = z.object({
|
|
email: z.string().email("Bitte eine gültige E-Mail-Adresse angeben.").toLowerCase().trim(),
|
|
password: z.string().min(1, "Passwort fehlt.").max(72),
|
|
callbackUrl: z.string().optional(),
|
|
});
|
|
|
|
export type LoginState = {
|
|
errors?: {
|
|
email?: string[];
|
|
password?: string[];
|
|
_form?: string[];
|
|
};
|
|
};
|
|
|
|
export async function loginAction(
|
|
_prev: LoginState,
|
|
formData: FormData,
|
|
): Promise<LoginState> {
|
|
const parsed = loginSchema.safeParse(Object.fromEntries(formData));
|
|
if (!parsed.success) {
|
|
return { errors: parsed.error.flatten().fieldErrors };
|
|
}
|
|
const { email, password } = parsed.data;
|
|
const redirectTo = safeCallbackUrl(parsed.data.callbackUrl);
|
|
|
|
try {
|
|
await signIn("credentials", {
|
|
email,
|
|
password,
|
|
redirectTo,
|
|
});
|
|
} catch (err) {
|
|
// signIn wirft NEXT_REDIRECT bei Erfolg → das müssen wir bewusst
|
|
// weiterwerfen, sonst wird der Redirect verschluckt.
|
|
if (err instanceof AuthError) {
|
|
return {
|
|
errors: { _form: ["E-Mail oder Passwort ist nicht korrekt."] },
|
|
};
|
|
}
|
|
throw err;
|
|
}
|
|
|
|
return {};
|
|
}
|