@autional/shared 0.0.0-stage → 0.1.0-rc.32
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 +49 -3
- package/src/api/__tests__/contract.test.ts +172 -0
- package/src/api/__tests__/entry-plane.test.ts +119 -0
- package/src/api/__tests__/namespace.test.ts +27 -0
- package/src/api/__tests__/preflight-refresh.test.ts +89 -0
- package/src/api/bff-client.ts +94 -0
- package/src/api/client.ts +191 -0
- package/src/api/entry-plane.ts +61 -0
- package/src/api/namespace.ts +14 -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__/auth-trace.test.ts +321 -0
- package/src/auth/__tests__/build-login-url.test.ts +259 -0
- package/src/auth/__tests__/get-current-role.test.ts +61 -0
- package/src/auth/__tests__/jwt-payload.test.ts +49 -0
- package/src/auth/__tests__/logout-redirect.test.ts +44 -0
- package/src/auth/__tests__/logout-revoke.test.ts +219 -0
- package/src/auth/__tests__/oauth-callback-error-codes.test.ts +69 -0
- package/src/auth/__tests__/oauth-client-from-slug.test.ts +132 -0
- package/src/auth/__tests__/oauth-initiate-stay.test.ts +118 -0
- package/src/auth/__tests__/oauth-login-user-resolution.test.ts +185 -0
- package/src/auth/__tests__/pkce-uniformity.test.ts +48 -0
- package/src/auth/__tests__/session-client-id.test.ts +147 -0
- package/src/auth/__tests__/session-terminal-redirect.test.ts +138 -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/auth-trace.ts +511 -0
- package/src/auth/build-tenant-urls.ts +48 -0
- package/src/auth/jwt-payload.ts +21 -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 +388 -0
- package/src/auth/pkce.ts +141 -0
- package/src/auth/portal-config.ts +68 -0
- package/src/auth/roles.ts +120 -0
- package/src/auth/service.ts +433 -0
- package/src/auth/slug-from-url.ts +92 -0
- package/src/auth/store.ts +227 -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/__tests__/brand-vars.test.ts +126 -0
- package/src/branding/brand-color.ts +193 -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 +101 -0
- package/src/components/AuditStatsOnly.tsx +65 -0
- package/src/components/OAuthCallbackPage.tsx +83 -0
- package/src/components/PortalGuard.tsx +105 -0
- package/src/components/RequireAuth.tsx +220 -0
- package/src/components/TenantIndexGuard.tsx +131 -0
- package/src/components/TenantRootRedirect.tsx +49 -0
- package/src/components/__tests__/RequireAuth.adversarial.test.tsx +165 -0
- package/src/components/__tests__/RequireAuth.stay.test.tsx +131 -0
- package/src/components/__tests__/RequireAuth.test.tsx +296 -0
- package/src/components/__tests__/TenantIndexGuard.test.tsx +218 -0
- package/src/components/__tests__/TenantRootRedirect.test.tsx +180 -0
- package/src/config/__tests__/portal-resolver.test.ts +302 -0
- package/src/config/__tests__/valid-redirect.test.ts +47 -0
- package/src/config/constants.ts +27 -0
- package/src/config/index.ts +426 -0
- package/src/config/resolve-basename.ts +57 -0
- package/src/constants/api-paths.ts +450 -0
- package/src/generated/api.ts +13012 -0
- package/src/generated/queries.ts +12984 -0
- package/src/generated/types.ts +13982 -0
- package/src/hooks/__tests__/useBootstrap.test.tsx +91 -0
- package/src/hooks/__tests__/useCurrentRole.test.tsx +122 -0
- package/src/hooks/__tests__/useCurrentTenantIdOr.test.tsx +38 -0
- package/src/hooks/__tests__/useLogout.test.tsx +61 -0
- package/src/hooks/__tests__/usePermission.map.test.tsx +156 -0
- package/src/hooks/__tests__/usePortalCatalog.test.tsx +301 -0
- package/src/hooks/__tests__/useSodMode.test.tsx +205 -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/useCurrentTenantIdOr.ts +29 -0
- package/src/hooks/useLogout.ts +42 -0
- package/src/hooks/usePermission.ts +128 -0
- package/src/hooks/usePortalCatalog.ts +199 -0
- package/src/hooks/useSodMode.ts +81 -0
- package/src/index.ts +247 -0
- package/src/lib/__tests__/push.test.ts +30 -0
- package/src/lib/push.ts +44 -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 +190 -0
- package/src/utils/__tests__/case.test.ts +94 -0
- package/src/utils/__tests__/error.test.ts +72 -0
- package/src/utils/__tests__/mock-test.ts +15 -0
- package/src/utils/__tests__/page.test.ts +54 -0
- package/src/utils/__tests__/query-state.test.ts +120 -0
- package/src/utils/browser.ts +15 -0
- package/src/utils/case.ts +59 -0
- package/src/utils/error.ts +35 -0
- package/src/utils/format.ts +63 -0
- package/src/utils/page.ts +57 -0
- package/src/utils/password-transmission.test.ts +145 -0
- package/src/utils/password-transmission.ts +253 -0
- package/src/utils/query-state.ts +85 -0
- package/src/utils/response.ts +71 -0
- package/src/utils/vite.ts +4 -0
- package/src/vite-env.d.ts +19 -0
- package/README.md +0 -3
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthMachine — 认证流程状态机(轻量版)
|
|
3
|
+
*
|
|
4
|
+
* 统一管理认证流程的各个阶段:
|
|
5
|
+
* checking → authenticated → bootstrap → ready
|
|
6
|
+
* → unauthenticated → redirecting
|
|
7
|
+
*
|
|
8
|
+
* 替代 RequireAuth 中的手动 useEffect 链式调用。
|
|
9
|
+
* 不依赖外部状态机库,使用 React hook + reducer 模式。
|
|
10
|
+
*
|
|
11
|
+
* @see docs/architecture/auth-next-issues-analysis.md
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use client';
|
|
15
|
+
|
|
16
|
+
import { useReducer, useEffect, useMemo } from 'react';
|
|
17
|
+
import { AuthService } from './service';
|
|
18
|
+
import { useBootstrap } from '../hooks/useBootstrap';
|
|
19
|
+
|
|
20
|
+
// ============ Types ============
|
|
21
|
+
|
|
22
|
+
export type AuthStatus =
|
|
23
|
+
| 'checking' // 初始态:正在检查 token
|
|
24
|
+
| 'authenticated' // token 有效
|
|
25
|
+
| 'bootstrap' // token 有效,正在加载权限/租户
|
|
26
|
+
| 'ready' // 全部就绪
|
|
27
|
+
| 'unauthenticated' // 无有效 token
|
|
28
|
+
| 'redirecting'; // 正在跳转到登录页
|
|
29
|
+
|
|
30
|
+
export interface AuthMachineState {
|
|
31
|
+
status: AuthStatus;
|
|
32
|
+
hasToken: boolean;
|
|
33
|
+
bootstrapState: ReturnType<typeof useBootstrap>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type AuthAction =
|
|
37
|
+
| { type: 'TOKEN_FOUND' }
|
|
38
|
+
| { type: 'NO_TOKEN' }
|
|
39
|
+
| { type: 'BOOTSTRAP_LOADING' }
|
|
40
|
+
| { type: 'BOOTSTRAP_READY' }
|
|
41
|
+
| { type: 'BOOTSTRAP_ERROR' }
|
|
42
|
+
| { type: 'SESSION_EXPIRED' }
|
|
43
|
+
| { type: 'REDIRECT' };
|
|
44
|
+
|
|
45
|
+
// ============ Reducer ============
|
|
46
|
+
|
|
47
|
+
function authReducer(state: AuthMachineState, action: AuthAction): AuthMachineState {
|
|
48
|
+
switch (action.type) {
|
|
49
|
+
case 'TOKEN_FOUND':
|
|
50
|
+
return { ...state, status: 'authenticated', hasToken: true };
|
|
51
|
+
|
|
52
|
+
case 'NO_TOKEN':
|
|
53
|
+
return { ...state, status: 'unauthenticated', hasToken: false };
|
|
54
|
+
|
|
55
|
+
case 'BOOTSTRAP_LOADING':
|
|
56
|
+
return state.status === 'authenticated' ? { ...state, status: 'bootstrap' } : state;
|
|
57
|
+
|
|
58
|
+
case 'BOOTSTRAP_READY':
|
|
59
|
+
return state.status === 'bootstrap'
|
|
60
|
+
? { ...state, status: 'ready' }
|
|
61
|
+
: state.status === 'authenticated'
|
|
62
|
+
? { ...state, status: 'ready' }
|
|
63
|
+
: state;
|
|
64
|
+
|
|
65
|
+
case 'BOOTSTRAP_ERROR':
|
|
66
|
+
// Bootstrap 失败仍应显示 UI(降级运行)
|
|
67
|
+
return state.status === 'bootstrap' ? { ...state, status: 'ready' } : state;
|
|
68
|
+
|
|
69
|
+
case 'SESSION_EXPIRED':
|
|
70
|
+
return { ...state, status: 'unauthenticated', hasToken: false };
|
|
71
|
+
|
|
72
|
+
case 'REDIRECT':
|
|
73
|
+
return { ...state, status: 'redirecting' };
|
|
74
|
+
|
|
75
|
+
default:
|
|
76
|
+
return state;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ============ Initial State ============
|
|
81
|
+
|
|
82
|
+
function createInitialState(): AuthMachineState {
|
|
83
|
+
const token = AuthService.getAccessToken();
|
|
84
|
+
return {
|
|
85
|
+
status: 'checking',
|
|
86
|
+
hasToken: !!token,
|
|
87
|
+
bootstrapState: 'idle' as const,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ============ Hook ============
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 认证流程状态机 Hook。
|
|
95
|
+
*
|
|
96
|
+
* 整合 token 检查 + bootstrap 加载为统一状态机,
|
|
97
|
+
* 替代 RequireAuth + useBootstrap 的手动链式调用。
|
|
98
|
+
*
|
|
99
|
+
* 用法:
|
|
100
|
+
* ```tsx
|
|
101
|
+
* function App() {
|
|
102
|
+
* const machine = useAuthMachine();
|
|
103
|
+
* const { status, hasToken } = machine;
|
|
104
|
+
*
|
|
105
|
+
* if (status === 'checking') return <Loading />;
|
|
106
|
+
* if (status === 'unauthenticated' || status === 'redirecting') return <LoginPage />;
|
|
107
|
+
* if (status === 'ready') return <Dashboard />;
|
|
108
|
+
* return <Loading />; // authenticated / bootstrap
|
|
109
|
+
* }
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export function useAuthMachine(): AuthMachineState {
|
|
113
|
+
const [state, dispatch] = useReducer(authReducer, undefined, createInitialState);
|
|
114
|
+
const bootstrapState = useBootstrap();
|
|
115
|
+
|
|
116
|
+
// Phase 1: 检查 token
|
|
117
|
+
const hasToken = useMemo(() => {
|
|
118
|
+
const token = AuthService.getAccessToken();
|
|
119
|
+
return !!token && token !== 'undefined' && token !== 'null';
|
|
120
|
+
}, []);
|
|
121
|
+
|
|
122
|
+
// Phase 2: 根据 token 和 bootstrap 状态分发事件
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (hasToken) {
|
|
125
|
+
dispatch({ type: 'TOKEN_FOUND' });
|
|
126
|
+
} else {
|
|
127
|
+
dispatch({ type: 'NO_TOKEN' });
|
|
128
|
+
}
|
|
129
|
+
}, [hasToken]);
|
|
130
|
+
|
|
131
|
+
// Phase 3: bootstrap 状态同步
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
switch (bootstrapState) {
|
|
134
|
+
case 'loading':
|
|
135
|
+
dispatch({ type: 'BOOTSTRAP_LOADING' });
|
|
136
|
+
break;
|
|
137
|
+
case 'ready':
|
|
138
|
+
dispatch({ type: 'BOOTSTRAP_READY' });
|
|
139
|
+
break;
|
|
140
|
+
case 'error':
|
|
141
|
+
dispatch({ type: 'BOOTSTRAP_ERROR' });
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}, [bootstrapState]);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
...state,
|
|
148
|
+
hasToken,
|
|
149
|
+
bootstrapState,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auth-queries — React Query 认证数据查询
|
|
3
|
+
*
|
|
4
|
+
* 为 permissions / tenants / me 等服务端数据提供缓存语义。
|
|
5
|
+
* 替代 useBootstrap 手动管理请求/loading/error 状态的方式。
|
|
6
|
+
*
|
|
7
|
+
* 用法:
|
|
8
|
+
* const { data: permissions, isLoading } = usePermissionsQuery();
|
|
9
|
+
* const { data: tenants, isError } = useTenantsQuery();
|
|
10
|
+
*
|
|
11
|
+
* @see docs/architecture/auth-next-issues-analysis.md
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use client';
|
|
15
|
+
|
|
16
|
+
import { useQuery } from '@tanstack/react-query';
|
|
17
|
+
import { apiClient } from '../api/client';
|
|
18
|
+
import { extractItem } from '../utils/response';
|
|
19
|
+
|
|
20
|
+
// ============ Constants ============
|
|
21
|
+
|
|
22
|
+
/** 权限/租户缓存时间:5 分钟内不重复请求 */
|
|
23
|
+
export const AUTH_DATA_STALE_TIME = 5 * 60 * 1000;
|
|
24
|
+
|
|
25
|
+
// ============ Query Keys ============
|
|
26
|
+
|
|
27
|
+
export const authQueryKeys = {
|
|
28
|
+
all: ['auth'] as const,
|
|
29
|
+
permissions: (tenantId?: string | null) =>
|
|
30
|
+
tenantId
|
|
31
|
+
? ([...authQueryKeys.all, 'permissions', tenantId] as const)
|
|
32
|
+
: ([...authQueryKeys.all, 'permissions'] as const),
|
|
33
|
+
tenants: (tenantId?: string | null) =>
|
|
34
|
+
tenantId
|
|
35
|
+
? ([...authQueryKeys.all, 'tenants', tenantId] as const)
|
|
36
|
+
: ([...authQueryKeys.all, 'tenants'] as const),
|
|
37
|
+
me: () => [...authQueryKeys.all, 'me'] as const,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ============ Queries ============
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 当前用户权限列表查询。
|
|
44
|
+
* 结果自动写入 AuthService(通过 onSuccess 回调),
|
|
45
|
+
* 供 usePermission hook 和权限检查使用。
|
|
46
|
+
*
|
|
47
|
+
* @param tenantId — 可选,切换租户时自动重新请求
|
|
48
|
+
*/
|
|
49
|
+
export function usePermissionsQuery(tenantId?: string | null, enabled = true) {
|
|
50
|
+
return useQuery({
|
|
51
|
+
queryKey: authQueryKeys.permissions(tenantId),
|
|
52
|
+
queryFn: async () => {
|
|
53
|
+
const headers: Record<string, string> = {};
|
|
54
|
+
if (tenantId) headers['X-Tenant-ID'] = tenantId;
|
|
55
|
+
const res = await apiClient.get('/identity/api/v1/auth/me/permissions', { headers });
|
|
56
|
+
const d = extractItem(res.data);
|
|
57
|
+
return ((Array.isArray(d) ? d : (d as { permissions?: string[] } | null)?.permissions) ||
|
|
58
|
+
[]) as string[];
|
|
59
|
+
},
|
|
60
|
+
staleTime: AUTH_DATA_STALE_TIME,
|
|
61
|
+
retry: 1,
|
|
62
|
+
retryDelay: 1000,
|
|
63
|
+
// 无 token 时禁止发起请求:未登录时该查询必然 401 → apiClient 401 拦截
|
|
64
|
+
// → refreshToken() 无 refresh token → onUnauthorized() 抢跑 buildLoginUrl,
|
|
65
|
+
// 与 RequireAuth 的 OAuth PKCE 触发形成时序竞争(跨域 admin 场景偶发 302 回登录页)。
|
|
66
|
+
// 未登录的跳转决策统一交给 RequireAuth(OAuth PKCE 或 buildLoginUrl)。
|
|
67
|
+
enabled,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 当前用户租户列表查询。
|
|
73
|
+
* 结果自动写入 AuthService。
|
|
74
|
+
*
|
|
75
|
+
* @param tenantId — 可选,切换租户时自动重新请求
|
|
76
|
+
*/
|
|
77
|
+
export function useTenantsQuery(tenantId?: string | null, enabled = true) {
|
|
78
|
+
return useQuery({
|
|
79
|
+
queryKey: authQueryKeys.tenants(tenantId),
|
|
80
|
+
queryFn: async () => {
|
|
81
|
+
const headers: Record<string, string> = {};
|
|
82
|
+
if (tenantId) headers['X-Tenant-ID'] = tenantId;
|
|
83
|
+
const res = await apiClient.get('/identity/api/v1/auth/me/tenants', { headers });
|
|
84
|
+
const d = extractItem(res.data);
|
|
85
|
+
return ((Array.isArray(d)
|
|
86
|
+
? d
|
|
87
|
+
: (d as { tenants?: Array<{ id: string; name: string; role: string }> } | null)?.tenants) ||
|
|
88
|
+
[]) as Array<{
|
|
89
|
+
id: string;
|
|
90
|
+
name: string;
|
|
91
|
+
role: string;
|
|
92
|
+
}>;
|
|
93
|
+
},
|
|
94
|
+
staleTime: AUTH_DATA_STALE_TIME,
|
|
95
|
+
retry: 1,
|
|
96
|
+
retryDelay: 1000,
|
|
97
|
+
// 与 usePermissionsQuery 一致:无 token 时禁止发起请求,
|
|
98
|
+
// 避免 401 → onUnauthorized 抢跑 RequireAuth 的 OAuth PKCE 流程。
|
|
99
|
+
enabled,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 当前用户信息查询。
|
|
105
|
+
* 用于 OAuth 回调后获取完整用户信息。
|
|
106
|
+
*/
|
|
107
|
+
export function useMeQuery() {
|
|
108
|
+
return useQuery({
|
|
109
|
+
queryKey: authQueryKeys.me(),
|
|
110
|
+
queryFn: async () => {
|
|
111
|
+
const res = await apiClient.get('/identity/api/v1/auth/me');
|
|
112
|
+
return extractItem(res.data) ?? res.data;
|
|
113
|
+
},
|
|
114
|
+
staleTime: AUTH_DATA_STALE_TIME,
|
|
115
|
+
retry: 1,
|
|
116
|
+
retryDelay: 1000,
|
|
117
|
+
});
|
|
118
|
+
}
|