@autional-cn/shared 0.1.0-rc.3 → 0.1.0-rc.5
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 +7 -2
- package/src/auth/__tests__/auth-trace.test.ts +237 -0
- package/src/auth/__tests__/build-login-url.test.ts +41 -1
- package/src/auth/__tests__/session-terminal-redirect.test.ts +138 -0
- package/src/auth/auth-trace.ts +473 -0
- package/src/auth/oauth-login.ts +15 -3
- package/src/auth/roles.ts +16 -0
- package/src/auth/service.ts +47 -20
- package/src/components/RequireAuth.tsx +6 -1
- package/src/components/TenantRootRedirect.tsx +3 -2
- package/src/components/__tests__/RequireAuth.test.tsx +42 -5
- package/src/components/__tests__/TenantRootRedirect.test.tsx +13 -5
- package/src/generated/api.ts +28 -4
- package/src/generated/queries.ts +20 -1
- package/src/generated/types.ts +93 -50
- package/src/hooks/useLogout.ts +3 -1
- package/src/index.ts +2 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useMemo } from 'react';
|
|
2
2
|
import { useCurrentTenantId } from '../hooks/useAuth';
|
|
3
3
|
import { getPortalUrl } from '../config';
|
|
4
|
+
import { traceRedirect } from '../auth/auth-trace';
|
|
4
5
|
import { usePublicTenantSlugs } from './TenantIndexGuard';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -29,7 +30,7 @@ export function TenantRootRedirect() {
|
|
|
29
30
|
if (typeof window === 'undefined') return;
|
|
30
31
|
if (slug) {
|
|
31
32
|
const base = window.location.pathname.replace(/\/+$/, '');
|
|
32
|
-
|
|
33
|
+
traceRedirect(`${base}/${slug}/`, { reason: 'funnel-slug' });
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
36
|
// 有会话但公开名单尚未返回:等待(名单是 slug 唯一权威,不得抢跑漏斗 brand)。
|
|
@@ -38,7 +39,7 @@ export function TenantRootRedirect() {
|
|
|
38
39
|
const target = `${getPortalUrl('brand')}/?redirect=${encodeURIComponent(
|
|
39
40
|
window.location.origin + window.location.pathname,
|
|
40
41
|
)}`;
|
|
41
|
-
|
|
42
|
+
traceRedirect(target, { reason: 'funnel-brand' });
|
|
42
43
|
}, [slug, currentTenantId, tenantsLoaded]);
|
|
43
44
|
|
|
44
45
|
return null;
|
|
@@ -55,8 +55,33 @@ import { getPortalUrl } from '../../config';
|
|
|
55
55
|
|
|
56
56
|
const originalWindowLocation = window.location;
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* 每例取全新模块实例:auth-trace 的模块级重入抑制(pending)与「停留」复位
|
|
60
|
+
* 是同页语义,跨用例泄漏会让后续 interstitial 被静默抑制(假红/假绿)。
|
|
61
|
+
*/
|
|
62
|
+
async function freshRequireAuth() {
|
|
63
|
+
vi.resetModules();
|
|
64
|
+
const mod = await import('../RequireAuth');
|
|
65
|
+
return mod.RequireAuth;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** authTrace 的提示条/确认页挂在 document.body(非 React 树),手动清理防跨用例残留。 */
|
|
69
|
+
function cleanupAuthTraceNodes() {
|
|
70
|
+
document.getElementById('auth-trace-notice')?.remove();
|
|
71
|
+
document.getElementById('auth-trace-confirm')?.remove();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function clickGoNow(): void {
|
|
75
|
+
const btn = Array.from(document.querySelectorAll<HTMLButtonElement>('#auth-trace-notice button')).find(
|
|
76
|
+
(b) => b.textContent === '立即前往',
|
|
77
|
+
);
|
|
78
|
+
expect(btn).toBeTruthy();
|
|
79
|
+
btn!.click();
|
|
80
|
+
}
|
|
81
|
+
|
|
58
82
|
beforeEach(() => {
|
|
59
83
|
vi.clearAllMocks();
|
|
84
|
+
document.documentElement.lang = 'zh-CN';
|
|
60
85
|
mockState.machineStatus = 'unauthenticated';
|
|
61
86
|
mockState.tenantRoute = {
|
|
62
87
|
slug: null,
|
|
@@ -85,22 +110,30 @@ afterEach(() => {
|
|
|
85
110
|
}
|
|
86
111
|
// vitest globals 未开启 ⇒ RTL 不会自动 cleanup,手动清理防跨用例 DOM 污染
|
|
87
112
|
cleanup();
|
|
113
|
+
cleanupAuthTraceNodes();
|
|
88
114
|
});
|
|
89
115
|
|
|
90
116
|
describe('RequireAuth 未登录出口', () => {
|
|
91
|
-
it('无可用 client_id →
|
|
117
|
+
it('无可用 client_id → 提示条(可停留)后跳 auth 登录页并带 from_requireauth=1', async () => {
|
|
118
|
+
const RequireAuthFresh = await freshRequireAuth();
|
|
119
|
+
|
|
92
120
|
render(
|
|
93
|
-
<
|
|
121
|
+
<RequireAuthFresh>
|
|
94
122
|
<div>protected</div>
|
|
95
|
-
</
|
|
123
|
+
</RequireAuthFresh>,
|
|
96
124
|
);
|
|
97
125
|
|
|
126
|
+
// 层①:被动弹登录先出提示条(不再静默整页弹走)
|
|
127
|
+
await waitFor(() => expect(document.getElementById('auth-trace-notice')).toBeTruthy());
|
|
128
|
+
clickGoNow();
|
|
129
|
+
|
|
98
130
|
await waitFor(() => {
|
|
99
131
|
expect(mockState.replace).toHaveBeenCalled();
|
|
100
132
|
});
|
|
101
133
|
const url = String(mockState.replace.mock.calls[0][0]);
|
|
102
134
|
expect(url).toContain('from_requireauth=1');
|
|
103
135
|
expect(url).toContain('redirect=');
|
|
136
|
+
expect(url).toContain('rt=localhost.unauthenticated.');
|
|
104
137
|
expect(url.startsWith(getPortalUrl('auth'))).toBe(true);
|
|
105
138
|
expect(mockState.initiate).not.toHaveBeenCalled();
|
|
106
139
|
});
|
|
@@ -230,12 +263,16 @@ describe('RequireAuth F-W6 确定性 404 闸门', () => {
|
|
|
230
263
|
|
|
231
264
|
it('非 unknownSlug(网络错误 fail-open)→ 维持原漏斗(回归锁)', async () => {
|
|
232
265
|
// 默认 tenantRoute: notFound=true, unknownSlug=false —— 即 by-slug 网络错误场景
|
|
266
|
+
const RequireAuthFresh = await freshRequireAuth();
|
|
267
|
+
|
|
233
268
|
render(
|
|
234
|
-
<
|
|
269
|
+
<RequireAuthFresh notFound={<div data-testid="tenant-404">no such tenant</div>}>
|
|
235
270
|
<div>protected</div>
|
|
236
|
-
</
|
|
271
|
+
</RequireAuthFresh>,
|
|
237
272
|
);
|
|
238
273
|
|
|
274
|
+
await waitFor(() => expect(document.getElementById('auth-trace-notice')).toBeTruthy());
|
|
275
|
+
clickGoNow();
|
|
239
276
|
await waitFor(() => expect(mockState.replace).toHaveBeenCalled());
|
|
240
277
|
const url = String(mockState.replace.mock.calls[0][0]);
|
|
241
278
|
expect(url).toContain('from_requireauth=1');
|
|
@@ -58,6 +58,13 @@ function expectedBrandFunnel() {
|
|
|
58
58
|
)}`;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** 剥除 authTrace 面包屑(rt=)后比较;rt 装饰本身由专门断言锁定。 */
|
|
62
|
+
function withoutRt(url: string): string {
|
|
63
|
+
const u = new URL(url, 'http://trace.invalid');
|
|
64
|
+
u.searchParams.delete('rt');
|
|
65
|
+
return u.pathname + u.search;
|
|
66
|
+
}
|
|
67
|
+
|
|
61
68
|
const originalWindowLocation = window.location;
|
|
62
69
|
|
|
63
70
|
beforeEach(() => {
|
|
@@ -102,7 +109,8 @@ describe('TenantRootRedirect 裸根漏斗', () => {
|
|
|
102
109
|
|
|
103
110
|
await waitFor(() => expect(mockState.replace).toHaveBeenCalled());
|
|
104
111
|
const url = String(mockState.replace.mock.calls[0][0]);
|
|
105
|
-
expect(url).
|
|
112
|
+
expect(url).toContain('rt=localhost.funnel-slug.');
|
|
113
|
+
expect(withoutRt(url)).toBe('/demo/');
|
|
106
114
|
expect(url).not.toContain('Demo%20Tenant');
|
|
107
115
|
expect(url).not.toContain('Demo');
|
|
108
116
|
});
|
|
@@ -114,7 +122,7 @@ describe('TenantRootRedirect 裸根漏斗', () => {
|
|
|
114
122
|
render(wrapper(<TenantRootRedirect />));
|
|
115
123
|
|
|
116
124
|
await waitFor(() => expect(mockState.replace).toHaveBeenCalled());
|
|
117
|
-
expect(String(mockState.replace.mock.calls[0][0])).toBe('/demo-fallback/');
|
|
125
|
+
expect(withoutRt(String(mockState.replace.mock.calls[0][0]))).toBe('/demo-fallback/');
|
|
118
126
|
});
|
|
119
127
|
|
|
120
128
|
it('有会话但名单未返回(加载中)→ 等待,不抢跑 brand 漏斗', async () => {
|
|
@@ -133,7 +141,7 @@ describe('TenantRootRedirect 裸根漏斗', () => {
|
|
|
133
141
|
render(wrapper(<TenantRootRedirect />));
|
|
134
142
|
|
|
135
143
|
await waitFor(() => expect(mockState.replace).toHaveBeenCalled());
|
|
136
|
-
expect(String(mockState.replace.mock.calls[0][0])).toBe(expectedBrandFunnel());
|
|
144
|
+
expect(withoutRt(String(mockState.replace.mock.calls[0][0]))).toBe(withoutRt(expectedBrandFunnel()));
|
|
137
145
|
});
|
|
138
146
|
|
|
139
147
|
it('有会话但名单无匹配(陈旧会话)→ 漏斗 brand', async () => {
|
|
@@ -143,7 +151,7 @@ describe('TenantRootRedirect 裸根漏斗', () => {
|
|
|
143
151
|
render(wrapper(<TenantRootRedirect />));
|
|
144
152
|
|
|
145
153
|
await waitFor(() => expect(mockState.replace).toHaveBeenCalled());
|
|
146
|
-
expect(String(mockState.replace.mock.calls[0][0])).toBe(expectedBrandFunnel());
|
|
154
|
+
expect(withoutRt(String(mockState.replace.mock.calls[0][0]))).toBe(withoutRt(expectedBrandFunnel()));
|
|
147
155
|
});
|
|
148
156
|
|
|
149
157
|
it('名单接口失败(返回空数组)→ 有会话按无匹配处理,漏斗 brand(不卡死)', async () => {
|
|
@@ -153,6 +161,6 @@ describe('TenantRootRedirect 裸根漏斗', () => {
|
|
|
153
161
|
render(wrapper(<TenantRootRedirect />));
|
|
154
162
|
|
|
155
163
|
await waitFor(() => expect(mockState.replace).toHaveBeenCalled());
|
|
156
|
-
expect(String(mockState.replace.mock.calls[0][0])).toBe(expectedBrandFunnel());
|
|
164
|
+
expect(withoutRt(String(mockState.replace.mock.calls[0][0]))).toBe(withoutRt(expectedBrandFunnel()));
|
|
157
165
|
});
|
|
158
166
|
});
|
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-09-30 17:04:31
|
|
4
4
|
|
|
5
5
|
// @ts-nocheck — auto-generated; validated by check-generated-api.py
|
|
6
6
|
import type * as Types from './types';
|
|
@@ -6855,6 +6855,20 @@ export async function mfaWebauthnCredentialsByCredentialsPut(webauthnCredentialI
|
|
|
6855
6855
|
// Notification Service
|
|
6856
6856
|
// ============================================================
|
|
6857
6857
|
|
|
6858
|
+
/**
|
|
6859
|
+
* 管理端列出公告
|
|
6860
|
+
* 管理端分页查询本租户公告列表,支持按状态过滤和关键词搜索;与用户面 /announcements 同体(同租户范围),走 admin 受众供控制台读取 参考:CAN-SPAM Act (15 U.S.C. §7701) — Commercial Email Compliance。
|
|
6861
|
+
*/
|
|
6862
|
+
export async function adminAnnouncements(params?: {
|
|
6863
|
+
page?: number; // 页码
|
|
6864
|
+
page_size?: number; // 每页条数
|
|
6865
|
+
status?: string; // 状态过滤: draft/scheduled/published/expired
|
|
6866
|
+
search?: string; // 搜索关键词
|
|
6867
|
+
}) {
|
|
6868
|
+
const res = await api.get(`/notification/api/v1/admin/announcements`, { params });
|
|
6869
|
+
return res.data;
|
|
6870
|
+
}
|
|
6871
|
+
|
|
6858
6872
|
/**
|
|
6859
6873
|
* 创建公告
|
|
6860
6874
|
* 创建新的公告,支持草稿/定时发布,可指定目标租户和角色 参考:CAN-SPAM Act (15 U.S.C. §7701) — Commercial Email Compliance。
|
|
@@ -7224,15 +7238,25 @@ export async function notificationsStats() {
|
|
|
7224
7238
|
|
|
7225
7239
|
/**
|
|
7226
7240
|
* SSE实时通知流
|
|
7227
|
-
* 通过Server-Sent Events
|
|
7241
|
+
* 通过Server-Sent Events实时推送通知。P5-4:经 POST /notifications/stream-ticket 换取一次性票据后以 ?ticket= 建流(浏览器 EventSource 主路径);非浏览器客户端可走 Authorization 头 JWT(?token= 兼容回退已删除)。订阅身份以认证上下文(票据/JWT claims)为准。 参考:CAN-SPAM Act (15 U.S.C. §7701) — Commercial Email Compliance。
|
|
7228
7242
|
*/
|
|
7229
7243
|
export async function notificationsStream(params?: {
|
|
7230
|
-
|
|
7244
|
+
ticket?: string; // 一次性短期票据(P5-4 主路径,60s 内一次性消费)
|
|
7245
|
+
user_id?: string; // 用户ID(已弃用回退:仅在认证上下文无用户时使用)
|
|
7231
7246
|
}) {
|
|
7232
7247
|
const res = await api.get(`/notification/api/v1/notifications/stream`, { params });
|
|
7233
7248
|
return res.data;
|
|
7234
7249
|
}
|
|
7235
7250
|
|
|
7251
|
+
/**
|
|
7252
|
+
* 签发 SSE 短期票据
|
|
7253
|
+
* 为 /notifications/stream 签发一次性票据(P5-4):浏览器 EventSource 不能携带 Authorization 头,故以 JWT 保护的本端点换取 60s 一次性票据,再以 ?ticket= 建流,避免 access token 进入 URL/接入日志。需 JWT 认证(正常 Authorization 头调用)。
|
|
7254
|
+
*/
|
|
7255
|
+
export async function notificationsStreamTicketPost() {
|
|
7256
|
+
const res = await api.post(`/notification/api/v1/notifications/stream-ticket`);
|
|
7257
|
+
return res.data;
|
|
7258
|
+
}
|
|
7259
|
+
|
|
7236
7260
|
/**
|
|
7237
7261
|
* 列出通知模板
|
|
7238
7262
|
* 获取所有通知模板列表,支持包含已停用模板的选项 参考:CAN-SPAM Act (15 U.S.C. §7701) — Commercial Email Compliance。
|
|
@@ -7377,7 +7401,7 @@ export async function pushSubscriptionsBySubscriptions(subscriptionId: string) {
|
|
|
7377
7401
|
|
|
7378
7402
|
/**
|
|
7379
7403
|
* 获取 VAPID 公钥
|
|
7380
|
-
* 返回 Web Push VAPID 公钥,供前端注册 Push
|
|
7404
|
+
* 返回 Web Push VAPID 公钥,供前端注册 Push 订阅时使用(来源:communication-service) 参考:CAN-SPAM Act (15 U.S.C. §7701) — Commercial Email Compliance。
|
|
7381
7405
|
*/
|
|
7382
7406
|
export async function pushVapidPublicKey() {
|
|
7383
7407
|
const res = await api.get(`/notification/api/v1/push/vapid-public-key`);
|
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-09-30 17:04:31
|
|
4
4
|
|
|
5
5
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
6
6
|
import * as api from './api';
|
|
@@ -2599,6 +2599,14 @@ export function useMfaWebauthnCredentialsByCredentials(webauthnCredentialId: str
|
|
|
2599
2599
|
|
|
2600
2600
|
// --- notification-service ---
|
|
2601
2601
|
|
|
2602
|
+
/** 管理端列出公告 */
|
|
2603
|
+
export function useAdminAnnouncements(params?: any) {
|
|
2604
|
+
return useQuery({
|
|
2605
|
+
queryKey: ['notification-service', params] as const,
|
|
2606
|
+
queryFn: () => api.adminAnnouncements(params),
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2602
2610
|
/** 管理员查看指定用户通知列表 */
|
|
2603
2611
|
export function useAdminNotifications(params?: any) {
|
|
2604
2612
|
return useQuery({
|
|
@@ -9153,6 +9161,17 @@ export function usePostNotificationsSend_from_template() {
|
|
|
9153
9161
|
});
|
|
9154
9162
|
}
|
|
9155
9163
|
|
|
9164
|
+
/** 签发 SSE 短期票据 */
|
|
9165
|
+
export function usePostNotificationsStream_ticket() {
|
|
9166
|
+
const qc = useQueryClient();
|
|
9167
|
+
return useMutation({
|
|
9168
|
+
mutationFn: () => api.notificationsStreamTicketPost(),
|
|
9169
|
+
onSuccess: () => {
|
|
9170
|
+
qc.invalidateQueries({ queryKey: ['notification-service'] });
|
|
9171
|
+
},
|
|
9172
|
+
});
|
|
9173
|
+
}
|
|
9174
|
+
|
|
9156
9175
|
/** 发送测试通知 */
|
|
9157
9176
|
export function usePostNotificationsTest() {
|
|
9158
9177
|
const qc = useQueryClient();
|
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-09-30 17:04:31
|
|
4
4
|
|
|
5
5
|
// ============================================================
|
|
6
6
|
// Shared generic types
|
|
@@ -7516,6 +7516,71 @@ export type COSEAlgorithmIdentifier = "-7" | "-8" | "-9" | "-19" | "-35" | "-36"
|
|
|
7516
7516
|
// notification-service
|
|
7517
7517
|
// ============================================================
|
|
7518
7518
|
|
|
7519
|
+
export interface DataResponsegitee_com_autional_servicenotification_internal_handler_dto_EventMappingResponse {
|
|
7520
|
+
code?: number;
|
|
7521
|
+
data?: EventMappingResponse;
|
|
7522
|
+
message?: string;
|
|
7523
|
+
timestamp?: string;
|
|
7524
|
+
}
|
|
7525
|
+
|
|
7526
|
+
export interface DataResponsegitee_com_autional_servicenotification_internal_handler_dto_ExportUserDataResponse {
|
|
7527
|
+
code?: number;
|
|
7528
|
+
data?: ExportUserDataResponse;
|
|
7529
|
+
message?: string;
|
|
7530
|
+
timestamp?: string;
|
|
7531
|
+
}
|
|
7532
|
+
|
|
7533
|
+
export interface DataResponsegitee_com_autional_servicenotification_internal_handler_dto_NotificationPreferencesResponse {
|
|
7534
|
+
code?: number;
|
|
7535
|
+
data?: NotificationPreferencesResponse;
|
|
7536
|
+
message?: string;
|
|
7537
|
+
timestamp?: string;
|
|
7538
|
+
}
|
|
7539
|
+
|
|
7540
|
+
export interface DataResponsegitee_com_autional_servicenotification_internal_handler_dto_NotificationResponse {
|
|
7541
|
+
code?: number;
|
|
7542
|
+
data?: NotificationResponse;
|
|
7543
|
+
message?: string;
|
|
7544
|
+
timestamp?: string;
|
|
7545
|
+
}
|
|
7546
|
+
|
|
7547
|
+
export interface DataResponsegitee_com_autional_servicenotification_internal_handler_dto_StreamTicketResponse {
|
|
7548
|
+
code?: number;
|
|
7549
|
+
data?: StreamTicketResponse;
|
|
7550
|
+
message?: string;
|
|
7551
|
+
timestamp?: string;
|
|
7552
|
+
}
|
|
7553
|
+
|
|
7554
|
+
export interface DataResponsegitee_com_autional_servicenotification_internal_handler_dto_TemplateResponse {
|
|
7555
|
+
code?: number;
|
|
7556
|
+
data?: TemplateResponse;
|
|
7557
|
+
message?: string;
|
|
7558
|
+
timestamp?: string;
|
|
7559
|
+
}
|
|
7560
|
+
|
|
7561
|
+
export interface DataResponsehandler_HealthErrorResponse {
|
|
7562
|
+
code?: number;
|
|
7563
|
+
data?: HealthErrorResponse;
|
|
7564
|
+
message?: string;
|
|
7565
|
+
timestamp?: string;
|
|
7566
|
+
}
|
|
7567
|
+
|
|
7568
|
+
export interface DataResponsemap_string_string {
|
|
7569
|
+
code?: number;
|
|
7570
|
+
data?: Map_string_string;
|
|
7571
|
+
message?: string;
|
|
7572
|
+
timestamp?: string;
|
|
7573
|
+
}
|
|
7574
|
+
|
|
7575
|
+
export interface ListResponsegitee_com_autional_servicenotification_internal_handler_dto_NotificationResponse {
|
|
7576
|
+
code?: number;
|
|
7577
|
+
items?: NotificationResponse[]; // 统一使用 items
|
|
7578
|
+
message?: string;
|
|
7579
|
+
pagination?: PageInfo; // 分页信息(嵌套对象)
|
|
7580
|
+
timestamp?: string;
|
|
7581
|
+
total?: number; // 总条数(平铺,便于直接读取)
|
|
7582
|
+
}
|
|
7583
|
+
|
|
7519
7584
|
export interface AnnouncementDetailResponse {
|
|
7520
7585
|
code?: number;
|
|
7521
7586
|
data?: AnnouncementResponse;
|
|
@@ -7641,48 +7706,6 @@ export interface CreateGlobalVariableRequest {
|
|
|
7641
7706
|
value: string;
|
|
7642
7707
|
}
|
|
7643
7708
|
|
|
7644
|
-
export interface DataResponsedto_ExportUserDataResponse {
|
|
7645
|
-
code?: number;
|
|
7646
|
-
data?: ExportUserDataResponse;
|
|
7647
|
-
message?: string;
|
|
7648
|
-
timestamp?: string;
|
|
7649
|
-
}
|
|
7650
|
-
|
|
7651
|
-
export interface DataResponsedto_NotificationPreferencesResponse {
|
|
7652
|
-
code?: number;
|
|
7653
|
-
data?: NotificationPreferencesResponse;
|
|
7654
|
-
message?: string;
|
|
7655
|
-
timestamp?: string;
|
|
7656
|
-
}
|
|
7657
|
-
|
|
7658
|
-
export interface DataResponsedto_NotificationResponse {
|
|
7659
|
-
code?: number;
|
|
7660
|
-
data?: NotificationResponse;
|
|
7661
|
-
message?: string;
|
|
7662
|
-
timestamp?: string;
|
|
7663
|
-
}
|
|
7664
|
-
|
|
7665
|
-
export interface DataResponsedto_TemplateResponse {
|
|
7666
|
-
code?: number;
|
|
7667
|
-
data?: TemplateResponse;
|
|
7668
|
-
message?: string;
|
|
7669
|
-
timestamp?: string;
|
|
7670
|
-
}
|
|
7671
|
-
|
|
7672
|
-
export interface DataResponsehandler_HealthErrorResponse {
|
|
7673
|
-
code?: number;
|
|
7674
|
-
data?: HealthErrorResponse;
|
|
7675
|
-
message?: string;
|
|
7676
|
-
timestamp?: string;
|
|
7677
|
-
}
|
|
7678
|
-
|
|
7679
|
-
export interface DataResponsemap_string_string {
|
|
7680
|
-
code?: number;
|
|
7681
|
-
data?: Map_string_string;
|
|
7682
|
-
message?: string;
|
|
7683
|
-
timestamp?: string;
|
|
7684
|
-
}
|
|
7685
|
-
|
|
7686
7709
|
export interface DeleteNotificationDetailResponse {
|
|
7687
7710
|
code?: number;
|
|
7688
7711
|
data?: DeleteNotificationResponse;
|
|
@@ -7776,13 +7799,20 @@ export interface InternalPushSendRequest {
|
|
|
7776
7799
|
userId?: string; // @example user-001
|
|
7777
7800
|
}
|
|
7778
7801
|
|
|
7779
|
-
export interface
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7802
|
+
export interface InternalSendExternalRequest {
|
|
7803
|
+
channel: string; // @example email
|
|
7804
|
+
locale?: string; // @example en-US
|
|
7805
|
+
recipient: string; // @example user@example.com
|
|
7806
|
+
templateCode: string; // @example status_subscription_confirm
|
|
7807
|
+
variables?: Record<string, string>;
|
|
7808
|
+
}
|
|
7809
|
+
|
|
7810
|
+
export interface InternalUpsertEventMappingRequest {
|
|
7811
|
+
channel?: string; // 空 → in_app | @example in_app
|
|
7812
|
+
eventType: string; // @example user.registered
|
|
7813
|
+
isEnabled?: boolean; // 空 → true
|
|
7814
|
+
priority?: string; // 空 → medium | @example medium
|
|
7815
|
+
templateCode: string; // @example welcome
|
|
7786
7816
|
}
|
|
7787
7817
|
|
|
7788
7818
|
export interface MarkReadDetailResponse {
|
|
@@ -8008,6 +8038,12 @@ export interface SendNotificationRequest {
|
|
|
8008
8038
|
userId: string; // 用户ID | @example usr_abc123
|
|
8009
8039
|
}
|
|
8010
8040
|
|
|
8041
|
+
/** 一次性 SSE 票据:60s 内有效、消费一次即失效;以 ?ticket= 建立 EventSource 流 */
|
|
8042
|
+
export interface StreamTicketResponse {
|
|
8043
|
+
expiresIn?: number; // 有效期(秒) | @example 60
|
|
8044
|
+
ticket?: string; // 一次性票据 | @example 1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8s9T0u1V2
|
|
8045
|
+
}
|
|
8046
|
+
|
|
8011
8047
|
export interface TestNotificationDetailResponse {
|
|
8012
8048
|
code?: number;
|
|
8013
8049
|
data?: TestNotificationResponse;
|
|
@@ -8873,6 +8909,13 @@ export interface DataResponsedomain_PointIntegrityResult {
|
|
|
8873
8909
|
timestamp?: string;
|
|
8874
8910
|
}
|
|
8875
8911
|
|
|
8912
|
+
export interface DataResponsedto_ExportUserDataResponse {
|
|
8913
|
+
code?: number;
|
|
8914
|
+
data?: ExportUserDataResponse;
|
|
8915
|
+
message?: string;
|
|
8916
|
+
timestamp?: string;
|
|
8917
|
+
}
|
|
8918
|
+
|
|
8876
8919
|
export interface DeleteUserDataRequest {
|
|
8877
8920
|
userId: string;
|
|
8878
8921
|
}
|
package/src/hooks/useLogout.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { useCallback } from 'react';
|
|
8
8
|
import { buildLogoutUrl } from '../auth/roles';
|
|
9
|
+
import { traceRedirect } from '../auth/auth-trace';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* 统一的登出 Hook
|
|
@@ -26,7 +27,8 @@ export function useLogout() {
|
|
|
26
27
|
|
|
27
28
|
// buildLogoutUrl = auth 域裸根 + `logout=1` 登出意图标记:入口路由据此先
|
|
28
29
|
// 终结会话再落 brand,而不是把带会话的回程当普通深链直送登录页(会静默重登)。
|
|
29
|
-
|
|
30
|
+
// funnel:用户主动行为,静默直跳(assign 保持原 href 历史语义);仅记 trace。
|
|
31
|
+
traceRedirect(buildLogoutUrl(currentUrl), { reason: 'logout', assign: true });
|
|
30
32
|
};
|
|
31
33
|
performLogout();
|
|
32
34
|
}, []);
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,8 @@ export { generatePKCE, generateCodeChallenge } from './auth/pkce';
|
|
|
25
25
|
export type { PKCEPair } from './auth/pkce';
|
|
26
26
|
export { initiateOAuthLogin, handleOAuthCallback, isOAuthEnabled } from './auth/oauth-login';
|
|
27
27
|
export { buildLoginUrl } from './auth/roles';
|
|
28
|
+
export { traceRedirect, traceEvent, traceDump } from './auth/auth-trace';
|
|
29
|
+
export type { AuthTraceEntry, AuthTraceRedirectOptions } from './auth/auth-trace';
|
|
28
30
|
|
|
29
31
|
// Types
|
|
30
32
|
export type {
|