feat(quick-260917-gyd): Sitzungswaechter erkennt tote Sitzung, Header leitet ab
- auth-actions.ts: fetchSessionState() unterscheidet tote Sitzung (401/403/leere 200-Antwort, Cookie wird geloescht) von API-Ausfall (5xx/Netzwerkfehler/Nicht-JSON, unavailable ohne Redirect); fetchCurrentUser bleibt unveraendert - header.tsx: Waechter im useEffect leitet bei toter Sitzung per Vollnavigation auf /login?next=… um, bleibt bei API-Ausfall still Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -236,6 +236,72 @@ export async function updateAccentColorAction(
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionState =
|
||||
| { status: 'authenticated'; user: AuthUser }
|
||||
| { status: 'unauthenticated' }
|
||||
| { status: 'unavailable' };
|
||||
|
||||
/**
|
||||
* Klassifiziert die aktuelle Sitzung fuer den Header-Waechter
|
||||
* (quick-260917-gyd). Die Unterscheidung ist load-bearing: nur eine
|
||||
* nachweislich tote Sitzung (401/403 oder 200 ohne Benutzerobjekt — so
|
||||
* antwortet NestJS, wenn `AuthService.getMe` bei geloeschtem Benutzer
|
||||
* `null` liefert, z. B. nach Neuanlage der Datenbank) darf das Cookie
|
||||
* loeschen und abmelden. Ein API-Ausfall (5xx, Netzwerkfehler, Antwort
|
||||
* ohne gueltiges JSON) darf KEINE Abmelde-Schleife ausloesen und liefert
|
||||
* deshalb `unavailable`, ohne das Cookie anzufassen.
|
||||
*
|
||||
* `cookieStore.delete()` ist nur in Server Actions/Route Handlers
|
||||
* erlaubt — deshalb passiert die Loeschung hier und nicht im Header.
|
||||
*/
|
||||
export async function fetchSessionState(): Promise<SessionState> {
|
||||
const cookieStore = await cookies();
|
||||
const session = cookieStore.get('session')?.value;
|
||||
|
||||
if (!session) {
|
||||
return { status: 'unauthenticated' };
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/auth/me`, {
|
||||
headers: {
|
||||
Cookie: `session=${session}`,
|
||||
},
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
cookieStore.delete('session');
|
||||
return { status: 'unauthenticated' };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
return { status: 'unavailable' };
|
||||
}
|
||||
|
||||
const body = (await response.text()).trim();
|
||||
if (body === '' || body === 'null') {
|
||||
cookieStore.delete('session');
|
||||
return { status: 'unauthenticated' };
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(body);
|
||||
} catch {
|
||||
return { status: 'unavailable' };
|
||||
}
|
||||
|
||||
if (parsed && typeof parsed === 'object' && 'id' in parsed) {
|
||||
return { status: 'authenticated', user: parsed as AuthUser };
|
||||
}
|
||||
|
||||
return { status: 'unavailable' };
|
||||
} catch {
|
||||
return { status: 'unavailable' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the current authenticated user from the API.
|
||||
* Uses the session cookie for authentication.
|
||||
|
||||
Reference in New Issue
Block a user