winofy-front/src/lib/api/winofy.ts
Afra ‌ 573f43ea41 fix: self-healing session — stale/expired sessions no longer crash the app
Root cause of the home-page 500 ("اطلاعات برای اعتبارسنجی ارسال نشده است",
401 not_authenticated): getSession() only decrypted the session cookie, it
never checked whether the access token inside it had actually expired. The
JWE wrapper lives 30 days; the real Gooyal access token lives ~10 hours
(expires_in: 36000). So isAuthenticated stayed true long after the token
died, the home page called getCart() anyway, winofyFetch's refresh attempt
failed silently (dead refresh token) and returned no Authorization header
at all, and the resulting 401 was never caught -- crashing the whole page.

Fix: getValidSession() (session.ts) is now the single source of truth --
refreshes when possible, self-heals by clearing the cookie when refresh
fails, deduped per-request via React's cache(). winofyFetch and every
isAuthenticated check (home page, shop layout) now use it instead of the
raw cookie read.

That surfaced a second bug: Next.js forbids writing cookies during a plain
Server Component render (Server Actions/Route Handlers only), so
getValidSession()'s self-heal itself crashed when called from a page like
home. createSession()/deleteSession() now swallow that specific failure --
the refreshed/cleared session is still correct for the rest of the current
request, it just won't persist when called from a context that can't write
cookies (the next request re-derives the same correct answer).

Also added requireSession(path) and wired it into every auth-required page
(cart, checkout, addresses, orders, order-groups/[uuid], profile) --
proxy.ts's gate is deliberately optimistic (cookie presence only, per
Next.js's own guidance), so a present-but-dead session was reaching these
pages and crashing the same way; they now redirect to /login instead.

Verified against the exact failure: a session with a dead access+refresh
token now renders the home page as logged-out (200, not 500) and redirects
/cart to /login (307) instead of crashing. Also verified the happy path
(a genuinely fresh, valid session from a real OTP login) still works.
2026-08-19 10:26:02 +03:30

61 lines
1.8 KiB
TypeScript

import "server-only";
import { getValidSession } from "@/lib/auth/session";
import { ApiError, type WinofyErrorDetails } from "./errors";
const BASE_URL = process.env.WINOFY_API_BASE_URL!;
interface WinofyFetchOptions extends Omit<RequestInit, "body"> {
/** Attach the caller's bearer token. Defaults to true — set false for public endpoints. */
auth?: boolean;
params?: Record<string, string | number | boolean | undefined>;
body?: unknown;
}
export interface PaginatedResponse<T> {
count: number;
next: string | null;
previous: string | null;
results: T[];
}
export async function winofyFetch<T>(path: string, options: WinofyFetchOptions = {}): Promise<T> {
const { auth = true, params, headers, body, ...rest } = options;
const url = new URL(BASE_URL.replace(/\/$/, "") + path);
if (params) {
for (const [key, value] of Object.entries(params)) {
if (value !== undefined) url.searchParams.set(key, String(value));
}
}
const finalHeaders = new Headers(headers);
const hasBody = body !== undefined;
if (hasBody && !finalHeaders.has("Content-Type")) {
finalHeaders.set("Content-Type", "application/json");
}
if (auth) {
const session = await getValidSession();
if (session) finalHeaders.set("Authorization", `Bearer ${session.accessToken}`);
}
const res = await fetch(url.toString(), {
...rest,
headers: finalHeaders,
body: hasBody ? JSON.stringify(body) : undefined,
cache: "no-store",
});
if (res.status === 204) return undefined as T;
const data = await res.json().catch(() => null);
if (!res.ok) {
const envelope = data as { status_message?: string; details?: WinofyErrorDetails } | null;
throw new ApiError(res.status, envelope?.status_message ?? res.statusText, envelope?.details ?? null);
}
return data as T;
}
export { ApiError };