@choiceform/shared-auth 0.1.16 → 0.1.18
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/README.md +286 -134
- package/dist/__tests__/auth-utils.test.d.ts +5 -0
- package/dist/__tests__/auth-utils.test.d.ts.map +1 -0
- package/dist/__tests__/auth-utils.test.js +96 -0
- package/dist/__tests__/store.test.d.ts +5 -0
- package/dist/__tests__/store.test.d.ts.map +1 -0
- package/dist/__tests__/store.test.js +210 -0
- package/dist/__tests__/user-mapper.test.d.ts +5 -0
- package/dist/__tests__/user-mapper.test.d.ts.map +1 -0
- package/dist/__tests__/user-mapper.test.js +76 -0
- package/dist/api/auth-api.d.ts +93 -9
- package/dist/api/auth-api.d.ts.map +1 -1
- package/dist/api/auth-api.js +219 -80
- package/dist/api/client.d.ts +10 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +10 -0
- package/dist/api/organization-api.d.ts +2 -7
- package/dist/api/organization-api.d.ts.map +1 -1
- package/dist/api/organization-api.js +2 -17
- package/dist/api/team-api.d.ts +1 -5
- package/dist/api/team-api.d.ts.map +1 -1
- package/dist/api/team-api.js +5 -11
- package/dist/components/auth-sync.d.ts +27 -0
- package/dist/components/auth-sync.d.ts.map +1 -0
- package/dist/components/auth-sync.js +117 -0
- package/dist/components/protected-route.d.ts +18 -0
- package/dist/components/protected-route.d.ts.map +1 -0
- package/dist/components/protected-route.js +34 -0
- package/dist/components/sign-in-page.d.ts +21 -0
- package/dist/components/sign-in-page.d.ts.map +1 -0
- package/dist/components/sign-in-page.js +31 -0
- package/dist/config.js +1 -1
- package/dist/core.d.ts +148 -71
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +109 -28
- package/dist/hooks/index.d.ts +8 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/use-auth-init.d.ts +4 -3
- package/dist/hooks/use-auth-init.d.ts.map +1 -1
- package/dist/hooks/use-auth-init.js +18 -30
- package/dist/hooks/use-auth-sync.d.ts +60 -0
- package/dist/hooks/use-auth-sync.d.ts.map +1 -0
- package/dist/hooks/use-auth-sync.js +116 -0
- package/dist/hooks/use-email-verification.d.ts +85 -0
- package/dist/hooks/use-email-verification.d.ts.map +1 -0
- package/dist/hooks/use-email-verification.js +145 -0
- package/dist/hooks/use-protected-route.d.ts +67 -0
- package/dist/hooks/use-protected-route.d.ts.map +1 -0
- package/dist/hooks/use-protected-route.js +102 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -13
- package/dist/init.d.ts +127 -70
- package/dist/init.d.ts.map +1 -1
- package/dist/lib/auth-client.d.ts.map +1 -1
- package/dist/lib/auth-client.js +75 -2
- package/dist/services/auth-service.d.ts +101 -0
- package/dist/services/auth-service.d.ts.map +1 -0
- package/dist/services/auth-service.js +356 -0
- package/dist/services/callback-service.d.ts +33 -0
- package/dist/services/callback-service.d.ts.map +1 -0
- package/dist/services/callback-service.js +473 -0
- package/dist/services/companion-team.d.ts.map +1 -1
- package/dist/services/companion-team.js +41 -39
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/store/actions.d.ts +54 -51
- package/dist/store/actions.d.ts.map +1 -1
- package/dist/store/actions.js +111 -243
- package/dist/store/computed.d.ts +72 -1
- package/dist/store/computed.d.ts.map +1 -1
- package/dist/store/computed.js +90 -3
- package/dist/store/index.d.ts +3 -3
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +2 -2
- package/dist/store/state.d.ts +10 -0
- package/dist/store/state.d.ts.map +1 -1
- package/dist/store/state.js +11 -1
- package/dist/store/utils.d.ts +3 -34
- package/dist/store/utils.d.ts.map +1 -1
- package/dist/store/utils.js +2 -22
- package/dist/types/auth.d.ts +106 -0
- package/dist/types/auth.d.ts.map +1 -1
- package/dist/types/callback.d.ts +35 -0
- package/dist/types/callback.d.ts.map +1 -0
- package/dist/types/callback.js +1 -0
- package/dist/types/index.d.ts +4 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/organization.d.ts +19 -3
- package/dist/types/organization.d.ts.map +1 -1
- package/dist/types/team.d.ts +6 -2
- package/dist/types/team.d.ts.map +1 -1
- package/dist/types/user.d.ts +7 -3
- package/dist/types/user.d.ts.map +1 -1
- package/dist/utils/auth-utils.d.ts +60 -0
- package/dist/utils/auth-utils.d.ts.map +1 -0
- package/dist/utils/auth-utils.js +146 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/user-mapper.d.ts.map +1 -1
- package/dist/utils/user-mapper.js +2 -1
- package/package.json +10 -2
package/dist/core.js
CHANGED
|
@@ -1,55 +1,136 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 认证系统核心
|
|
3
|
+
*
|
|
4
|
+
* 架构设计:
|
|
5
|
+
* - Store Layer: 响应式状态管理(authStore + storeActions)
|
|
6
|
+
* - Service Layer: 业务逻辑(authService)
|
|
7
|
+
* - API Layer: HTTP 请求(apiClient + authApi/organizationApi/teamApi)
|
|
8
|
+
*
|
|
9
|
+
* 使用方式:
|
|
10
|
+
* ```typescript
|
|
11
|
+
* // 初始化
|
|
12
|
+
* const auth = createAuth({ baseURL: 'https://api.example.com' })
|
|
13
|
+
*
|
|
14
|
+
* // 响应式读取状态
|
|
15
|
+
* const user = use$(auth.authStore.user)
|
|
16
|
+
*
|
|
17
|
+
* // 业务操作
|
|
18
|
+
* await auth.authService.signInWithEmail(email, password)
|
|
19
|
+
*
|
|
20
|
+
* // 状态更新
|
|
21
|
+
* auth.storeActions.setUser(user)
|
|
22
|
+
* ```
|
|
3
23
|
*/
|
|
4
24
|
import { createAuthClientFromConfig } from "./lib/auth-client";
|
|
5
25
|
import { createAuthStore } from "./store/state";
|
|
6
|
-
import {
|
|
26
|
+
import { createStoreActions } from "./store/actions";
|
|
7
27
|
import { createAuthComputed } from "./store/computed";
|
|
8
|
-
import {
|
|
28
|
+
import { waitForAuth as waitForAuthUtil, createUserManager } from "./store/utils";
|
|
9
29
|
import { createApiClient, createAuthApi, createOrganizationApi, createTeamApi } from "./api";
|
|
30
|
+
import { createAuthService } from "./services/auth-service";
|
|
10
31
|
export function createAuth(config) {
|
|
32
|
+
// ============================================================
|
|
33
|
+
// 1. Better Auth 客户端
|
|
34
|
+
// ============================================================
|
|
11
35
|
const authClient = createAuthClientFromConfig(config);
|
|
12
|
-
const { authStore, tokenStorage } = createAuthStore({
|
|
13
|
-
tokenStorageKey: config.tokenStorageKey || "auth-token",
|
|
14
|
-
});
|
|
15
|
-
// magicLink 由 magicLinkClient 插件提供,email 是 better-auth 内置方法
|
|
16
36
|
const signIn = authClient.signIn;
|
|
17
37
|
const signUp = authClient.signUp;
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
signOut: authClient.signOut,
|
|
25
|
-
signUp: signUp ? { email: signUp.email } : undefined,
|
|
38
|
+
const deleteUser = authClient.deleteUser;
|
|
39
|
+
// ============================================================
|
|
40
|
+
// 2. Store Layer - 响应式状态
|
|
41
|
+
// ============================================================
|
|
42
|
+
const { authStore, tokenStorage } = createAuthStore({
|
|
43
|
+
tokenStorageKey: config.tokenStorageKey || "auth-token",
|
|
26
44
|
});
|
|
45
|
+
const storeActions = createStoreActions(authStore, tokenStorage);
|
|
27
46
|
const authComputed = createAuthComputed(authStore);
|
|
47
|
+
// ============================================================
|
|
48
|
+
// 3. API Layer
|
|
49
|
+
// ============================================================
|
|
28
50
|
const apiClient = createApiClient({
|
|
29
51
|
authStore,
|
|
30
52
|
tokenStorage,
|
|
31
|
-
unauthorizedHandler:
|
|
53
|
+
unauthorizedHandler: storeActions,
|
|
32
54
|
baseURL: config.baseURL,
|
|
33
55
|
});
|
|
34
|
-
const authApi = createAuthApi(apiClient
|
|
56
|
+
const authApi = createAuthApi(apiClient);
|
|
35
57
|
const organizationApi = createOrganizationApi(apiClient, config.baseURL);
|
|
36
58
|
const teamApi = createTeamApi(apiClient);
|
|
37
|
-
|
|
59
|
+
// ============================================================
|
|
60
|
+
// 4. Service Layer - 业务逻辑
|
|
61
|
+
// ============================================================
|
|
62
|
+
const authService = createAuthService({
|
|
63
|
+
authApi,
|
|
64
|
+
storeActions,
|
|
65
|
+
tokenStorage,
|
|
66
|
+
}, {
|
|
67
|
+
deleteUser,
|
|
68
|
+
signIn: {
|
|
69
|
+
email: signIn.email,
|
|
70
|
+
magicLink: signIn.magicLink,
|
|
71
|
+
social: signIn.social,
|
|
72
|
+
},
|
|
73
|
+
signOut: authClient.signOut,
|
|
74
|
+
signUp: signUp ? { email: signUp.email } : undefined,
|
|
75
|
+
});
|
|
76
|
+
// ============================================================
|
|
77
|
+
// 5. Active 状态管理
|
|
78
|
+
// ============================================================
|
|
79
|
+
const basePath = "/v1/auth/organization";
|
|
80
|
+
async function setActiveOrganization(request) {
|
|
81
|
+
const response = await apiClient.post(`${basePath}/set-active`, request);
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new Error(`Failed to set active organization: ${response.status}`);
|
|
84
|
+
}
|
|
85
|
+
storeActions.setActiveOrganizationId(response.data.id);
|
|
86
|
+
return response.data;
|
|
87
|
+
}
|
|
88
|
+
async function setActiveTeam(request) {
|
|
89
|
+
const response = await apiClient.post(`${basePath}/set-active-team`, request);
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(`Failed to set active team: ${response.status}`);
|
|
92
|
+
}
|
|
93
|
+
storeActions.setActiveTeamId(request.teamId);
|
|
94
|
+
}
|
|
95
|
+
async function setActiveOrganizationAndTeam(organizationId, teamId) {
|
|
96
|
+
const organization = await setActiveOrganization({ organizationId });
|
|
97
|
+
await setActiveTeam({ teamId });
|
|
98
|
+
return organization;
|
|
99
|
+
}
|
|
100
|
+
// ============================================================
|
|
101
|
+
// 6. 返回 Auth 实例
|
|
102
|
+
// ============================================================
|
|
103
|
+
return {
|
|
104
|
+
// API Layer
|
|
38
105
|
apiClient,
|
|
39
|
-
authActions,
|
|
40
106
|
authApi,
|
|
41
|
-
authClient,
|
|
42
|
-
authComputed,
|
|
43
|
-
authStore,
|
|
44
107
|
organizationApi,
|
|
45
108
|
teamApi,
|
|
46
|
-
|
|
47
|
-
};
|
|
48
|
-
const utils = createBoundAuthUtils({
|
|
49
|
-
apiClient,
|
|
50
|
-
authActions,
|
|
109
|
+
// Store Layer
|
|
51
110
|
authStore,
|
|
111
|
+
authComputed,
|
|
52
112
|
tokenStorage,
|
|
53
|
-
|
|
54
|
-
|
|
113
|
+
storeActions,
|
|
114
|
+
// Service Layer
|
|
115
|
+
authService,
|
|
116
|
+
// Active 状态管理
|
|
117
|
+
setActiveOrganization,
|
|
118
|
+
setActiveTeam,
|
|
119
|
+
setActiveOrganizationAndTeam,
|
|
120
|
+
// Better Auth Client(高级用法)
|
|
121
|
+
authClient,
|
|
122
|
+
// 快捷方法
|
|
123
|
+
getCurrentUser: storeActions.getUser,
|
|
124
|
+
getCurrentUserId: storeActions.getUserId,
|
|
125
|
+
isAuthenticated: storeActions.isAuthenticated,
|
|
126
|
+
isLoading: storeActions.isLoading,
|
|
127
|
+
isLoaded: storeActions.isLoaded,
|
|
128
|
+
getAuthToken: () => tokenStorage.get(),
|
|
129
|
+
getAuthHeaders: () => {
|
|
130
|
+
const token = tokenStorage.get();
|
|
131
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
132
|
+
},
|
|
133
|
+
waitForAuth: () => waitForAuthUtil(authStore),
|
|
134
|
+
userManager: createUserManager(authStore),
|
|
135
|
+
};
|
|
55
136
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks 导出
|
|
3
|
+
*/
|
|
4
|
+
export { useAuthInit, initializeAuth } from "./use-auth-init";
|
|
5
|
+
export { useAuthSync, type UseAuthSyncConfig, type UseAuthSyncResult } from "./use-auth-sync";
|
|
6
|
+
export { useProtectedRoute, type UseProtectedRouteConfig, type UseProtectedRouteResult, type ProtectionStatus, } from "./use-protected-route";
|
|
7
|
+
export { useEmailVerification, type UseEmailVerificationConfig, type UseEmailVerificationResult, } from "./use-email-verification";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAC7D,OAAO,EAAE,WAAW,EAAE,KAAK,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAC7F,OAAO,EACL,iBAAiB,EACjB,KAAK,uBAAuB,EAC5B,KAAK,uBAAuB,EAC5B,KAAK,gBAAgB,GACtB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,oBAAoB,EACpB,KAAK,0BAA0B,EAC/B,KAAK,0BAA0B,GAChC,MAAM,0BAA0B,CAAA"}
|
|
@@ -3,15 +3,16 @@ import type { AuthInstance } from "../core";
|
|
|
3
3
|
* 初始化认证状态(非 hook 版本)
|
|
4
4
|
* 可在 React Router 的 clientLoader 中调用
|
|
5
5
|
*
|
|
6
|
+
* 职责:
|
|
7
|
+
* 1. 处理 URL 中的 token(OAuth 回调)并保存
|
|
8
|
+
* 2. 其他情况交给 Better Auth 的 useSession() 处理
|
|
9
|
+
*
|
|
6
10
|
* @returns 是否已认证
|
|
7
11
|
*/
|
|
8
12
|
export declare function initializeAuth(auth: AuthInstance): Promise<boolean>;
|
|
9
13
|
/**
|
|
10
14
|
* 认证初始化 hook
|
|
11
15
|
* 在应用启动时检查 URL 中的 token 或 localStorage 中的 token
|
|
12
|
-
*
|
|
13
|
-
* 注意:在 React Router Framework 模式下,应使用 initializeAuth 函数
|
|
14
|
-
* 在 clientLoader 中调用,而不是使用此 hook
|
|
15
16
|
*/
|
|
16
17
|
export declare function useAuthInit(auth: AuthInstance): void;
|
|
17
18
|
//# sourceMappingURL=use-auth-init.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-auth-init.d.ts","sourceRoot":"","sources":["../../src/hooks/use-auth-init.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C
|
|
1
|
+
{"version":3,"file":"use-auth-init.d.ts","sourceRoot":"","sources":["../../src/hooks/use-auth-init.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,CAwCzE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,QAQ7C"}
|
|
@@ -3,24 +3,24 @@ import { useEffect } from "react";
|
|
|
3
3
|
* 初始化认证状态(非 hook 版本)
|
|
4
4
|
* 可在 React Router 的 clientLoader 中调用
|
|
5
5
|
*
|
|
6
|
+
* 职责:
|
|
7
|
+
* 1. 处理 URL 中的 token(OAuth 回调)并保存
|
|
8
|
+
* 2. 其他情况交给 Better Auth 的 useSession() 处理
|
|
9
|
+
*
|
|
6
10
|
* @returns 是否已认证
|
|
7
11
|
*/
|
|
8
12
|
export async function initializeAuth(auth) {
|
|
9
|
-
const {
|
|
13
|
+
const { authService, tokenStorage, authStore, storeActions } = auth;
|
|
10
14
|
// 如果已经初始化,直接返回
|
|
11
15
|
if (authStore.isLoaded.get()) {
|
|
12
16
|
return authStore.isAuthenticated.get();
|
|
13
17
|
}
|
|
14
|
-
// 检查 URL 中的 token
|
|
18
|
+
// 检查 URL 中的 token(OAuth 回调)
|
|
15
19
|
const urlParams = new URLSearchParams(window.location.search);
|
|
16
20
|
let tokenFromUrl = urlParams.get("token");
|
|
17
21
|
if (tokenFromUrl) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
tokenFromUrl = decodeURIComponent(tokenFromUrl);
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
// 解码失败,使用原值
|
|
22
|
+
if (decodeURIComponent(tokenFromUrl) === tokenFromUrl) {
|
|
23
|
+
tokenFromUrl = encodeURIComponent(tokenFromUrl);
|
|
24
24
|
}
|
|
25
25
|
// 立即清理 URL 中的 token 参数(避免暴露)
|
|
26
26
|
urlParams.delete("token");
|
|
@@ -28,45 +28,33 @@ export async function initializeAuth(auth) {
|
|
|
28
28
|
? `${window.location.pathname}?${urlParams.toString()}`
|
|
29
29
|
: window.location.pathname;
|
|
30
30
|
window.history.replaceState({}, "", newUrl);
|
|
31
|
-
//
|
|
31
|
+
// URL token 必须验证并保存(OAuth 回调)
|
|
32
32
|
try {
|
|
33
|
-
await
|
|
33
|
+
await authService.fetchAndSetSession(tokenFromUrl);
|
|
34
34
|
return authStore.isAuthenticated.get();
|
|
35
35
|
}
|
|
36
36
|
catch {
|
|
37
|
-
// Token
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// 检查 localStorage 中是否有存储的 token
|
|
41
|
-
const storedToken = tokenStorage.get();
|
|
42
|
-
if (storedToken) {
|
|
43
|
-
try {
|
|
44
|
-
await authActions.fetchSessionWithToken(storedToken);
|
|
45
|
-
return authStore.isAuthenticated.get();
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
// Token 无效,初始化为未认证状态
|
|
49
|
-
await authActions.initialize(null, true);
|
|
37
|
+
// Token 无效,清理并标记为已加载
|
|
38
|
+
tokenStorage.clear();
|
|
39
|
+
storeActions.initialize(null, true);
|
|
50
40
|
return false;
|
|
51
41
|
}
|
|
52
42
|
}
|
|
53
|
-
// 没有 token
|
|
54
|
-
|
|
55
|
-
|
|
43
|
+
// 没有 URL token 时,不主动请求 session
|
|
44
|
+
// 让 Better Auth 的 useSession() 来处理 localStorage token
|
|
45
|
+
// 这里只标记初始化流程已完成(但 session 状态由 useSession 管理)
|
|
46
|
+
return authStore.isAuthenticated.get();
|
|
56
47
|
}
|
|
57
48
|
/**
|
|
58
49
|
* 认证初始化 hook
|
|
59
50
|
* 在应用启动时检查 URL 中的 token 或 localStorage 中的 token
|
|
60
|
-
*
|
|
61
|
-
* 注意:在 React Router Framework 模式下,应使用 initializeAuth 函数
|
|
62
|
-
* 在 clientLoader 中调用,而不是使用此 hook
|
|
63
51
|
*/
|
|
64
52
|
export function useAuthInit(auth) {
|
|
65
53
|
useEffect(() => {
|
|
66
54
|
initializeAuth(auth).catch((error) => {
|
|
67
55
|
console.error("Failed to initialize auth:", error);
|
|
68
56
|
// 即使认证初始化失败,也标记为已加载
|
|
69
|
-
auth.
|
|
57
|
+
auth.storeActions.initialize(null, true);
|
|
70
58
|
});
|
|
71
59
|
}, [auth]);
|
|
72
60
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 认证状态同步 Hook
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 监听 better-auth 的 session 变化
|
|
6
|
+
* 2. 同步认证状态到 authStore
|
|
7
|
+
* 3. 处理新用户的 companion team 设置
|
|
8
|
+
*
|
|
9
|
+
* 注意:
|
|
10
|
+
* - URL token 参数的处理由 useAuthInit 负责
|
|
11
|
+
* - 主题/语言同步由项目自行处理
|
|
12
|
+
*/
|
|
13
|
+
import type { AuthInstance } from "../core";
|
|
14
|
+
import type { SessionUser } from "../types";
|
|
15
|
+
/** useAuthSync 返回值类型 */
|
|
16
|
+
export interface UseAuthSyncResult {
|
|
17
|
+
isAuthenticated: boolean;
|
|
18
|
+
isLoaded: boolean;
|
|
19
|
+
isLoading: boolean;
|
|
20
|
+
user: SessionUser | null;
|
|
21
|
+
}
|
|
22
|
+
/** useAuthSync 配置 */
|
|
23
|
+
export interface UseAuthSyncConfig {
|
|
24
|
+
/**
|
|
25
|
+
* 认证状态变化回调
|
|
26
|
+
*/
|
|
27
|
+
onAuthChange?: (isAuthenticated: boolean) => void;
|
|
28
|
+
/**
|
|
29
|
+
* 是否跳过 companion team 设置
|
|
30
|
+
* 某些页面(如删除用户回调)不需要设置 companion team
|
|
31
|
+
*/
|
|
32
|
+
skipCompanionTeam?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* 跳过 companion team 的路径列表
|
|
35
|
+
* 如:['/auth/callback', '/auth/delete-success']
|
|
36
|
+
*/
|
|
37
|
+
skipCompanionTeamPaths?: string[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 认证状态同步 Hook
|
|
41
|
+
*
|
|
42
|
+
* 使用示例:
|
|
43
|
+
* ```tsx
|
|
44
|
+
* function App() {
|
|
45
|
+
* useAuthSync(auth, {
|
|
46
|
+
* skipCompanionTeamPaths: ['/auth/callback', '/auth/delete-success'],
|
|
47
|
+
* onAuthChange: (isAuthenticated) => {
|
|
48
|
+
* if (isAuthenticated) {
|
|
49
|
+
* syncTheme()
|
|
50
|
+
* syncLanguage()
|
|
51
|
+
* }
|
|
52
|
+
* }
|
|
53
|
+
* })
|
|
54
|
+
*
|
|
55
|
+
* return <YourApp />
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare function useAuthSync(auth: AuthInstance, config?: UseAuthSyncConfig): UseAuthSyncResult;
|
|
60
|
+
//# sourceMappingURL=use-auth-sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-auth-sync.d.ts","sourceRoot":"","sources":["../../src/hooks/use-auth-sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAG3C,wBAAwB;AACxB,MAAM,WAAW,iBAAiB;IAChC,eAAe,EAAE,OAAO,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;CACzB;AAED,qBAAqB;AACrB,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,eAAe,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAA;CAClC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,GAAE,iBAAsB,GAAG,iBAAiB,CA8FjG"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 认证状态同步 Hook
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 监听 better-auth 的 session 变化
|
|
6
|
+
* 2. 同步认证状态到 authStore
|
|
7
|
+
* 3. 处理新用户的 companion team 设置
|
|
8
|
+
*
|
|
9
|
+
* 注意:
|
|
10
|
+
* - URL token 参数的处理由 useAuthInit 负责
|
|
11
|
+
* - 主题/语言同步由项目自行处理
|
|
12
|
+
*/
|
|
13
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
14
|
+
import { setupCompanionTeam } from "../services/companion-team";
|
|
15
|
+
import { extractSessionUser } from "../utils/user-mapper";
|
|
16
|
+
/**
|
|
17
|
+
* 认证状态同步 Hook
|
|
18
|
+
*
|
|
19
|
+
* 使用示例:
|
|
20
|
+
* ```tsx
|
|
21
|
+
* function App() {
|
|
22
|
+
* useAuthSync(auth, {
|
|
23
|
+
* skipCompanionTeamPaths: ['/auth/callback', '/auth/delete-success'],
|
|
24
|
+
* onAuthChange: (isAuthenticated) => {
|
|
25
|
+
* if (isAuthenticated) {
|
|
26
|
+
* syncTheme()
|
|
27
|
+
* syncLanguage()
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* })
|
|
31
|
+
*
|
|
32
|
+
* return <YourApp />
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function useAuthSync(auth, config = {}) {
|
|
37
|
+
const { skipCompanionTeam = false, skipCompanionTeamPaths = [], onAuthChange, } = config;
|
|
38
|
+
const teamSetupRef = useRef(false);
|
|
39
|
+
const prevAuthRef = useRef(null);
|
|
40
|
+
// 获取 better-auth session
|
|
41
|
+
const { data: sessionData, isPending } = auth.authClient.useSession();
|
|
42
|
+
// 检测是否是新用户(从 URL 参数读取)
|
|
43
|
+
const isNewUser = useMemo(() => {
|
|
44
|
+
if (typeof window === "undefined")
|
|
45
|
+
return false;
|
|
46
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
47
|
+
return searchParams.get("isNew") === "true";
|
|
48
|
+
}, []);
|
|
49
|
+
// 检查当前路径是否应该跳过 companion team
|
|
50
|
+
const shouldSkipCompanionTeam = useMemo(() => {
|
|
51
|
+
if (skipCompanionTeam)
|
|
52
|
+
return true;
|
|
53
|
+
if (typeof window === "undefined")
|
|
54
|
+
return false;
|
|
55
|
+
const pathname = window.location.pathname;
|
|
56
|
+
return skipCompanionTeamPaths.some((path) => pathname === path || pathname.startsWith(path));
|
|
57
|
+
}, [skipCompanionTeam, skipCompanionTeamPaths]);
|
|
58
|
+
// 提取用户信息
|
|
59
|
+
const user = useMemo(() => extractSessionUser(sessionData), [sessionData]);
|
|
60
|
+
const isAuthenticated = !!user?.id;
|
|
61
|
+
const isLoaded = !isPending;
|
|
62
|
+
// 同步认证状态到 authStore
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
auth.storeActions.setUser(user);
|
|
65
|
+
auth.authStore.isLoaded.set(isLoaded);
|
|
66
|
+
auth.authStore.loading.set(!isLoaded);
|
|
67
|
+
}, [user, isLoaded, auth]);
|
|
68
|
+
// Companion team 设置(只有邮箱验证后才执行)
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (shouldSkipCompanionTeam)
|
|
71
|
+
return;
|
|
72
|
+
// 必须邮箱已验证才执行 companion team 设置
|
|
73
|
+
if (isLoaded &&
|
|
74
|
+
isAuthenticated &&
|
|
75
|
+
user?.id &&
|
|
76
|
+
user.emailVerified === true &&
|
|
77
|
+
!teamSetupRef.current) {
|
|
78
|
+
teamSetupRef.current = true;
|
|
79
|
+
const token = auth.tokenStorage.get();
|
|
80
|
+
if (token && typeof token === "string" && token.trim().length > 0) {
|
|
81
|
+
setupCompanionTeam(auth, token, {
|
|
82
|
+
onComplete: () => {
|
|
83
|
+
// 清除 isNew 参数
|
|
84
|
+
if (isNewUser && typeof window !== "undefined") {
|
|
85
|
+
const url = new URL(window.location.href);
|
|
86
|
+
url.searchParams.delete("isNew");
|
|
87
|
+
window.history.replaceState({}, "", url.pathname + url.search);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
onError: () => {
|
|
91
|
+
teamSetupRef.current = false;
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// 用户登出时重置
|
|
97
|
+
if (!user && teamSetupRef.current) {
|
|
98
|
+
teamSetupRef.current = false;
|
|
99
|
+
}
|
|
100
|
+
}, [user, isAuthenticated, isLoaded, auth, shouldSkipCompanionTeam, isNewUser]);
|
|
101
|
+
// 认证状态变化回调
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!onAuthChange)
|
|
104
|
+
return;
|
|
105
|
+
if (prevAuthRef.current === isAuthenticated)
|
|
106
|
+
return;
|
|
107
|
+
prevAuthRef.current = isAuthenticated;
|
|
108
|
+
onAuthChange(isAuthenticated);
|
|
109
|
+
}, [isAuthenticated, onAuthChange]);
|
|
110
|
+
return {
|
|
111
|
+
user,
|
|
112
|
+
isAuthenticated,
|
|
113
|
+
isLoaded,
|
|
114
|
+
isLoading: !isLoaded,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 邮箱验证 Hook
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 重新发送验证邮件
|
|
6
|
+
* 2. 检查邮箱是否已验证
|
|
7
|
+
* 3. 更换邮箱(登出并跳转)
|
|
8
|
+
* 4. 倒计时逻辑
|
|
9
|
+
*/
|
|
10
|
+
import type { AuthInstance } from "../core";
|
|
11
|
+
/** useEmailVerification 配置 */
|
|
12
|
+
export interface UseEmailVerificationConfig {
|
|
13
|
+
/** 邮箱地址 */
|
|
14
|
+
email: string;
|
|
15
|
+
/** 语言 */
|
|
16
|
+
lang?: string;
|
|
17
|
+
/** 倒计时时长(秒),默认 60 */
|
|
18
|
+
countdownDuration?: number;
|
|
19
|
+
/** 重定向函数 */
|
|
20
|
+
onRedirect?: (path: string) => void;
|
|
21
|
+
/** 发送成功回调 */
|
|
22
|
+
onSendSuccess?: (email: string) => void;
|
|
23
|
+
/** 发送失败回调 */
|
|
24
|
+
onSendError?: (error: Error) => void;
|
|
25
|
+
/** 已验证回调 */
|
|
26
|
+
onAlreadyVerified?: () => void;
|
|
27
|
+
}
|
|
28
|
+
/** useEmailVerification 返回值 */
|
|
29
|
+
export interface UseEmailVerificationResult {
|
|
30
|
+
/** 是否正在发送 */
|
|
31
|
+
isLoading: boolean;
|
|
32
|
+
/** 邮箱是否已验证 */
|
|
33
|
+
isAlreadyVerified: boolean;
|
|
34
|
+
/** 是否在倒计时中 */
|
|
35
|
+
isCountingDown: boolean;
|
|
36
|
+
/** 剩余倒计时秒数 */
|
|
37
|
+
countdown: number;
|
|
38
|
+
/** 重新发送验证邮件 */
|
|
39
|
+
resendVerification: () => Promise<void>;
|
|
40
|
+
/** 更换邮箱(登出并跳转) */
|
|
41
|
+
changeEmail: () => Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 邮箱验证 Hook
|
|
45
|
+
*
|
|
46
|
+
* 使用示例:
|
|
47
|
+
* ```tsx
|
|
48
|
+
* function VerifyEmailPage({ email, lang }) {
|
|
49
|
+
* const navigate = useNavigate()
|
|
50
|
+
*
|
|
51
|
+
* const {
|
|
52
|
+
* isLoading,
|
|
53
|
+
* isAlreadyVerified,
|
|
54
|
+
* isCountingDown,
|
|
55
|
+
* countdown,
|
|
56
|
+
* resendVerification,
|
|
57
|
+
* changeEmail
|
|
58
|
+
* } = useEmailVerification(auth, {
|
|
59
|
+
* email,
|
|
60
|
+
* lang,
|
|
61
|
+
* onRedirect: (path) => navigate(path),
|
|
62
|
+
* onSendSuccess: (email) => toast.success(`验证邮件已发送到 ${email}`),
|
|
63
|
+
* onSendError: () => toast.error('发送失败'),
|
|
64
|
+
* onAlreadyVerified: () => navigate('/community')
|
|
65
|
+
* })
|
|
66
|
+
*
|
|
67
|
+
* return (
|
|
68
|
+
* <div>
|
|
69
|
+
* {isAlreadyVerified ? (
|
|
70
|
+
* <p>邮箱已验证,正在跳转...</p>
|
|
71
|
+
* ) : (
|
|
72
|
+
* <>
|
|
73
|
+
* <button onClick={resendVerification} disabled={isLoading || isCountingDown}>
|
|
74
|
+
* {isCountingDown ? `${countdown}s 后重试` : '重新发送'}
|
|
75
|
+
* </button>
|
|
76
|
+
* <button onClick={changeEmail}>使用其他邮箱</button>
|
|
77
|
+
* </>
|
|
78
|
+
* )}
|
|
79
|
+
* </div>
|
|
80
|
+
* )
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export declare function useEmailVerification(auth: AuthInstance, config: UseEmailVerificationConfig): UseEmailVerificationResult;
|
|
85
|
+
//# sourceMappingURL=use-email-verification.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-email-verification.d.ts","sourceRoot":"","sources":["../../src/hooks/use-email-verification.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C,8BAA8B;AAC9B,MAAM,WAAW,0BAA0B;IACzC,WAAW;IACX,KAAK,EAAE,MAAM,CAAA;IACb,SAAS;IACT,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,qBAAqB;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,YAAY;IACZ,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,aAAa;IACb,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,aAAa;IACb,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IACpC,YAAY;IACZ,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAA;CAC/B;AAED,+BAA+B;AAC/B,MAAM,WAAW,0BAA0B;IACzC,aAAa;IACb,SAAS,EAAE,OAAO,CAAA;IAClB,cAAc;IACd,iBAAiB,EAAE,OAAO,CAAA;IAC1B,cAAc;IACd,cAAc,EAAE,OAAO,CAAA;IACvB,cAAc;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,eAAe;IACf,kBAAkB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACvC,kBAAkB;IAClB,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,0BAA0B,GACjC,0BAA0B,CA4G5B"}
|