refactor: enforce frontend view model boundaries
Frontend verify / commits (pull_request) Successful in 28s
Frontend verify / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 8m19s

This commit is contained in:
2026-09-28 18:48:23 +03:00
parent 5b69003516
commit ca0ad4f824
287 changed files with 8558 additions and 4538 deletions
+34 -4
View File
@@ -8,13 +8,16 @@ import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "./problem-details";
const HTTP_UNAUTHORIZED = 401;
const configuredBaseUrl: unknown = import.meta.env.VITE_API_BASE_URL;
const apiBaseUrl
= typeof configuredBaseUrl === "string"
? configuredBaseUrl
: "http://localhost:3000";
type AuthHandlers = {
accessToken: () => null | string | undefined;
accessToken: () => null
| string
| undefined;
clear: () => void;
generation?: () => number;
refresh: () => Promise<boolean>;
@@ -22,12 +25,16 @@ type AuthHandlers = {
const inFlight = new Set<{ invalidated: boolean; pathname: string }>();
const latestReads = new Map<string, { pending: number; sequence: number }>();
function invalidateApiRequests(isMatches: (pathname: string) => boolean) {
for (const request of inFlight)
if (isMatches(request.pathname)) request.invalidated = true;
}
const sessionState: { handlers?: AuthHandlers; hasSession: boolean } = { hasSession: false };
const sessionState: { handlers?: AuthHandlers; hasSession: boolean } = {
hasSession: false,
};
const authSessionListeners = new Set<() => void>();
async function authenticatedFetch(request: Request): Promise<Response> {
@@ -36,17 +43,21 @@ async function authenticatedFetch(request: Request): Promise<Response> {
url.searchParams.sort();
const key = `${request.method} ${url}`;
const ticket = { invalidated: false, pathname: url.pathname };
const read
= request.method === "GET"
? (latestReads.get(key) ?? { pending: 0, sequence: 0 })
: undefined;
if (read) {
read.sequence += 1;
read.pending += 1;
latestReads.set(key, read);
}
const sequence = read?.sequence;
inFlight.add(ticket);
try {
return await fetchCurrent(
request,
@@ -72,47 +83,65 @@ async function fetchCurrent(
const isAuthRequest = new URL(request.url).pathname.startsWith(
"/api/v1/auth/",
);
const session = sessionState.handlers;
const generation = session?.generation?.();
const isCurrent = () =>
(session?.generation
? generation === session.generation()
&& generation === sessionState.handlers?.generation?.()
: session === sessionState.handlers) && isLatest();
const token = session?.accessToken();
const withToken = (source: Request, value: string) => {
const headers = new Headers(source.headers);
headers.set("Authorization", `Bearer ${value}`);
return new Request(source, { headers });
};
const authorized
= token && !isAuthRequest ? withToken(request, token) : request;
const retryRequest = authorized.clone();
const response = await fetch(authorized);
await response.clone().arrayBuffer();
if (!isAuthRequest && !isCurrent())
throw new DOMException("Session changed", "AbortError");
if (isAuthRequest || !sessionState.handlers || response.status !== HTTP_UNAUTHORIZED)
if (
isAuthRequest
|| !sessionState.handlers
|| response.status !== HTTP_UNAUTHORIZED
)
return response;
let problem: unknown;
try {
problem = await response.clone().json();
}
catch {
return response;
}
if (!isApiErrorCode(apiErrorCode(problem), ApiErrorCode.Unauthorized)) return response;
if (!isApiErrorCode(apiErrorCode(problem), ApiErrorCode.Unauthorized))
return response;
if (!isCurrent()) throw new DOMException("Session changed", "AbortError");
if (!(await sessionState.handlers.refresh())) {
if (isCurrent()) sessionState.handlers.clear();
return response;
}
if (!isCurrent()) throw new DOMException("Session changed", "AbortError");
const refreshedToken = sessionState.handlers.accessToken();
if (!refreshedToken) return response;
const retried = await fetch(withToken(retryRequest, refreshedToken));
await retried.clone().arrayBuffer();
if (!isCurrent()) throw new DOMException("Session changed", "AbortError");
return retried;
}
@@ -128,6 +157,7 @@ function setAuthenticatedSession(hasSession: boolean) {
function subscribeAuthenticatedSession(listener: () => void) {
authSessionListeners.add(listener);
return () => authSessionListeners.delete(listener);
}