@autional-cn/shared 0.1.0-rc.6 → 0.1.0-rc.8
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/package.json +1 -1
- package/src/generated/api.ts +33 -1
- package/src/generated/queries.ts +25 -1
- package/src/generated/types.ts +1 -1
- package/src/hooks/__tests__/useSodMode.test.tsx +205 -0
- package/src/hooks/useSodMode.ts +31 -23
package/package.json
CHANGED
package/src/generated/api.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Auto-generated from swagger.json annotations
|
|
2
2
|
// DO NOT EDIT — run `python scripts/generate_api_ts.py` to regenerate
|
|
3
|
-
// Generated: 2026-
|
|
3
|
+
// Generated: 2026-10-01 00:28:51
|
|
4
4
|
|
|
5
5
|
// @ts-nocheck — auto-generated; validated by check-generated-api.py
|
|
6
6
|
import type * as Types from './types';
|
|
@@ -7940,6 +7940,20 @@ export async function adminPayIntegrityByIntegrity(payId: string) {
|
|
|
7940
7940
|
return res.data;
|
|
7941
7941
|
}
|
|
7942
7942
|
|
|
7943
|
+
/**
|
|
7944
|
+
* 查询支付列表(管理端)
|
|
7945
|
+
* 与用户面 /payments 同体、同租户范围;供控制台在 admin 平面读取。参考:PCI DSS v4.0 Req 3.3 (Mask PAN)、PCI DSS v4.0 Req 4 (Encrypt Transmission)。
|
|
7946
|
+
*/
|
|
7947
|
+
export async function adminPayments(params?: {
|
|
7948
|
+
app_id?: string; // 应用ID
|
|
7949
|
+
status?: string; // 支付状态
|
|
7950
|
+
page?: number; // 页码
|
|
7951
|
+
page_size?: number; // 每页数量
|
|
7952
|
+
}) {
|
|
7953
|
+
const res = await api.get(`/pay/api/v1/admin/payments`, { params });
|
|
7954
|
+
return res.data;
|
|
7955
|
+
}
|
|
7956
|
+
|
|
7943
7957
|
/**
|
|
7944
7958
|
* 查询支付渠道列表
|
|
7945
7959
|
* 查询当前租户的所有支付渠道配置列表。参考:PCI DSS v4.0 Req 3.3 (Mask PAN)、PCI DSS v4.0 Req 4 (Encrypt Transmission)。
|
|
@@ -8038,6 +8052,24 @@ export async function adminPaymentsWebhooksByWebhooksDelete(webhookId: string) {
|
|
|
8038
8052
|
await api.delete(`/pay/api/v1/admin/payments/webhooks/${webhookId}`);
|
|
8039
8053
|
}
|
|
8040
8054
|
|
|
8055
|
+
/**
|
|
8056
|
+
* 查询支付详情(管理端)
|
|
8057
|
+
* 与用户面 /payments/{payment_id} 同体、同租户范围;供控制台在 admin 平面读取。参考:PCI DSS v4.0 Req 3.3 (Mask PAN)、PCI DSS v4.0 Req 4 (Encrypt Transmission)。
|
|
8058
|
+
*/
|
|
8059
|
+
export async function adminPaymentsByPayments(paymentId: string) {
|
|
8060
|
+
const res = await api.get(`/pay/api/v1/admin/payments/${paymentId}`);
|
|
8061
|
+
return res.data;
|
|
8062
|
+
}
|
|
8063
|
+
|
|
8064
|
+
/**
|
|
8065
|
+
* 获取支付回执(管理端)
|
|
8066
|
+
* 与用户面 /payments/{payment_id}/receipt 同体、同租户范围;供控制台在 admin 平面读取。参考:PCI DSS v4.0 Req 3.3 (Mask PAN)、PCI DSS v4.0 Req 4 (Encrypt Transmission)。
|
|
8067
|
+
*/
|
|
8068
|
+
export async function adminPaymentsReceiptByPayments(paymentId: string) {
|
|
8069
|
+
const res = await api.get(`/pay/api/v1/admin/payments/${paymentId}/receipt`);
|
|
8070
|
+
return res.data;
|
|
8071
|
+
}
|
|
8072
|
+
|
|
8041
8073
|
/**
|
|
8042
8074
|
* 查询支付列表
|
|
8043
8075
|
* 查询支付订单列表,支持按应用ID和状态筛选。参考:PCI DSS v4.0 Req 3.3 (Mask PAN)、PCI DSS v4.0 Req 4 (Encrypt Transmission)。
|
package/src/generated/queries.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Auto-generated from swagger.json annotations
|
|
2
2
|
// DO NOT EDIT — run `python scripts/generate_api_ts.py` to regenerate
|
|
3
|
-
// Generated: 2026-
|
|
3
|
+
// Generated: 2026-10-01 00:28:51
|
|
4
4
|
|
|
5
5
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
6
6
|
import * as api from './api';
|
|
@@ -3003,6 +3003,14 @@ export function useAdminPayIntegrityByIntegrity(payId: string) {
|
|
|
3003
3003
|
});
|
|
3004
3004
|
}
|
|
3005
3005
|
|
|
3006
|
+
/** 查询支付列表(管理端) */
|
|
3007
|
+
export function useAdminPayments(params?: any) {
|
|
3008
|
+
return useQuery({
|
|
3009
|
+
queryKey: ['pay-service', params] as const,
|
|
3010
|
+
queryFn: () => api.adminPayments(params),
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3006
3014
|
/** 查询支付渠道列表 */
|
|
3007
3015
|
export function useAdminPaymentsChannels() {
|
|
3008
3016
|
return useQuery({
|
|
@@ -3035,6 +3043,22 @@ export function useAdminPaymentsWebhooks(params?: any) {
|
|
|
3035
3043
|
});
|
|
3036
3044
|
}
|
|
3037
3045
|
|
|
3046
|
+
/** 查询支付详情(管理端) */
|
|
3047
|
+
export function useAdminPaymentsByPayments(paymentId: string) {
|
|
3048
|
+
return useQuery({
|
|
3049
|
+
queryKey: ['pay-service', 'by_' + paymentId] as const,
|
|
3050
|
+
queryFn: () => api.adminPaymentsByPayments(paymentId),
|
|
3051
|
+
});
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
/** 获取支付回执(管理端) */
|
|
3055
|
+
export function useAdminPaymentsReceiptByPayments(paymentId: string) {
|
|
3056
|
+
return useQuery({
|
|
3057
|
+
queryKey: ['pay-service', 'by_' + paymentId] as const,
|
|
3058
|
+
queryFn: () => api.adminPaymentsReceiptByPayments(paymentId),
|
|
3059
|
+
});
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3038
3062
|
/** 查询支付列表 */
|
|
3039
3063
|
export function usePayments(params?: any) {
|
|
3040
3064
|
return useQuery({
|
package/src/generated/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Auto-generated from swagger.json annotations
|
|
2
2
|
// DO NOT EDIT — run `python scripts/generate_api_ts.py` to regenerate
|
|
3
|
-
// Generated: 2026-
|
|
3
|
+
// Generated: 2026-10-01 00:28:51
|
|
4
4
|
|
|
5
5
|
// ============================================================
|
|
6
6
|
// Shared generic types
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
4
|
+
import { renderHook, cleanup, waitFor } from '@testing-library/react';
|
|
5
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
6
|
+
|
|
7
|
+
// ============================================================
|
|
8
|
+
// U97 回归锁 — useSodMode 四连缺陷修复:
|
|
9
|
+
// ① URL 含 /tenant 服务段(不再是 /api/v1/... 裸径 → 网关 404)
|
|
10
|
+
// ② 请求参数为租户 ULID(不再是 slug → 后端按主键 id 查找必 404)
|
|
11
|
+
// ③ 失败不再静默吞成 'single'(返回 null;错误留在 react-query 通道)
|
|
12
|
+
// ④ 读取键为 camelCase `sodMode`(响应拦截器 camelCaseKeys 契约;
|
|
13
|
+
// 读 snake_case `sod_mode` 恒 undefined —— 请求虽 200 但值被吞)
|
|
14
|
+
// ============================================================
|
|
15
|
+
|
|
16
|
+
vi.mock('../../api/client', () => ({
|
|
17
|
+
apiClient: { get: vi.fn() },
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import { apiClient } from '../../api/client';
|
|
21
|
+
import { useAuthStore } from '../../auth/store';
|
|
22
|
+
import { useSodMode, useIsAuditRestricted } from '../useSodMode';
|
|
23
|
+
|
|
24
|
+
const mockedGet = vi.mocked(apiClient.get);
|
|
25
|
+
|
|
26
|
+
/** 租户 ULID(后端 GetSodConfig 按主键查找的键) */
|
|
27
|
+
const TENANT_ULID = '01M24BB8G083JHWKTXXVGWMWQA';
|
|
28
|
+
/** slug 字样:URL 中不得出现(缺陷② 的负例锚点) */
|
|
29
|
+
const SLUG = 'demo';
|
|
30
|
+
const TENANTS = [{ id: TENANT_ULID, name: SLUG, role: 'admin' }];
|
|
31
|
+
|
|
32
|
+
/** 模拟 axios 响应拦截器(api/client.ts camelCaseKeys)之后的形状 */
|
|
33
|
+
function sodResponse(sodMode: 'single' | 'strict') {
|
|
34
|
+
return {
|
|
35
|
+
data: {
|
|
36
|
+
tenantId: TENANT_ULID,
|
|
37
|
+
sodMode,
|
|
38
|
+
updatedAt: '2026-09-30T00:00:00Z',
|
|
39
|
+
},
|
|
40
|
+
} as never;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createWrapper() {
|
|
44
|
+
const queryClient = new QueryClient({
|
|
45
|
+
defaultOptions: { queries: { retry: false, retryDelay: 10 } },
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
queryClient,
|
|
49
|
+
wrapper: function Wrapper({ children }: { children: React.ReactNode }) {
|
|
50
|
+
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 重置 store + localStorage,避免跨用例污染 */
|
|
56
|
+
function resetAuthStore(): void {
|
|
57
|
+
useAuthStore.setState({
|
|
58
|
+
user: null,
|
|
59
|
+
accessToken: null,
|
|
60
|
+
refreshToken: null,
|
|
61
|
+
tenants: [],
|
|
62
|
+
currentTenantId: null,
|
|
63
|
+
permissions: [],
|
|
64
|
+
isAuthenticated: false,
|
|
65
|
+
});
|
|
66
|
+
window.localStorage.clear();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('useSodMode(U97 回归锁:路径服务段 / ULID 参数 / 失败不吞 / camelCase 键)', () => {
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
vi.clearAllMocks();
|
|
72
|
+
resetAuthStore();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
cleanup();
|
|
77
|
+
resetAuthStore();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('①+②+④ 成功: URL = /tenant/api/v1/admin/tenants/<ULID>/sod-config 且读到 sodMode=strict', async () => {
|
|
81
|
+
useAuthStore.setState({ tenants: TENANTS, currentTenantId: TENANT_ULID, accessToken: 't' });
|
|
82
|
+
mockedGet.mockResolvedValue(sodResponse('strict'));
|
|
83
|
+
|
|
84
|
+
const { wrapper } = createWrapper();
|
|
85
|
+
const { result } = renderHook(() => useSodMode(), { wrapper });
|
|
86
|
+
|
|
87
|
+
await waitFor(() => expect(result.current).toBe('strict'));
|
|
88
|
+
|
|
89
|
+
// ① 服务段:完整路径精确断言(旧实现 /api/v1/... 裸径将 FAIL)
|
|
90
|
+
expect(mockedGet).toHaveBeenCalledWith(
|
|
91
|
+
`/tenant/api/v1/admin/tenants/${TENANT_ULID}/sod-config`,
|
|
92
|
+
);
|
|
93
|
+
const calledUrl = mockedGet.mock.calls[0][0] as string;
|
|
94
|
+
expect(calledUrl).not.toMatch(/^\/api\/v1\//);
|
|
95
|
+
// ② 参数是 ULID,不是 slug
|
|
96
|
+
expect(calledUrl).toContain(TENANT_ULID);
|
|
97
|
+
expect(calledUrl).not.toContain(`/${SLUG}/`);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('④ 服务端返回 single → single(正常值不丢)', async () => {
|
|
101
|
+
useAuthStore.setState({ tenants: TENANTS, currentTenantId: TENANT_ULID, accessToken: 't' });
|
|
102
|
+
mockedGet.mockResolvedValue(sodResponse('single'));
|
|
103
|
+
|
|
104
|
+
const { wrapper } = createWrapper();
|
|
105
|
+
const { result } = renderHook(() => useSodMode(), { wrapper });
|
|
106
|
+
|
|
107
|
+
await waitFor(() => expect(result.current).toBe('single'));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('② 无租户(currentTenantId=null)→ 不发请求,返回 null', () => {
|
|
111
|
+
const { wrapper } = createWrapper();
|
|
112
|
+
const { result } = renderHook(() => useSodMode(), { wrapper });
|
|
113
|
+
|
|
114
|
+
expect(result.current).toBeNull();
|
|
115
|
+
expect(mockedGet).not.toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('③ 请求失败 → null(不吞成 single),且错误进入 query error 态(不再静默)', async () => {
|
|
119
|
+
useAuthStore.setState({ tenants: TENANTS, currentTenantId: TENANT_ULID, accessToken: 't' });
|
|
120
|
+
mockedGet.mockRejectedValue(new Error('Network Error'));
|
|
121
|
+
|
|
122
|
+
const { queryClient, wrapper } = createWrapper();
|
|
123
|
+
const { result } = renderHook(() => useSodMode(), { wrapper });
|
|
124
|
+
|
|
125
|
+
// retry:1(hook 显式)→ 失败重试一次后进入 error 态
|
|
126
|
+
await waitFor(
|
|
127
|
+
() => {
|
|
128
|
+
expect(queryClient.getQueryState(['sod-config', TENANT_ULID])?.status).toBe('error');
|
|
129
|
+
},
|
|
130
|
+
{ timeout: 5000 },
|
|
131
|
+
);
|
|
132
|
+
// 关键回归锁:绝不是 'single'(旧实现 catch 吞错 + `?? 'single'` 恒 single)
|
|
133
|
+
expect(result.current).toBeNull();
|
|
134
|
+
expect(result.current).not.toBe('single');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('useIsAuditRestricted(U97 消费面语义)', () => {
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
vi.clearAllMocks();
|
|
141
|
+
resetAuthStore();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
afterEach(() => {
|
|
145
|
+
cleanup();
|
|
146
|
+
resetAuthStore();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('strict ∧ role=admin → true(strict 模式 L2 限制生效)', async () => {
|
|
150
|
+
useAuthStore.setState({ tenants: TENANTS, currentTenantId: TENANT_ULID, accessToken: 't' });
|
|
151
|
+
mockedGet.mockResolvedValue(sodResponse('strict'));
|
|
152
|
+
|
|
153
|
+
const { wrapper } = createWrapper();
|
|
154
|
+
const { result } = renderHook(() => useIsAuditRestricted(), { wrapper });
|
|
155
|
+
|
|
156
|
+
await waitFor(() => expect(result.current).toBe(true));
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('strict ∧ 非 admin → false', async () => {
|
|
160
|
+
useAuthStore.setState({
|
|
161
|
+
tenants: [{ id: TENANT_ULID, name: SLUG, role: 'member' }],
|
|
162
|
+
currentTenantId: TENANT_ULID,
|
|
163
|
+
accessToken: 't',
|
|
164
|
+
});
|
|
165
|
+
mockedGet.mockResolvedValue(sodResponse('strict'));
|
|
166
|
+
|
|
167
|
+
const { queryClient, wrapper } = createWrapper();
|
|
168
|
+
const { result } = renderHook(() => useIsAuditRestricted(), { wrapper });
|
|
169
|
+
|
|
170
|
+
// 等 sod 数据确实到达(success)后再断言 —— 避免「数据未到即 false」的假绿
|
|
171
|
+
await waitFor(() => {
|
|
172
|
+
expect(queryClient.getQueryState(['sod-config', TENANT_ULID])?.status).toBe('success');
|
|
173
|
+
});
|
|
174
|
+
expect(result.current).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('single(admin)→ false', async () => {
|
|
178
|
+
useAuthStore.setState({ tenants: TENANTS, currentTenantId: TENANT_ULID, accessToken: 't' });
|
|
179
|
+
mockedGet.mockResolvedValue(sodResponse('single'));
|
|
180
|
+
|
|
181
|
+
const { queryClient, wrapper } = createWrapper();
|
|
182
|
+
const { result } = renderHook(() => useIsAuditRestricted(), { wrapper });
|
|
183
|
+
|
|
184
|
+
await waitFor(() => {
|
|
185
|
+
expect(queryClient.getQueryState(['sod-config', TENANT_ULID])?.status).toBe('success');
|
|
186
|
+
});
|
|
187
|
+
expect(result.current).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('读取失败(null 模式)→ false(不误判受限;fail-open 语义保持)', async () => {
|
|
191
|
+
useAuthStore.setState({ tenants: TENANTS, currentTenantId: TENANT_ULID, accessToken: 't' });
|
|
192
|
+
mockedGet.mockRejectedValue(new Error('Network Error'));
|
|
193
|
+
|
|
194
|
+
const { queryClient, wrapper } = createWrapper();
|
|
195
|
+
const { result } = renderHook(() => useIsAuditRestricted(), { wrapper });
|
|
196
|
+
|
|
197
|
+
await waitFor(
|
|
198
|
+
() => {
|
|
199
|
+
expect(queryClient.getQueryState(['sod-config', TENANT_ULID])?.status).toBe('error');
|
|
200
|
+
},
|
|
201
|
+
{ timeout: 5000 },
|
|
202
|
+
);
|
|
203
|
+
expect(result.current).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
});
|
package/src/hooks/useSodMode.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* 'single' — 小客户模式(默认): admin 可看 L1 统计 + L2 操作反馈
|
|
9
9
|
* 'strict' — 大客户/受监管: admin 仅看 L1 统计摘要
|
|
10
10
|
*
|
|
11
|
-
* API: GET /api/v1/admin/tenants/{
|
|
12
|
-
*
|
|
11
|
+
* API: GET /tenant/api/v1/admin/tenants/{tenantId}/sod-config (apiClient baseURL=/bff)
|
|
12
|
+
* PUT /tenant/api/v1/admin/tenants/{tenantId}/sod-config
|
|
13
|
+
* {tenantId} 为租户 ULID —— 后端 GetSodConfig 按租户主键(id)查找,传 slug 必 404。
|
|
13
14
|
*
|
|
14
15
|
* @see document/architecture/decisions/007-security-dashboard-audit.md §2.3
|
|
15
16
|
*/
|
|
@@ -19,49 +20,56 @@
|
|
|
19
20
|
import { useMemo } from 'react';
|
|
20
21
|
import { useQuery } from '@tanstack/react-query';
|
|
21
22
|
import { apiClient } from '../api/client';
|
|
23
|
+
import { API as API_PATHS } from '../constants/api-paths';
|
|
22
24
|
import { extractItem } from '../utils/response';
|
|
23
|
-
import {
|
|
25
|
+
import { useAuthStore } from '../auth/store';
|
|
24
26
|
import { useCurrentRole } from './useCurrentRole';
|
|
25
27
|
|
|
26
28
|
export type SodMode = 'single' | 'strict';
|
|
27
29
|
|
|
30
|
+
/**
|
|
31
|
+
* 响应字段为 camelCase —— axios 响应拦截器(api/client.ts)对后端
|
|
32
|
+
* `{tenant_id, sod_mode, updated_at}` 做过 camelCaseKeys 转换。
|
|
33
|
+
*/
|
|
28
34
|
interface SodConfigResponse {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
tenantId: string;
|
|
36
|
+
sodMode: SodMode;
|
|
37
|
+
updatedAt: string;
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
/**
|
|
35
|
-
*
|
|
36
|
-
* 从 tenant-service 的 /
|
|
37
|
-
*
|
|
41
|
+
* 返回当前租户的 sod_mode。
|
|
42
|
+
* 从 tenant-service 的 /admin/tenants/{ULID}/sod-config 读取(经 /bff 网关)。
|
|
43
|
+
*
|
|
44
|
+
* 返回 null 表示模式未知(无租户 / 加载中 / 请求失败)—— 不再折叠成 'single':
|
|
45
|
+
* 'single' 仅表示服务端确认的小客户模式;请求失败经 react-query 错误通道暴露。
|
|
46
|
+
* (U97: 此前路径缺 /tenant 服务段 + 传 slug + catch 吞错 + 读 snake_case 键,
|
|
47
|
+
* 四连缺陷叠加使 strict 租户的审计 L2 限制恒不生效。)
|
|
38
48
|
*/
|
|
39
|
-
export function useSodMode(): SodMode {
|
|
40
|
-
const
|
|
49
|
+
export function useSodMode(): SodMode | null {
|
|
50
|
+
const tenantId = useAuthStore((s) => s.currentTenantId);
|
|
41
51
|
|
|
42
52
|
const { data: config } = useQuery<SodConfigResponse | null>({
|
|
43
|
-
queryKey: ['sod-config',
|
|
44
|
-
queryFn: () => fetchSodConfig(
|
|
53
|
+
queryKey: ['sod-config', tenantId],
|
|
54
|
+
queryFn: () => fetchSodConfig(tenantId),
|
|
45
55
|
staleTime: 5 * 60 * 1000, // 5 min cache
|
|
46
56
|
retry: 1,
|
|
47
|
-
enabled: !!
|
|
57
|
+
enabled: !!tenantId,
|
|
48
58
|
});
|
|
49
59
|
|
|
50
|
-
return config?.
|
|
60
|
+
return config?.sodMode ?? null;
|
|
51
61
|
}
|
|
52
62
|
|
|
53
|
-
async function fetchSodConfig(
|
|
54
|
-
if (!
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
} catch {
|
|
59
|
-
return null; // 默认 single
|
|
60
|
-
}
|
|
63
|
+
async function fetchSodConfig(tenantId: string | null): Promise<SodConfigResponse | null> {
|
|
64
|
+
if (!tenantId) return null;
|
|
65
|
+
// 路径须循 /<服务>/api/v1/... 惯例(U97: 此前漏 /tenant 服务段 → 网关 404)
|
|
66
|
+
const res = await apiClient.get(API_PATHS.TENANT.SOD_CONFIG(tenantId));
|
|
67
|
+
return extractItem<SodConfigResponse>(res.data);
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
/**
|
|
64
71
|
* 判断当前用户在 strict 模式下是否被限制查看审计详情。
|
|
72
|
+
* 模式未知(null)时不判定为受限(维持既有 fail-open 语义)。
|
|
65
73
|
*/
|
|
66
74
|
export function useIsAuditRestricted(): boolean {
|
|
67
75
|
const sodMode = useSodMode();
|