124 lines
5.4 KiB
TypeScript
124 lines
5.4 KiB
TypeScript
const gatewayAPIBase = (import.meta.env.VITE_GATEWAY_API_BASE_URL ?? 'http://localhost:8088').replace(/\/$/, '');
|
|
|
|
export type OIDCRuntimeConfiguration = {
|
|
enabled: boolean;
|
|
oidcLogin: boolean;
|
|
loginUrl?: string;
|
|
logoutUrl?: string;
|
|
status: string;
|
|
};
|
|
|
|
const disabledRuntime: OIDCRuntimeConfiguration = { enabled: false, oidcLogin: false, status: 'disabled' };
|
|
let runtimeConfiguration = disabledRuntime;
|
|
let runtimeLoaded = false;
|
|
let runtimeRequest: Promise<OIDCRuntimeConfiguration> | null = null;
|
|
|
|
export type OIDCCallbackError = {
|
|
code: string;
|
|
reason?: string;
|
|
diagnosticId?: string;
|
|
message: string;
|
|
retryable: boolean;
|
|
};
|
|
|
|
const callbackErrors: Record<string, Omit<OIDCCallbackError, 'code'>> = {
|
|
OIDC_LOGIN_INVALID: { message: '统一认证登录事务无效或已过期,请重新登录', retryable: true },
|
|
OIDC_TOKEN_EXCHANGE_FAILED: { message: '统一认证登录结果无效,请重新登录', retryable: false },
|
|
OIDC_SESSION_STORE_UNAVAILABLE: { message: '登录会话存储暂时不可用,请稍后重试', retryable: false },
|
|
GATEWAY_USER_NOT_PROVISIONED: { message: '该账号尚未开通 EasyAI Gateway', retryable: false },
|
|
GATEWAY_USER_DISABLED: { message: '该 Gateway 账号已停用,请联系管理员', retryable: false },
|
|
GATEWAY_TENANT_UNAVAILABLE: { message: 'Gateway 租户尚未就绪,请联系管理员', retryable: false },
|
|
};
|
|
|
|
const loginTransactionFailureMessages: Record<string, string> = {
|
|
COOKIE_MISSING: '浏览器未携带统一认证登录事务 Cookie,请允许 localhost Cookie 后重新登录',
|
|
TRANSACTION_INVALID: '统一认证登录事务无法验证或已过期,请重新登录',
|
|
STATE_MISMATCH: '认证回调与当前登录事务不匹配,请关闭重复登录页面后重新登录',
|
|
AUTHORIZATION_RESPONSE_MISSING: '认证中心回调缺少必要参数,请重新登录',
|
|
};
|
|
|
|
export function oidcLoginEnabled() {
|
|
return runtimeConfiguration.enabled && runtimeConfiguration.oidcLogin;
|
|
}
|
|
|
|
export function oidcBrowserSessionEnabled() {
|
|
return oidcLoginEnabled();
|
|
}
|
|
|
|
export async function loadOIDCRuntimeConfiguration(force = false): Promise<OIDCRuntimeConfiguration> {
|
|
if (!force && runtimeLoaded) return runtimeConfiguration;
|
|
if (!force && runtimeRequest) return runtimeRequest;
|
|
runtimeRequest = fetch(`${gatewayAPIBase}/api/v1/public/identity`, {
|
|
credentials: 'include',
|
|
headers: { Accept: 'application/json' },
|
|
}).then(async (response) => {
|
|
if (!response.ok) return disabledRuntime;
|
|
const value = await response.json() as Partial<OIDCRuntimeConfiguration>;
|
|
if (typeof value.enabled !== 'boolean' || typeof value.oidcLogin !== 'boolean' || typeof value.status !== 'string') {
|
|
return disabledRuntime;
|
|
}
|
|
return {
|
|
enabled: value.enabled,
|
|
oidcLogin: value.oidcLogin,
|
|
status: value.status,
|
|
...(typeof value.loginUrl === 'string' ? { loginUrl: value.loginUrl } : {}),
|
|
...(typeof value.logoutUrl === 'string' ? { logoutUrl: value.logoutUrl } : {}),
|
|
};
|
|
}).catch(() => disabledRuntime).then((configuration) => {
|
|
runtimeConfiguration = configuration;
|
|
runtimeLoaded = true;
|
|
runtimeRequest = null;
|
|
return configuration;
|
|
});
|
|
return runtimeRequest;
|
|
}
|
|
|
|
export async function startOIDCLogin() {
|
|
const configuration = await loadOIDCRuntimeConfiguration(true);
|
|
if (!configuration.enabled || !configuration.oidcLogin || !configuration.loginUrl) throw new Error('统一认证未配置');
|
|
const returnTo = `${window.location.pathname}${window.location.search}${window.location.hash}` || '/';
|
|
const loginURL = new URL(identityEndpointURL(configuration.loginUrl), window.location.origin);
|
|
loginURL.searchParams.set('returnTo', returnTo.startsWith('/') ? returnTo : '/');
|
|
window.location.assign(loginURL.toString());
|
|
}
|
|
|
|
export async function startOIDCLogout() {
|
|
const configuration = await loadOIDCRuntimeConfiguration(true);
|
|
if (!configuration.enabled || !configuration.oidcLogin || !configuration.logoutUrl) return false;
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = identityEndpointURL(configuration.logoutUrl);
|
|
form.style.display = 'none';
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
return true;
|
|
}
|
|
|
|
function identityEndpointURL(path: string) {
|
|
if (/^https:\/\//i.test(path) || /^http:\/\/(localhost|127\.0\.0\.1)(:|\/)/i.test(path)) return path;
|
|
return `${gatewayAPIBase}/${path.replace(/^\//, '')}`;
|
|
}
|
|
|
|
export function consumeOIDCCallbackError() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const code = params.get('oidcError') ?? '';
|
|
if (!code) return null;
|
|
const rawReason = params.get('oidcErrorReason') ?? '';
|
|
const reason = code === 'OIDC_LOGIN_INVALID' && loginTransactionFailureMessages[rawReason] ? rawReason : '';
|
|
const rawDiagnosticId = params.get('oidcDiagnosticId') ?? '';
|
|
const diagnosticId = /^[A-Za-z0-9_-]{6,64}$/.test(rawDiagnosticId) ? rawDiagnosticId : '';
|
|
params.delete('oidcError');
|
|
params.delete('oidcErrorReason');
|
|
params.delete('oidcDiagnosticId');
|
|
const query = params.toString();
|
|
window.history.replaceState({}, '', `${window.location.pathname}${query ? `?${query}` : ''}${window.location.hash}`);
|
|
const callbackError = callbackErrors[code] ?? { message: '统一认证登录失败,请重试', retryable: false };
|
|
return {
|
|
code,
|
|
...(reason ? { reason } : {}),
|
|
...(diagnosticId ? { diagnosticId } : {}),
|
|
...callbackError,
|
|
...(reason ? { message: loginTransactionFailureMessages[reason] } : {}),
|
|
};
|
|
}
|