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:
@@ -0,0 +1,152 @@
|
|||||||
|
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { useAuthStore } from '@/lib/stores/auth-store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* header.test — Sitzungswaechter im Header (quick-260917-gyd).
|
||||||
|
*
|
||||||
|
* next-intl de.json-gestuetzt (Muster: account-settings-form.test.tsx),
|
||||||
|
* fetchSessionState/logout aus @/lib/auth-actions per vi.hoisted gemockt,
|
||||||
|
* usePathname per mutable Variable (Muster: sidebar.test.tsx). Der echte
|
||||||
|
* useAuthStore wird verwendet (nicht gemockt) und in afterEach
|
||||||
|
* zurueckgesetzt.
|
||||||
|
*/
|
||||||
|
|
||||||
|
vi.mock('next-intl', async () => {
|
||||||
|
const messages = (await import('@/messages/de.json')).default as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
const lookup = (path: string): string | undefined =>
|
||||||
|
path
|
||||||
|
.split('.')
|
||||||
|
.reduce<unknown>(
|
||||||
|
(o, k) => (o && typeof o === 'object' ? (o as any)[k] : undefined),
|
||||||
|
messages,
|
||||||
|
) as string | undefined;
|
||||||
|
return {
|
||||||
|
useTranslations:
|
||||||
|
(ns?: string) =>
|
||||||
|
(key: string, values?: Record<string, unknown>) => {
|
||||||
|
const raw = lookup(ns ? `${ns}.${key}` : key) ?? key;
|
||||||
|
return values
|
||||||
|
? raw.replace(/\{(\w+)\}/g, (_: string, n: string) =>
|
||||||
|
String(values[n] ?? ''),
|
||||||
|
)
|
||||||
|
: raw;
|
||||||
|
},
|
||||||
|
useLocale: () => 'de',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let mockPathname = '/settings/general/desktop';
|
||||||
|
vi.mock('next/navigation', () => ({
|
||||||
|
usePathname: () => mockPathname,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('next/link', () => ({
|
||||||
|
default: ({
|
||||||
|
children,
|
||||||
|
href,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
href: string;
|
||||||
|
className?: string;
|
||||||
|
}) => (
|
||||||
|
<a href={href} className={className}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { fetchSessionState, logout } = vi.hoisted(() => ({
|
||||||
|
fetchSessionState: vi.fn(),
|
||||||
|
logout: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock('@/lib/auth-actions', () => ({ fetchSessionState, logout }));
|
||||||
|
|
||||||
|
vi.mock('@/components/bug-report/bug-report-button', () => ({
|
||||||
|
BugReportButton: () => null,
|
||||||
|
}));
|
||||||
|
vi.mock('@/components/theme-toggle', () => ({
|
||||||
|
ThemeToggle: () => null,
|
||||||
|
}));
|
||||||
|
vi.mock('@/components/brand/tessera-logo', () => ({
|
||||||
|
TesseraLogo: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
useAuthStore.setState({ user: null });
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockPathname = '/settings/general/desktop';
|
||||||
|
});
|
||||||
|
|
||||||
|
async function renderHeader() {
|
||||||
|
const { Header } = await import('./header');
|
||||||
|
render(<Header />);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Header — Sitzungswaechter (quick-260917-gyd)', () => {
|
||||||
|
it('Test 1 (authenticated): Store enthaelt den Benutzer, kein Redirect', async () => {
|
||||||
|
vi.stubGlobal('location', { href: '', pathname: '/settings/general/desktop', search: '' });
|
||||||
|
fetchSessionState.mockResolvedValue({
|
||||||
|
status: 'authenticated',
|
||||||
|
user: {
|
||||||
|
id: 'u1',
|
||||||
|
username: 'schalli',
|
||||||
|
displayName: 'Schalli',
|
||||||
|
role: 'ADMIN',
|
||||||
|
tenantId: 't1',
|
||||||
|
hasAvatar: false,
|
||||||
|
accentColor: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await renderHeader();
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(useAuthStore.getState().user?.username).toBe('schalli'),
|
||||||
|
);
|
||||||
|
expect((window as any).location.href).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Test 2 (unauthenticated auf Unterseite): Vollnavigation auf /login?next=…', async () => {
|
||||||
|
vi.stubGlobal('location', { href: '', pathname: '/settings/general/desktop', search: '' });
|
||||||
|
fetchSessionState.mockResolvedValue({ status: 'unauthenticated' });
|
||||||
|
|
||||||
|
await renderHeader();
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect((window as any).location.href).toBe(
|
||||||
|
'/login?next=%2Fsettings%2Fgeneral%2Fdesktop',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Test 3 (unauthenticated auf /): Vollnavigation auf /login ohne next', async () => {
|
||||||
|
mockPathname = '/';
|
||||||
|
vi.stubGlobal('location', { href: '', pathname: '/', search: '' });
|
||||||
|
fetchSessionState.mockResolvedValue({ status: 'unauthenticated' });
|
||||||
|
|
||||||
|
await renderHeader();
|
||||||
|
|
||||||
|
await waitFor(() => expect((window as any).location.href).toBe('/login'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Test 4 (unavailable): kein Redirect, Store bleibt leer, Avatar zeigt "?"', async () => {
|
||||||
|
vi.stubGlobal('location', { href: '', pathname: '/settings/general/desktop', search: '' });
|
||||||
|
fetchSessionState.mockResolvedValue({ status: 'unavailable' });
|
||||||
|
|
||||||
|
await renderHeader();
|
||||||
|
|
||||||
|
await waitFor(() => expect(fetchSessionState).toHaveBeenCalledTimes(1));
|
||||||
|
expect((window as any).location.href).toBe('');
|
||||||
|
expect(useAuthStore.getState().user).toBeNull();
|
||||||
|
expect(
|
||||||
|
screen.getByRole('button', { name: 'Benutzermenu' }),
|
||||||
|
).toHaveTextContent('?');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,7 +6,8 @@ import { useTranslations } from 'next-intl';
|
|||||||
import { TesseraLogo } from '@/components/brand/tessera-logo';
|
import { TesseraLogo } from '@/components/brand/tessera-logo';
|
||||||
import { useSidebarStore } from '@/lib/stores/sidebar-store';
|
import { useSidebarStore } from '@/lib/stores/sidebar-store';
|
||||||
import { useAuthStore } from '@/lib/stores/auth-store';
|
import { useAuthStore } from '@/lib/stores/auth-store';
|
||||||
import { fetchCurrentUser, logout } from '@/lib/auth-actions';
|
import { fetchSessionState, logout } from '@/lib/auth-actions';
|
||||||
|
import { buildNextParam } from '@/lib/safe-next';
|
||||||
import { ThemeToggle } from '@/components/theme-toggle';
|
import { ThemeToggle } from '@/components/theme-toggle';
|
||||||
import { BugReportButton } from '@/components/bug-report/bug-report-button';
|
import { BugReportButton } from '@/components/bug-report/bug-report-button';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
@@ -22,23 +23,51 @@ export function Header() {
|
|||||||
|
|
||||||
const isAdmin = user?.role === 'ADMIN' || user?.role === 'SUPER_ADMIN';
|
const isAdmin = user?.role === 'ADMIN' || user?.role === 'SUPER_ADMIN';
|
||||||
|
|
||||||
// Fetch current user on mount to populate auth store
|
// Redirect-Sperre gegen doppelte Navigation (StrictMode-Doppeleffekt,
|
||||||
|
// Effekt-Wiederholungen) — quick-260917-gyd.
|
||||||
|
const redirectedRef = useRef(false);
|
||||||
|
|
||||||
|
// Sitzungswaechter (quick-260917-gyd): Der Header ist der EINZIGE
|
||||||
|
// Waechter, weil er auf jeder Portalseite genau einmal gerendert wird
|
||||||
|
// (AppShell im (portal)-Layout); das (auth)-Layout hat keinen Header,
|
||||||
|
// /login ist oeffentlich (middleware.ts publicRoutes) — Seitenleiste
|
||||||
|
// und Widget-Aufrufe brauchen deshalb keinen eigenen Umbau, ein
|
||||||
|
// Doppel-Redirect ist ausgeschlossen. `authenticated` fuellt den Store,
|
||||||
|
// `unauthenticated` (tote Sitzung, Cookie bereits serverseitig
|
||||||
|
// geloescht) leitet per Vollnavigation zur Anmeldeseite mit `next` auf
|
||||||
|
// die aktuelle Seite, `unavailable` (API-Ausfall) bleibt still.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) {
|
if (user) {
|
||||||
fetchCurrentUser().then((u) => {
|
return;
|
||||||
if (u) {
|
|
||||||
setUser({
|
|
||||||
id: u.id,
|
|
||||||
username: u.username,
|
|
||||||
displayName: u.displayName,
|
|
||||||
role: u.role,
|
|
||||||
tenantId: u.tenantId,
|
|
||||||
hasAvatar: u.hasAvatar,
|
|
||||||
accentColor: u.accentColor,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
fetchSessionState().then((state) => {
|
||||||
|
if (state.status === 'authenticated') {
|
||||||
|
const u = state.user;
|
||||||
|
setUser({
|
||||||
|
id: u.id,
|
||||||
|
username: u.username,
|
||||||
|
displayName: u.displayName,
|
||||||
|
role: u.role,
|
||||||
|
tenantId: u.tenantId,
|
||||||
|
hasAvatar: u.hasAvatar,
|
||||||
|
accentColor: u.accentColor,
|
||||||
|
});
|
||||||
|
} else if (state.status === 'unauthenticated') {
|
||||||
|
if (redirectedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redirectedRef.current = true;
|
||||||
|
const next = buildNextParam(
|
||||||
|
window.location.pathname,
|
||||||
|
window.location.search,
|
||||||
|
);
|
||||||
|
window.location.href = next
|
||||||
|
? `/login?next=${encodeURIComponent(next)}`
|
||||||
|
: '/login';
|
||||||
|
}
|
||||||
|
// 'unavailable' (T-gyd-02): kein Redirect, bisheriges stilles
|
||||||
|
// Verhalten -- ein API-Ausfall darf keine Abmelde-Schleife ausloesen.
|
||||||
|
});
|
||||||
}, [user, setUser]);
|
}, [user, setUser]);
|
||||||
|
|
||||||
// Close dropdown on outside click
|
// Close dropdown on outside click
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* auth-actions.test (fetchSessionState) — Sitzungswaechter (quick-260917-gyd).
|
||||||
|
*
|
||||||
|
* next/headers wird gemockt (cookies() liefert ein Promise auf ein Objekt
|
||||||
|
* mit get/set/delete aus vi.hoisted, Muster: module-access.test.tsx),
|
||||||
|
* next/navigation ebenfalls (redirect ist hier ungenutzt, muss aber
|
||||||
|
* importierbar bleiben), fetch per vi.stubGlobal. Die 'use server'-
|
||||||
|
* Direktive ist unter vitest wirkungslos.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { cookieGet, cookieSet, cookieDelete } = vi.hoisted(() => ({
|
||||||
|
cookieGet: vi.fn(),
|
||||||
|
cookieSet: vi.fn(),
|
||||||
|
cookieDelete: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('next/headers', () => ({
|
||||||
|
cookies: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
get: cookieGet,
|
||||||
|
set: cookieSet,
|
||||||
|
delete: cookieDelete,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('next/navigation', () => ({
|
||||||
|
redirect: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchSessionState', () => {
|
||||||
|
it('Test 1: kein Cookie -> unauthenticated, kein fetch', async () => {
|
||||||
|
cookieGet.mockReturnValue(undefined);
|
||||||
|
const fetchMock = vi.fn();
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
const { fetchSessionState } = await import('./auth-actions');
|
||||||
|
const result = await fetchSessionState();
|
||||||
|
|
||||||
|
expect(result).toEqual({ status: 'unauthenticated' });
|
||||||
|
expect(fetchMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Test 2: 200 mit Benutzer -> authenticated, Cookie bleibt, Cookie-Header gesetzt', async () => {
|
||||||
|
cookieGet.mockReturnValue({ value: 'session-abc' });
|
||||||
|
const fetchMock = vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: () => Promise.resolve('{"id":"u1","username":"schalli"}'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
const { fetchSessionState } = await import('./auth-actions');
|
||||||
|
const result = await fetchSessionState();
|
||||||
|
|
||||||
|
expect(result.status).toBe('authenticated');
|
||||||
|
if (result.status === 'authenticated') {
|
||||||
|
expect(result.user.username).toBe('schalli');
|
||||||
|
}
|
||||||
|
expect(cookieDelete).not.toHaveBeenCalled();
|
||||||
|
const [, options] = fetchMock.mock.calls[0] as unknown as [
|
||||||
|
string,
|
||||||
|
RequestInit,
|
||||||
|
];
|
||||||
|
expect((options.headers as Record<string, string>).Cookie).toBe(
|
||||||
|
'session=session-abc',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Test 3: Status 401 -> unauthenticated, Cookie genau einmal geloescht', async () => {
|
||||||
|
cookieGet.mockReturnValue({ value: 'session-abc' });
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn(() => Promise.resolve({ ok: false, status: 401, text: () => Promise.resolve('') })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { fetchSessionState } = await import('./auth-actions');
|
||||||
|
const result = await fetchSessionState();
|
||||||
|
|
||||||
|
expect(result).toEqual({ status: 'unauthenticated' });
|
||||||
|
expect(cookieDelete).toHaveBeenCalledTimes(1);
|
||||||
|
expect(cookieDelete).toHaveBeenCalledWith('session');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Test 4: Status 403 -> unauthenticated, Cookie geloescht', async () => {
|
||||||
|
cookieGet.mockReturnValue({ value: 'session-abc' });
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn(() => Promise.resolve({ ok: false, status: 403, text: () => Promise.resolve('') })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { fetchSessionState } = await import('./auth-actions');
|
||||||
|
const result = await fetchSessionState();
|
||||||
|
|
||||||
|
expect(result).toEqual({ status: 'unauthenticated' });
|
||||||
|
expect(cookieDelete).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Test 5: Status 200 mit leerem Body (oder "null") -> unauthenticated, Cookie geloescht', async () => {
|
||||||
|
cookieGet.mockReturnValue({ value: 'session-abc' });
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn(() => Promise.resolve({ ok: true, status: 200, text: () => Promise.resolve('') })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { fetchSessionState } = await import('./auth-actions');
|
||||||
|
const result1 = await fetchSessionState();
|
||||||
|
expect(result1).toEqual({ status: 'unauthenticated' });
|
||||||
|
expect(cookieDelete).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
vi.clearAllMocks();
|
||||||
|
cookieGet.mockReturnValue({ value: 'session-abc' });
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn(() => Promise.resolve({ ok: true, status: 200, text: () => Promise.resolve('null') })),
|
||||||
|
);
|
||||||
|
const result2 = await fetchSessionState();
|
||||||
|
expect(result2).toEqual({ status: 'unauthenticated' });
|
||||||
|
expect(cookieDelete).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Test 6: Status 500 -> unavailable, Cookie bleibt', async () => {
|
||||||
|
cookieGet.mockReturnValue({ value: 'session-abc' });
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn(() => Promise.resolve({ ok: false, status: 500, text: () => Promise.resolve('') })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { fetchSessionState } = await import('./auth-actions');
|
||||||
|
const result = await fetchSessionState();
|
||||||
|
|
||||||
|
expect(result).toEqual({ status: 'unavailable' });
|
||||||
|
expect(cookieDelete).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Test 7: fetch wirft (Netzwerkfehler) -> unavailable, Cookie bleibt', async () => {
|
||||||
|
cookieGet.mockReturnValue({ value: 'session-abc' });
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn(() => Promise.reject(new TypeError('fetch failed'))),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { fetchSessionState } = await import('./auth-actions');
|
||||||
|
const result = await fetchSessionState();
|
||||||
|
|
||||||
|
expect(result).toEqual({ status: 'unavailable' });
|
||||||
|
expect(cookieDelete).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Test 8: Status 200 mit nicht-JSON-Body -> unavailable, Cookie bleibt', async () => {
|
||||||
|
cookieGet.mockReturnValue({ value: 'session-abc' });
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn(() => Promise.resolve({ ok: true, status: 200, text: () => Promise.resolve('<html>') })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { fetchSessionState } = await import('./auth-actions');
|
||||||
|
const result = await fetchSessionState();
|
||||||
|
|
||||||
|
expect(result).toEqual({ status: 'unavailable' });
|
||||||
|
expect(cookieDelete).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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.
|
* Fetch the current authenticated user from the API.
|
||||||
* Uses the session cookie for authentication.
|
* Uses the session cookie for authentication.
|
||||||
|
|||||||
Reference in New Issue
Block a user