fix(auth): Sitzungs-Cookie auch ueber http nutzbar, Anmeldung als Formular (#013)

Das Cookie erhielt im Produktionsbau immer Secure und wurde ueber http vom
Browser verworfen – nach der Anmeldung landete jeder Klick wieder auf der
Anmeldeseite. Massgeblich ist jetzt das Schema in APP_URL.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 23:41:01 +02:00
co-authored by Claude Opus 5
parent f838a44991
commit 2c2f842d20
31 changed files with 277 additions and 69 deletions
+1 -3
View File
@@ -30,9 +30,7 @@ export default async function AnmeldeSeite({ searchParams }: { searchParams: Suc
<div className="card-body">
{fehler ? (
<div className="alert alert-danger py-2" role="alert">
{fehler === "oidc"
? "Die Anmeldung über den Anbieter hat nicht geklappt."
: "Anmeldung nicht möglich."}
{fehler === "oidc" ? "Die Anmeldung über den Anbieter hat nicht geklappt." : fehler}
</div>
) : null}
+1 -3
View File
@@ -184,9 +184,7 @@ export default async function DashboardSeite() {
titel="Aufgaben"
rahmen="warning"
werkzeuge={
<span className="badge pl-bg-warning">
{aufgabenDaten.gesamt + ohneKunde.length}
</span>
<span className="badge pl-bg-warning">{aufgabenDaten.gesamt + ohneKunde.length}</span>
}
>
<TodoList aufgaben={aufgaben} />
+49
View File
@@ -0,0 +1,49 @@
import { cookies, headers } from "next/headers";
import { clientIp } from "@/lib/ratelimit";
import { cookieOptionen, SITZUNGS_COOKIE, sitzungErzeugen } from "@/lib/sitzung";
import { AnmeldeFehler, mitPasswortAnmelden } from "@/server/auth/benutzer";
export const dynamic = "force-dynamic";
/**
* Anmeldung als gewöhnliches Formular. Bewusst kein Server-Action-Weg: So funktioniert die
* Anmeldung auch ohne JavaScript, und das Sitzungs-Cookie hängt an einer normalen Antwort.
*/
export async function POST(request: Request) {
const formular = await request.formData();
const email = formular.get("email")?.toString().trim() ?? "";
const passwort = formular.get("passwort")?.toString() ?? "";
const zielEingabe = formular.get("ziel")?.toString() || "/";
// Nur eigene Pfade zulassen, damit die Anmeldung nicht auf fremde Seiten führt.
const ziel = zielEingabe.startsWith("/") && !zielEingabe.startsWith("//") ? zielEingabe : "/";
function zurueckZurAnmeldung(meldung: string) {
const adresse = new URL("/login", request.url);
adresse.searchParams.set("fehler", meldung);
if (ziel !== "/") adresse.searchParams.set("ziel", ziel);
return Response.redirect(adresse, 303);
}
if (!email || !passwort) {
return zurueckZurAnmeldung("E-Mail-Adresse und Passwort eingeben.");
}
try {
const kopfzeilen = await headers();
const benutzer = await mitPasswortAnmelden(email, passwort, clientIp(kopfzeilen));
const token = await sitzungErzeugen({
benutzerId: benutzer.id,
email: benutzer.email,
name: benutzer.name,
herkunft: "passwort",
});
(await cookies()).set(SITZUNGS_COOKIE, token, cookieOptionen());
return Response.redirect(new URL(ziel, request.url), 303);
} catch (fehler) {
if (fehler instanceof AnmeldeFehler) return zurueckZurAnmeldung(fehler.message);
console.error("Anmeldung fehlgeschlagen:", fehler);
return zurueckZurAnmeldung("Anmeldung derzeit nicht möglich.");
}
}
+2 -1
View File
@@ -1,4 +1,5 @@
import { cookies } from "next/headers";
import { ueberHttps } from "@/lib/sitzung";
import { anmeldungStarten, oidcEinstellungen } from "@/server/auth/oidc";
export const dynamic = "force-dynamic";
@@ -18,7 +19,7 @@ export async function GET(request: Request) {
const optionen = {
httpOnly: true,
sameSite: "lax" as const,
secure: process.env.NODE_ENV === "production",
secure: ueberHttps(),
path: "/",
maxAge: 600,
};