ci/woodpecker/push/woodpecker Pipeline was successful
Refactor asset drilldowns with combined dimension filters, parent summaries and progressive loading. Add the hidden /who read-only permission preview with scoped authorization, identity mapping and audit logging. Refine inventory counts and AI问数 presentation.
62 lines
3.3 KiB
TypeScript
62 lines
3.3 KiB
TypeScript
/** 登录身份和预览身份分别传递;切换视角会取消旧视角的在途请求。 */
|
|
let tokenGetter: () => string | null = () => null;
|
|
let previewTokenGetter: () => string | null = () => null;
|
|
let identityAbort = new AbortController();
|
|
let identityVersion = 0;
|
|
export function setTokenGetter(fn: () => string | null) { tokenGetter = fn; }
|
|
export function setPreviewTokenGetter(fn: () => string | null) { previewTokenGetter = fn; }
|
|
export function invalidateAuthenticatedRequests() {
|
|
identityVersion += 1;
|
|
identityAbort.abort();
|
|
identityAbort = new AbortController();
|
|
}
|
|
const expiredPreviewCodes = new Set(['PREVIEW_EXPIRED', 'PREVIEW_REVOKED', 'PREVIEW_INVALID', 'PREVIEW_TARGET_UNAVAILABLE']);
|
|
|
|
async function authenticatedRequest(url: string, options: RequestInit | undefined, preview: boolean, business: boolean): Promise<Response> {
|
|
const version = identityVersion;
|
|
const headers = new Headers(options?.headers);
|
|
const token = tokenGetter();
|
|
if (token) headers.set('Authorization', `Bearer ${token}`);
|
|
headers.delete('X-BI-Preview');
|
|
const previewToken = preview ? previewTokenGetter() : null;
|
|
if (previewToken) headers.set('X-BI-Preview', previewToken);
|
|
const signal = business
|
|
? options?.signal ? AbortSignal.any([options.signal, identityAbort.signal]) : identityAbort.signal
|
|
: options?.signal;
|
|
const res = await fetch(url, { ...options, headers, signal });
|
|
if (business && version !== identityVersion) throw new DOMException('权限视角已切换', 'AbortError');
|
|
if (res.status === 401 || res.status === 403 || res.status === 410) {
|
|
const body = await res.clone().json().catch(() => ({}));
|
|
if (business && version !== identityVersion) throw new DOMException('权限视角已切换', 'AbortError');
|
|
if (expiredPreviewCodes.has(body.code)) {
|
|
// 已切换到其他人的旧响应不能结束新预览。
|
|
if (previewToken && previewToken === previewTokenGetter()) window.dispatchEvent(new CustomEvent('auth:preview-ended', { detail: body.code }));
|
|
throw new Error('权限视角已失效,请重新选择用户');
|
|
}
|
|
if (res.status === 401) {
|
|
window.dispatchEvent(new CustomEvent('auth:unauthorized'));
|
|
throw new Error('Unauthorized');
|
|
}
|
|
if (body.code === 'PREVIEW_READ_ONLY') {
|
|
window.dispatchEvent(new CustomEvent('auth:preview-readonly'));
|
|
throw new Error('权限视角为只读,请退出预览后操作');
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
export function fetchAuthenticated(url: string, options?: RequestInit): Promise<Response> {
|
|
return authenticatedRequest(url, options, true, true);
|
|
}
|
|
/** 管理接口始终使用实际登录身份;仅 current/stop 附带当前预览凭证。 */
|
|
export async function fetchPreviewControl<T>(path: string, options?: RequestInit, includePreview = false): Promise<T> {
|
|
const res = await authenticatedRequest(`/api/permission-preview/${path}`, options, includePreview, false);
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || data.message || '权限视角请求失败,请重试');
|
|
return data as T;
|
|
}
|
|
export async function fetchJson<T>(url: string, options?: RequestInit): Promise<T> {
|
|
const res = await fetchAuthenticated(url, options);
|
|
if (!res.ok) throw new Error(`API error: ${res.status} ${res.statusText}`);
|
|
return res.json();
|
|
}
|