Bei COOKIE_SECURE=true und Zugriff über HTTP verwirft der Browser das Sitzungs-Cookie. Die Anmeldung selbst meldet Erfolg, die nächste Anfrage gilt aber als nicht angemeldet – der Nutzer landet ohne jede Meldung wieder auf der Anmeldemaske und hält es für ein falsches Passwort. - Das Frontend prüft die Sitzung unmittelbar nach der Anmeldung und nennt bei einem Fehlschlag die Ursache samt beider Auswege - Das Backend protokolliert dieselbe Kombination als Warnung und wertet dabei X-Forwarded-Proto aus, weil hinter einem Reverse Proxy immer http ankommt - docs/deployment.md beschreibt das Symptom wörtlich Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
101 lines
3.2 KiB
TypeScript
101 lines
3.2 KiB
TypeScript
/** Anmeldung, Abmeldung und der angemeldete Benutzer. */
|
||
|
||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||
|
||
import { ApiError, api, isUnauthorized } from "@/lib/api";
|
||
import { keys } from "@/lib/queryClient";
|
||
import { toast } from "@/store/toast";
|
||
import type { MessageResponse, User } from "@/types/api";
|
||
|
||
export function useMe() {
|
||
return useQuery({
|
||
queryKey: keys.me,
|
||
queryFn: async () => {
|
||
try {
|
||
return await api.get<User>("/me");
|
||
} catch (fehler) {
|
||
// Kein gültiges Token bedeutet schlicht: nicht angemeldet.
|
||
if (isUnauthorized(fehler)) return null;
|
||
throw fehler;
|
||
}
|
||
},
|
||
staleTime: 60_000,
|
||
retry: false,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Wird ausgelöst, wenn die Anmeldedaten stimmen, die Sitzung aber nicht hält.
|
||
*
|
||
* Ursache ist praktisch immer ein `Secure`-Cookie über eine HTTP-Verbindung:
|
||
* Der Browser verwirft es stillschweigend, die Anmeldung meldet Erfolg und die
|
||
* nächste Anfrage gilt wieder als nicht angemeldet.
|
||
*/
|
||
export class SessionNotEstablishedError extends Error {
|
||
constructor() {
|
||
super(
|
||
"Die Anmeldung hat geklappt, aber der Browser hat das Sitzungs-Cookie verworfen. " +
|
||
"Das passiert bei Zugriff über HTTP, solange COOKIE_SECURE aktiv ist – " +
|
||
"greife über HTTPS zu oder setze COOKIE_SECURE=false.",
|
||
);
|
||
this.name = "SessionNotEstablishedError";
|
||
}
|
||
}
|
||
|
||
export function useLogin() {
|
||
const client = useQueryClient();
|
||
return useMutation({
|
||
mutationFn: async (daten: { username: string; password: string }) => {
|
||
const benutzer = await api.post<User>("/auth/login", daten);
|
||
|
||
// Gegenprobe: hält die Sitzung wirklich? Ohne sie liefe der Nutzer in eine
|
||
// stille Schleife zurück zur Anmeldemaske, ohne je einen Grund zu sehen.
|
||
try {
|
||
await api.get<User>("/me");
|
||
} catch (fehler) {
|
||
if (isUnauthorized(fehler)) throw new SessionNotEstablishedError();
|
||
throw fehler;
|
||
}
|
||
|
||
return benutzer;
|
||
},
|
||
onSuccess: (benutzer) => {
|
||
client.setQueryData(keys.me, benutzer);
|
||
void client.invalidateQueries();
|
||
},
|
||
});
|
||
}
|
||
|
||
export function useLogout() {
|
||
const client = useQueryClient();
|
||
return useMutation({
|
||
mutationFn: () => api.post<MessageResponse>("/auth/logout"),
|
||
onSuccess: () => {
|
||
client.setQueryData(keys.me, null);
|
||
client.clear();
|
||
toast.info("Abgemeldet.");
|
||
},
|
||
});
|
||
}
|
||
|
||
export function useChangePassword() {
|
||
const client = useQueryClient();
|
||
return useMutation({
|
||
mutationFn: (daten: { current_password: string; new_password: string }) =>
|
||
api.post<MessageResponse>("/auth/change-password", daten),
|
||
onSuccess: () => {
|
||
// Der Passwortwechsel beendet alle Sitzungen – es folgt eine neue Anmeldung.
|
||
client.setQueryData(keys.me, null);
|
||
client.clear();
|
||
toast.success("Passwort geändert.", "Bitte melde dich neu an.");
|
||
},
|
||
});
|
||
}
|
||
|
||
/** Übersetzt einen Anmeldefehler in eine Meldung für das Formular. */
|
||
export function loginErrorMessage(error: unknown): string {
|
||
if (error instanceof SessionNotEstablishedError) return error.message;
|
||
if (error instanceof ApiError) return error.message;
|
||
return "Der Server ist nicht erreichbar.";
|
||
}
|