Files
moneyfy/frontend/src/hooks/useAuth.ts
T
Jonas MenzelandClaude Opus 5 998d5867df
CI / backend (push) Successful in 2m33s
Images bauen / build (backend) (push) Successful in 3m45s
Images bauen / build (frontend) (push) Successful in 4m23s
CI / frontend (push) Successful in 6m38s
fix: stille Anmeldeschleife bei Secure-Cookie über HTTP erklären
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
2026-09-10 00:00:29 +02:00

101 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 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.";
}