101 lines
3.6 KiB
TypeScript
101 lines
3.6 KiB
TypeScript
'use client';
|
|
|
|
const WORKS_OPERATIONS_AUTH_STORAGE_KEY = 'works-square.operations.auth.v1';
|
|
|
|
export function worksAccessToken(): string | null {
|
|
try {
|
|
const raw = window.localStorage.getItem(WORKS_OPERATIONS_AUTH_STORAGE_KEY);
|
|
if (!raw) return null;
|
|
const session = JSON.parse(raw) as { accessToken?: unknown; expiresAt?: unknown };
|
|
if (typeof session.accessToken !== 'string' || !session.accessToken.trim()) return null;
|
|
if (typeof session.expiresAt === 'number' && session.expiresAt <= Date.now()) return null;
|
|
return session.accessToken.trim();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function opsApiPath(pathname: string): string {
|
|
const path = pathname.startsWith('/') ? pathname : `/${pathname}`;
|
|
return `${process.env.NEXT_PUBLIC_OPENMAIC_BASE_PATH ?? ''}${path}`;
|
|
}
|
|
|
|
export function opsApiFetch(pathname: string, init: RequestInit = {}): Promise<Response> {
|
|
const headers = new Headers(init.headers);
|
|
const token = worksAccessToken();
|
|
if (token) headers.set('Authorization', `Bearer ${token}`);
|
|
headers.set('Accept', headers.get('Accept') || 'application/json');
|
|
return fetch(opsApiPath(pathname), { ...init, headers });
|
|
}
|
|
|
|
const OPS_PUBLIC_ASSET_ROOTS = [
|
|
'/avatars/',
|
|
'/logos/',
|
|
'/vendor/',
|
|
'/mailuo-logo.png',
|
|
'/openmaic-mark.png',
|
|
'/logo-horizontal.png',
|
|
] as const;
|
|
|
|
function opsRootRelativePath(pathname: string): string | null {
|
|
if (pathname === '/api' || pathname.startsWith('/api/')) return opsApiPath(pathname);
|
|
if (OPS_PUBLIC_ASSET_ROOTS.some((root) => pathname === root || pathname.startsWith(root))) {
|
|
return opsApiPath(pathname);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Install before child effects so legacy root-relative API calls remain basePath-aware. */
|
|
export function installOpsBrowserBoundary(): () => void {
|
|
if (process.env.NEXT_PUBLIC_OPENMAIC_DEPLOYMENT_ROLE !== 'ops') return () => undefined;
|
|
const nativeFetch = window.fetch.bind(window);
|
|
const wrappedFetch: typeof window.fetch = (input, init = {}) => {
|
|
const mapped = typeof input === 'string' ? opsRootRelativePath(input) : null;
|
|
if (!mapped) return nativeFetch(input, init);
|
|
if (typeof input !== 'string' || !input.startsWith('/api')) return nativeFetch(mapped, init);
|
|
const headers = new Headers(init.headers);
|
|
const token = worksAccessToken();
|
|
if (token && !headers.has('Authorization')) headers.set('Authorization', `Bearer ${token}`);
|
|
return nativeFetch(mapped, { ...init, headers });
|
|
};
|
|
window.fetch = wrappedFetch;
|
|
|
|
const rewriteElement = (element: Element) => {
|
|
for (const attribute of ['src', 'poster']) {
|
|
const value = element.getAttribute(attribute);
|
|
if (!value) continue;
|
|
const mapped = opsRootRelativePath(value);
|
|
if (mapped && mapped !== value && !value.startsWith('/api')) {
|
|
element.setAttribute(attribute, mapped);
|
|
}
|
|
}
|
|
};
|
|
const rewriteTree = (root: ParentNode) => {
|
|
if (root instanceof Element) rewriteElement(root);
|
|
root.querySelectorAll('[src], [poster]').forEach(rewriteElement);
|
|
};
|
|
rewriteTree(document);
|
|
const observer = new MutationObserver((mutations) => {
|
|
for (const mutation of mutations) {
|
|
if (mutation.type === 'attributes') rewriteElement(mutation.target as Element);
|
|
for (const node of mutation.addedNodes) {
|
|
if (node instanceof Element) rewriteTree(node);
|
|
}
|
|
}
|
|
});
|
|
observer.observe(document.documentElement, {
|
|
subtree: true,
|
|
childList: true,
|
|
attributes: true,
|
|
attributeFilter: ['src', 'poster'],
|
|
});
|
|
|
|
let closed = false;
|
|
return () => {
|
|
if (closed) return;
|
|
closed = true;
|
|
observer.disconnect();
|
|
if (window.fetch === wrappedFetch) window.fetch = nativeFetch;
|
|
};
|
|
}
|