@autional-cn/shared 0.1.0-rc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/package.json +47 -0
- package/src/api/__tests__/entry-plane.test.ts +119 -0
- package/src/api/bff-client.ts +94 -0
- package/src/api/client.ts +174 -0
- package/src/api/entry-plane.ts +61 -0
- package/src/api/trace.ts +21 -0
- package/src/auth/__tests__/ac007-role-source.test.ts +124 -0
- package/src/auth/__tests__/auth-machine.test.ts +157 -0
- package/src/auth/__tests__/build-login-url.test.ts +219 -0
- package/src/auth/__tests__/get-current-role.test.ts +61 -0
- package/src/auth/__tests__/oauth-client-from-slug.test.ts +132 -0
- package/src/auth/__tests__/pkce-uniformity.test.ts +48 -0
- package/src/auth/__tests__/session-client-id.test.ts +124 -0
- package/src/auth/__tests__/slug-from-url.test.ts +112 -0
- package/src/auth/auth-machine.ts +151 -0
- package/src/auth/auth-queries.ts +118 -0
- package/src/auth/build-tenant-urls.ts +48 -0
- package/src/auth/oauth-client-from-slug.ts +119 -0
- package/src/auth/oauth-client-id-store.ts +41 -0
- package/src/auth/oauth-login.ts +291 -0
- package/src/auth/pkce.ts +141 -0
- package/src/auth/portal-config.ts +68 -0
- package/src/auth/roles.ts +102 -0
- package/src/auth/service.ts +368 -0
- package/src/auth/slug-from-url.ts +92 -0
- package/src/auth/store.ts +226 -0
- package/src/auth/tenant-route-middleware.ts +113 -0
- package/src/auth/tenant-slug-context.tsx +9 -0
- package/src/branding/BrandingInitializer.tsx +60 -0
- package/src/branding/brand-color.ts +155 -0
- package/src/branding/branding-cache.ts +71 -0
- package/src/branding/extract-branding.ts +27 -0
- package/src/branding/index.ts +15 -0
- package/src/branding/tenant-store.ts +13 -0
- package/src/branding/types.ts +14 -0
- package/src/branding/useBranding.ts +85 -0
- package/src/components/AuditStatsOnly.tsx +65 -0
- package/src/components/OAuthCallbackPage.tsx +56 -0
- package/src/components/PortalGuard.tsx +105 -0
- package/src/components/RequireAuth.tsx +142 -0
- package/src/components/TenantIndexGuard.tsx +131 -0
- package/src/components/__tests__/RequireAuth.test.tsx +244 -0
- package/src/components/__tests__/TenantIndexGuard.test.tsx +218 -0
- package/src/config/__tests__/portal-resolver.test.ts +247 -0
- package/src/config/__tests__/valid-redirect.test.ts +47 -0
- package/src/config/constants.ts +27 -0
- package/src/config/index.ts +411 -0
- package/src/config/resolve-basename.ts +57 -0
- package/src/constants/api-paths.ts +450 -0
- package/src/generated/api.ts +12566 -0
- package/src/generated/queries.ts +12623 -0
- package/src/generated/types.ts +13888 -0
- package/src/hooks/__tests__/useBootstrap.test.tsx +91 -0
- package/src/hooks/__tests__/useCurrentRole.test.tsx +122 -0
- package/src/hooks/useAuth.ts +239 -0
- package/src/hooks/useBootstrap.ts +74 -0
- package/src/hooks/useCurrentRole.ts +18 -0
- package/src/hooks/useLogout.ts +33 -0
- package/src/hooks/usePermission.ts +116 -0
- package/src/hooks/useSodMode.ts +73 -0
- package/src/index.ts +209 -0
- package/src/lib/push.ts +41 -0
- package/src/seo/dom-utils.ts +94 -0
- package/src/seo/index.ts +6 -0
- package/src/seo/jsonld.ts +29 -0
- package/src/seo/types.ts +29 -0
- package/src/seo/usePageMeta.ts +17 -0
- package/src/seo/usePageTitle.ts +14 -0
- package/src/seo/useSEO.ts +80 -0
- package/src/types/index.ts +185 -0
- package/src/utils/__tests__/mock-test.ts +15 -0
- package/src/utils/browser.ts +6 -0
- package/src/utils/case.ts +44 -0
- package/src/utils/error.ts +26 -0
- package/src/utils/format.ts +63 -0
- package/src/utils/password-transmission.test.ts +145 -0
- package/src/utils/password-transmission.ts +253 -0
- package/src/utils/response.ts +62 -0
- package/src/utils/vite.ts +4 -0
- package/src/vite-env.d.ts +18 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
// Autional Web Environment Configuration
|
|
2
|
+
// Override priority: runtime (window.__APP_CONFIG__) > build-time (import.meta.env) > defaults
|
|
3
|
+
|
|
4
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
5
|
+
|
|
6
|
+
// ─── Runtime Config ───
|
|
7
|
+
function getRuntimeConfig(): Record<string, string> | undefined {
|
|
8
|
+
try {
|
|
9
|
+
return (globalThis as any)['window']?.__APP_CONFIG__;
|
|
10
|
+
} catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const getEnv = (key: string, fallback: string): string => {
|
|
16
|
+
const runtime = getRuntimeConfig();
|
|
17
|
+
if (key === 'BASE_PATH') {
|
|
18
|
+
const viteBase = (() => {
|
|
19
|
+
try {
|
|
20
|
+
const baseUrl = (import.meta as any).env?.BASE_URL;
|
|
21
|
+
if (!baseUrl || baseUrl === '/') return undefined;
|
|
22
|
+
return baseUrl.replace(/\/$/, '');
|
|
23
|
+
} catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
})();
|
|
27
|
+
return runtime?.[key] ?? viteBase ?? fallback;
|
|
28
|
+
}
|
|
29
|
+
const val =
|
|
30
|
+
runtime?.[key] ??
|
|
31
|
+
(() => {
|
|
32
|
+
try {
|
|
33
|
+
return (import.meta as any).env?.[key];
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
})() ??
|
|
38
|
+
fallback;
|
|
39
|
+
|
|
40
|
+
if ((import.meta as any).env?.PROD && val.startsWith('http://localhost')) {
|
|
41
|
+
console.error(
|
|
42
|
+
`[Autional] CRITICAL: ${key} resolved to ${val}. ` +
|
|
43
|
+
`env.js is missing or broken. Check deployment pipeline.`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return val;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ─── Base Path ───
|
|
51
|
+
export const BASE_PATH = getEnv('BASE_PATH', '/');
|
|
52
|
+
|
|
53
|
+
const APP_PORTAL_SLUGS = ['admin', 'developer', 'security', 'platform', 'status', 'trust'] as const;
|
|
54
|
+
void APP_PORTAL_SLUGS;
|
|
55
|
+
|
|
56
|
+
export function getRouterBasename(): string {
|
|
57
|
+
const base = BASE_PATH.replace(/\/$/, '') || '/';
|
|
58
|
+
if (typeof window === 'undefined') return base;
|
|
59
|
+
if (base === '/') return '/';
|
|
60
|
+
const path = window.location.pathname;
|
|
61
|
+
if (path === '/') return '/';
|
|
62
|
+
const match = path.match(/^\/([a-zA-Z0-9_-]+)(\/[a-z-]+)/);
|
|
63
|
+
if (match) {
|
|
64
|
+
const portal = match[2].replace(/^\//, '').split('/')[0];
|
|
65
|
+
if (getAppPortalSlugs().includes(portal)) {
|
|
66
|
+
return '/' + match[1] + '/' + portal;
|
|
67
|
+
}
|
|
68
|
+
return '/' + match[1];
|
|
69
|
+
}
|
|
70
|
+
return base;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const ROUTER_BASENAME = getRouterBasename();
|
|
74
|
+
|
|
75
|
+
export const VITE_BASE = BASE_PATH.endsWith('/') ? BASE_PATH : BASE_PATH + '/';
|
|
76
|
+
|
|
77
|
+
// ─── Portal URL Resolution (API-driven, runtime getRootDomain fallback) ───
|
|
78
|
+
export interface PortalEntry {
|
|
79
|
+
host: string;
|
|
80
|
+
base: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const PORTAL_DEFAULTS: Record<string, PortalEntry> = {
|
|
84
|
+
auth: { host: 'auth', base: '' },
|
|
85
|
+
admin: { host: 'admin', base: '' },
|
|
86
|
+
developer: { host: 'developer', base: '' },
|
|
87
|
+
user: { host: 'user', base: '' },
|
|
88
|
+
security: { host: 'security', base: '' },
|
|
89
|
+
status: { host: 'status', base: '' },
|
|
90
|
+
landing: { host: '', base: '' },
|
|
91
|
+
authenticator: { host: 'authenticator', base: '' },
|
|
92
|
+
trust: { host: 'trust', base: '' },
|
|
93
|
+
platform: { host: 'platform', base: '' },
|
|
94
|
+
brand: { host: 'brand', base: '' },
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
let portalConfig: Record<string, PortalEntry> | null = null;
|
|
98
|
+
|
|
99
|
+
export async function initPortalConfig(
|
|
100
|
+
apiBaseUrl: string,
|
|
101
|
+
platformTenantId?: string,
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
if (portalConfig) return;
|
|
104
|
+
try {
|
|
105
|
+
const tid = platformTenantId || '';
|
|
106
|
+
if (!tid) {
|
|
107
|
+
portalConfig = PORTAL_DEFAULTS;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const res = await fetch(
|
|
111
|
+
`${apiBaseUrl}/tenant/api/v1/admin/tenants/${tid}/applications?type=portal&is_platform=true`,
|
|
112
|
+
);
|
|
113
|
+
const json = await res.json();
|
|
114
|
+
if (json.code === 0 && Array.isArray(json.data) && json.data.length > 0) {
|
|
115
|
+
const config: Record<string, PortalEntry> = {};
|
|
116
|
+
for (const app of json.data) {
|
|
117
|
+
config[app.code] = {
|
|
118
|
+
host: app.config?.portal?.host || PORTAL_DEFAULTS[app.code]?.host || app.code,
|
|
119
|
+
base: app.config?.portal?.base || PORTAL_DEFAULTS[app.code]?.base || '',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
portalConfig = config;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
/* ignore */
|
|
127
|
+
}
|
|
128
|
+
portalConfig = PORTAL_DEFAULTS;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 旧 env.js 直连 URL 键(VITE_*_URL)→ portalId 映射(向后兼容)
|
|
132
|
+
const PORTAL_KEY_MAP: Record<string, string> = {
|
|
133
|
+
auth: 'VITE_AUTH_PAGES_URL',
|
|
134
|
+
admin: 'VITE_ADMIN_CONSOLE_URL',
|
|
135
|
+
developer: 'VITE_DEVELOPER_PORTAL_URL',
|
|
136
|
+
user: 'VITE_END_USER_PORTAL_URL',
|
|
137
|
+
security: 'VITE_SECURITY_DASHBOARD_URL',
|
|
138
|
+
status: 'VITE_STATUS_PAGE_URL',
|
|
139
|
+
landing: 'VITE_LANDING_SITE_URL',
|
|
140
|
+
authenticator: 'VITE_AUTHENTICATOR_APP_URL',
|
|
141
|
+
trust: 'VITE_TRUST_CENTER_URL',
|
|
142
|
+
platform: 'VITE_PLATFORM_CONSOLE_URL',
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 读取构建期注入的 VITE_PORTAL_CONFIG(env.js → window.__APP_CONFIG__)。
|
|
147
|
+
* 不存在时返回 null(兼容旧 env.js)。
|
|
148
|
+
*/
|
|
149
|
+
function getPortalConfigFromEnv(): Record<string, PortalEntry> | null {
|
|
150
|
+
try {
|
|
151
|
+
return (globalThis as any)['window']?.__APP_CONFIG__?.VITE_PORTAL_CONFIG || null;
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function resolvePortalUrl(entry: PortalEntry, hostname: string, slug?: string): string {
|
|
158
|
+
const { host, base } = entry;
|
|
159
|
+
|
|
160
|
+
// 推导根域名:如果当前域名以已知 Portal 子域名开头,剥离前缀得到根
|
|
161
|
+
// 例如 auth.iam.tianv.local → iam.tianv.local,auth.autional.local → autional.local
|
|
162
|
+
const knownHosts = Object.values(getPortalEntries())
|
|
163
|
+
.map((e) => e.host)
|
|
164
|
+
.filter(Boolean);
|
|
165
|
+
const parts = hostname.split('.');
|
|
166
|
+
const root =
|
|
167
|
+
parts.length >= 3 && knownHosts.includes(parts[0])
|
|
168
|
+
? parts.slice(1).join('.')
|
|
169
|
+
: getRootDomain(hostname);
|
|
170
|
+
|
|
171
|
+
const fullHost = host ? `${host}.${root}` : root;
|
|
172
|
+
let url = `${window.location.protocol}//${fullHost}${base || ''}`;
|
|
173
|
+
if (slug) {
|
|
174
|
+
const slugPath = slug.startsWith('/') ? slug : '/' + slug;
|
|
175
|
+
url += slugPath;
|
|
176
|
+
}
|
|
177
|
+
return url;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 租户段门户白名单:仅这些门户的入口带 /<tenantSlug>(门户 URL 统一方案 —— user/security/admin/authenticator,auth 一并统一)。
|
|
181
|
+
// 其余门户(platform/status/trust/developer/brand/landing)是域根应用,拼租户段会命中不存在的路由 ⇒ 空白/404
|
|
182
|
+
//(F-W8:auth dashboard 磁贴曾因此断 4/8)。新增租户段门户时须同步在此登记。
|
|
183
|
+
const SLUG_PORTALS: ReadonlySet<string> = new Set([
|
|
184
|
+
'auth',
|
|
185
|
+
'admin',
|
|
186
|
+
'user',
|
|
187
|
+
'security',
|
|
188
|
+
'authenticator',
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
export function getPortalUrl(portalId: string, slug?: string): string {
|
|
192
|
+
if (typeof window === 'undefined') return '';
|
|
193
|
+
const hostname = window.location.hostname;
|
|
194
|
+
const effectiveSlug = SLUG_PORTALS.has(portalId) ? slug : undefined;
|
|
195
|
+
|
|
196
|
+
// 1. env-driven VITE_PORTAL_CONFIG(构建期写入 env.js,多环境域名解析架构)
|
|
197
|
+
const envConfig = getPortalConfigFromEnv();
|
|
198
|
+
if (envConfig?.[portalId])
|
|
199
|
+
return resolvePortalUrl(envConfig[portalId], hostname, effectiveSlug);
|
|
200
|
+
|
|
201
|
+
// 2. 运行时 API 配置(initPortalConfig — platform tenant apps)
|
|
202
|
+
if (portalConfig?.[portalId])
|
|
203
|
+
return resolvePortalUrl(portalConfig[portalId], hostname, effectiveSlug);
|
|
204
|
+
|
|
205
|
+
// 3. 旧 env.js 直连 URL(VITE_*_URL,与 VITE_PORTAL_CONFIG 不共存)
|
|
206
|
+
const cfg = getRuntimeConfig();
|
|
207
|
+
const key = PORTAL_KEY_MAP[portalId];
|
|
208
|
+
if (cfg?.[key]) {
|
|
209
|
+
let url = cfg[key];
|
|
210
|
+
if (effectiveSlug)
|
|
211
|
+
url =
|
|
212
|
+
url.replace(/\/+$/, '') +
|
|
213
|
+
(effectiveSlug.startsWith('/') ? effectiveSlug : '/' + effectiveSlug);
|
|
214
|
+
return url;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 4. 默认表
|
|
218
|
+
const def = PORTAL_DEFAULTS[portalId];
|
|
219
|
+
if (def) return resolvePortalUrl(def, hostname, effectiveSlug);
|
|
220
|
+
|
|
221
|
+
// 5. 未知 portal:回退当前 origin + /portalId(未知门户不在白名单 ⇒ 不拼租户段)
|
|
222
|
+
let fallback = `${window.location.origin}/${portalId}`;
|
|
223
|
+
if (effectiveSlug)
|
|
224
|
+
fallback += effectiveSlug.startsWith('/') ? effectiveSlug : '/' + effectiveSlug;
|
|
225
|
+
return fallback;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 导出 portal 配置(供 getPortalUrl 内部使用)
|
|
229
|
+
// 三源合并:env(VITE_PORTAL_CONFIG)优先 → 运行时 API 配置补充 → defaults 兜底,
|
|
230
|
+
// 避免 env 只配部分 portal 时 knownHosts 不完整导致域名推导偏差
|
|
231
|
+
function getPortalEntries(): Record<string, PortalEntry> {
|
|
232
|
+
const merged: Record<string, PortalEntry> = { ...PORTAL_DEFAULTS, ...portalConfig };
|
|
233
|
+
const envConfig = getPortalConfigFromEnv();
|
|
234
|
+
if (envConfig) {
|
|
235
|
+
Object.assign(merged, envConfig);
|
|
236
|
+
}
|
|
237
|
+
return merged;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function getAppPortalSlugs(): string[] {
|
|
241
|
+
return ['admin', 'developer', 'security', 'platform', 'status', 'trust'];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function getRootDomain(hostname: string, _rootParts?: number): string {
|
|
245
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) return hostname;
|
|
246
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1') return hostname;
|
|
247
|
+
|
|
248
|
+
// 优先从 env.js 读注入的根域名
|
|
249
|
+
const knownRoot = getEnv('VITE_ROOT_DOMAIN', '');
|
|
250
|
+
if (knownRoot && (hostname === knownRoot || hostname.endsWith('.' + knownRoot))) {
|
|
251
|
+
return knownRoot;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const parts = hostname.split('.');
|
|
255
|
+
|
|
256
|
+
// .local 结尾(如 iam.tianv.local):取 3 段
|
|
257
|
+
if (hostname.endsWith('.local') && parts.length >= 3) {
|
|
258
|
+
return parts.slice(-3).join('.');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 常见两段式 TLD(如 .com.cn、.co.uk)
|
|
262
|
+
const twoPartTLDs = new Set(['com.cn', 'co.uk', 'co.jp', 'com.au', 'co.nz']);
|
|
263
|
+
if (parts.length >= 4) {
|
|
264
|
+
const lastTwo = parts.slice(-2).join('.');
|
|
265
|
+
if (twoPartTLDs.has(lastTwo)) return parts.slice(-3).join('.');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 常见单段 TLD(.com/.net 等):与 .local / 两段式 TLD 一致,超过 3 段时保留 3 段
|
|
269
|
+
// app.iam.tianv.com → iam.tianv.com(保留中间段 iam,避免推导成 tianv.com)
|
|
270
|
+
// app.autional.com → autional.com(3 段走默认规则取后 2 段)
|
|
271
|
+
const singlePartTLDs = new Set([
|
|
272
|
+
'com',
|
|
273
|
+
'net',
|
|
274
|
+
'org',
|
|
275
|
+
'io',
|
|
276
|
+
'co',
|
|
277
|
+
'ai',
|
|
278
|
+
'dev',
|
|
279
|
+
'app',
|
|
280
|
+
'me',
|
|
281
|
+
'info',
|
|
282
|
+
'biz',
|
|
283
|
+
'cn',
|
|
284
|
+
'tech',
|
|
285
|
+
'site',
|
|
286
|
+
]);
|
|
287
|
+
if (parts.length >= 4 && singlePartTLDs.has(parts[parts.length - 1])) {
|
|
288
|
+
return parts.slice(-3).join('.');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 标准情况:取后 2 段(domain.tld)
|
|
292
|
+
if (parts.length >= 2) return parts.slice(-2).join('.');
|
|
293
|
+
return hostname;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Resolve all portal URLs for redirect origin validation. */
|
|
297
|
+
function getAllowedRedirectOrigins(): string[] {
|
|
298
|
+
const portalIds = [
|
|
299
|
+
'auth',
|
|
300
|
+
'admin',
|
|
301
|
+
'developer',
|
|
302
|
+
'user',
|
|
303
|
+
'security',
|
|
304
|
+
'status',
|
|
305
|
+
'landing',
|
|
306
|
+
'authenticator',
|
|
307
|
+
'trust',
|
|
308
|
+
'platform',
|
|
309
|
+
'brand',
|
|
310
|
+
];
|
|
311
|
+
return portalIds
|
|
312
|
+
.map((id) => {
|
|
313
|
+
try {
|
|
314
|
+
return getPortalUrl(id);
|
|
315
|
+
} catch {
|
|
316
|
+
return '';
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
.filter(Boolean);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ─── Service URLs (runtime-resolved via getPortalUrl) ───
|
|
323
|
+
export function getAUTH_PAGES_URL(): string {
|
|
324
|
+
return getPortalUrl('auth');
|
|
325
|
+
}
|
|
326
|
+
export function getADMIN_CONSOLE_URL(): string {
|
|
327
|
+
return getPortalUrl('admin');
|
|
328
|
+
}
|
|
329
|
+
export function getDEVELOPER_PORTAL_URL(): string {
|
|
330
|
+
return getPortalUrl('developer');
|
|
331
|
+
}
|
|
332
|
+
export function getEND_USER_PORTAL_URL(): string {
|
|
333
|
+
return getPortalUrl('user');
|
|
334
|
+
}
|
|
335
|
+
export function getSECURITY_DASHBOARD_URL(): string {
|
|
336
|
+
return getPortalUrl('security');
|
|
337
|
+
}
|
|
338
|
+
export function getSTATUS_PAGE_URL(): string {
|
|
339
|
+
return getPortalUrl('status');
|
|
340
|
+
}
|
|
341
|
+
export function getLANDING_SITE_URL(): string {
|
|
342
|
+
return getPortalUrl('landing');
|
|
343
|
+
}
|
|
344
|
+
export function getAUTHENTICATOR_APP_URL(): string {
|
|
345
|
+
return getPortalUrl('authenticator');
|
|
346
|
+
}
|
|
347
|
+
export function getTRUST_CENTER_URL(): string {
|
|
348
|
+
return getPortalUrl('trust');
|
|
349
|
+
}
|
|
350
|
+
export function getPLATFORM_CONSOLE_URL(): string {
|
|
351
|
+
return getPortalUrl('platform');
|
|
352
|
+
}
|
|
353
|
+
export const API_BASE_URL = getEnv('VITE_API_BASE_URL', '/bff');
|
|
354
|
+
|
|
355
|
+
export function getApiBaseUrl(): string {
|
|
356
|
+
return API_BASE_URL;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function isValidRedirect(url: string): boolean {
|
|
360
|
+
// 仅允许站内相对路径;拒绝 //evil.com、/\、/%5c 等协议相对/反斜杠变体
|
|
361
|
+
if (
|
|
362
|
+
url.startsWith('/') &&
|
|
363
|
+
!url.startsWith('//') &&
|
|
364
|
+
!url.startsWith('/\\') &&
|
|
365
|
+
!url.startsWith('/%5c')
|
|
366
|
+
) {
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
const target = new URL(url);
|
|
371
|
+
if (typeof window !== 'undefined' && target.origin === window.location.origin) {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
const origins = getAllowedRedirectOrigins();
|
|
375
|
+
return origins.some((o) => {
|
|
376
|
+
if (!o) return false;
|
|
377
|
+
try {
|
|
378
|
+
const origin = new URL(o);
|
|
379
|
+
return target.origin === origin.origin;
|
|
380
|
+
} catch {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
} catch {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ─── Navigation Helpers ───
|
|
390
|
+
export function appPath(path: string): string {
|
|
391
|
+
const base = BASE_PATH.replace(/\/$/, '');
|
|
392
|
+
const cleanPath = path.startsWith('/') ? path : '/' + path;
|
|
393
|
+
return base + cleanPath;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function navigateTo(path: string): void {
|
|
397
|
+
if (typeof window !== 'undefined') {
|
|
398
|
+
window.location.href = appPath(path);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function crossAppUrl(base: string, path?: string): string {
|
|
403
|
+
const normalizedBase = base.replace(/\/+$/, '');
|
|
404
|
+
const normalizedPath = path && path !== '/' ? (path.startsWith('/') ? path : '/' + path) : '';
|
|
405
|
+
const full = normalizedBase + normalizedPath;
|
|
406
|
+
if (/^https?:\/\//.test(full)) return full;
|
|
407
|
+
if (typeof window !== 'undefined') {
|
|
408
|
+
return window.location.origin + full;
|
|
409
|
+
}
|
|
410
|
+
return full;
|
|
411
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { BASE_PATH } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the React Router basename based on the current URL.
|
|
5
|
+
*
|
|
6
|
+
* This function is used by the END-USER PORTAL only (see apps/end-user-portal/src/main.tsx).
|
|
7
|
+
* In the user portal every route lives under /{tenantSlug}/..., so the basename is
|
|
8
|
+
* always "/{tenantSlug}" — the second path segment is an APPLICATION ROUTE (e.g. /security,
|
|
9
|
+
* /profile, /devices), never a portal slug. Treating the second segment as a portal slug
|
|
10
|
+
* (as the shared getAppPortalSlugs() table does for the admin/developer portals) hijacks
|
|
11
|
+
* URLs such as /acme-corp/security and renders the Dashboard instead of the security page.
|
|
12
|
+
*
|
|
13
|
+
* Returns "/{firstSegment}" when the URL matches /{segment}/{...}, otherwise BASE_PATH.
|
|
14
|
+
*
|
|
15
|
+
* Also syncs window.__APP_CONFIG__.BASE_PATH so oauth-login.ts
|
|
16
|
+
* derives a redirect_uri consistent with the actual basename.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* URL: /acme-corp/security → basename: "/acme-corp"
|
|
20
|
+
* URL: /acme-corp/profile → basename: "/acme-corp"
|
|
21
|
+
* URL: /oauth/callback → basename: BASE_PATH (reserved)
|
|
22
|
+
*/
|
|
23
|
+
export function resolvePortalBasename(): string {
|
|
24
|
+
const base = (BASE_PATH || '').replace(/\/*$/, '') || '/';
|
|
25
|
+
|
|
26
|
+
if (typeof window === 'undefined') return base;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const path = window.location.pathname;
|
|
30
|
+
const RESERVED = ['oauth', 'bff', 'api', 'health', 'env.js', 'assets', 'docs', 'scalar'];
|
|
31
|
+
|
|
32
|
+
const m = path.match(/^\/([a-zA-Z0-9_-]+)(\/[a-z-]+)/);
|
|
33
|
+
let basename = base;
|
|
34
|
+
if (m && !RESERVED.includes(m[1])) {
|
|
35
|
+
// User portal: the first segment is the tenant slug; the second segment
|
|
36
|
+
// (and beyond) are app routes that must stay inside the router path.
|
|
37
|
+
basename = '/' + m[1];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Sync __APP_CONFIG__.BASE_PATH for oauth-login.ts redirect_uri derivation
|
|
41
|
+
syncAppConfigPath(basename);
|
|
42
|
+
return basename;
|
|
43
|
+
} catch {
|
|
44
|
+
return base;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function syncAppConfigPath(basename: string): void {
|
|
49
|
+
try {
|
|
50
|
+
if (!(globalThis as any).__APP_CONFIG__) {
|
|
51
|
+
(globalThis as any).__APP_CONFIG__ = {};
|
|
52
|
+
}
|
|
53
|
+
(globalThis as any).__APP_CONFIG__.BASE_PATH = basename;
|
|
54
|
+
} catch {
|
|
55
|
+
// ignore
|
|
56
|
+
}
|
|
57
|
+
}
|