@choiceform/shared-auth 0.1.0
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 +452 -0
- package/dist/components/auth-sync.d.ts +9 -0
- package/dist/components/auth-sync.d.ts.map +1 -0
- package/dist/components/auth-sync.js +60 -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 +28 -0
- package/dist/components/sign-in-page.d.ts +49 -0
- package/dist/components/sign-in-page.d.ts.map +1 -0
- package/dist/components/sign-in-page.js +33 -0
- package/dist/config.d.ts +50 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +14 -0
- package/dist/core.d.ts +2162 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +37 -0
- package/dist/hooks/use-auth-init.d.ts +7 -0
- package/dist/hooks/use-auth-init.d.ts.map +1 -0
- package/dist/hooks/use-auth-init.js +41 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/init.d.ts +2167 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +17 -0
- package/dist/lib/auth-client.d.ts +2120 -0
- package/dist/lib/auth-client.d.ts.map +1 -0
- package/dist/lib/auth-client.js +11 -0
- package/dist/store/actions.d.ts +60 -0
- package/dist/store/actions.d.ts.map +1 -0
- package/dist/store/actions.js +234 -0
- package/dist/store/computed.d.ts +12 -0
- package/dist/store/computed.d.ts.map +1 -0
- package/dist/store/computed.js +14 -0
- package/dist/store/state.d.ts +16 -0
- package/dist/store/state.d.ts.map +1 -0
- package/dist/store/state.js +52 -0
- package/dist/store/utils.d.ts +103 -0
- package/dist/store/utils.d.ts.map +1 -0
- package/dist/store/utils.js +198 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
# @choiceform/shared-auth
|
|
2
|
+
|
|
3
|
+
共享认证包 - 用于 Choiceform 项目的统一认证解决方案
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- ✅ Better Auth 集成
|
|
8
|
+
- ✅ Legend State 状态管理
|
|
9
|
+
- ✅ GitHub OAuth 支持
|
|
10
|
+
- ✅ Token 自动管理和持久化
|
|
11
|
+
- ✅ 路由保护组件
|
|
12
|
+
- ✅ 登录页面组件(可定制)
|
|
13
|
+
- ✅ 完整的 TypeScript 类型支持
|
|
14
|
+
- ✅ 预配置的 API 客户端(自动添加认证头)
|
|
15
|
+
- ✅ 用户管理器工具
|
|
16
|
+
- ✅ 响应式状态管理(基于 Legend State)
|
|
17
|
+
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm add @choiceform/shared-auth
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 环境变量配置
|
|
25
|
+
|
|
26
|
+
在使用认证功能前,需要在项目根目录配置环境变量:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# .env
|
|
30
|
+
VITE_AUTH_API_URL=http://localhost:4320
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**注意:** 如果你的项目仍在使用 `VITE_CORE_AI_API_URL`,代码会自动向后兼容,但建议迁移到新的 `VITE_AUTH_API_URL`。
|
|
34
|
+
|
|
35
|
+
## 快速开始
|
|
36
|
+
|
|
37
|
+
### 1. 初始化认证系统
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { initAuth } from "@choiceform/shared-auth"
|
|
41
|
+
import { adminClient, organizationClient } from "better-auth/client/plugins"
|
|
42
|
+
|
|
43
|
+
export const auth = initAuth({
|
|
44
|
+
baseURL: import.meta.env.VITE_AUTH_API_URL || "http://localhost:4320",
|
|
45
|
+
tokenStorageKey: "auth-token",
|
|
46
|
+
defaultRedirectAfterLogin: "/explore",
|
|
47
|
+
signInPath: "/sign-in",
|
|
48
|
+
plugins: [adminClient(), organizationClient({ teams: { enabled: true } })],
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 2. 在应用中使用
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { AuthSync, ProtectedRoute, useAuthInit } from "@choiceform/shared-auth"
|
|
56
|
+
import { auth } from "./lib/auth"
|
|
57
|
+
|
|
58
|
+
function App() {
|
|
59
|
+
useAuthInit(auth)
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
<AuthSync auth={auth} />
|
|
64
|
+
<Router>
|
|
65
|
+
<Route path="/sign-in" element={<SignInPage />} />
|
|
66
|
+
<Route
|
|
67
|
+
path="/*"
|
|
68
|
+
element={
|
|
69
|
+
<ProtectedRoute auth={auth} signInPath="/sign-in">
|
|
70
|
+
<YourApp />
|
|
71
|
+
</ProtectedRoute>
|
|
72
|
+
}
|
|
73
|
+
/>
|
|
74
|
+
</Router>
|
|
75
|
+
</>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 3. 使用登录页面组件
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { SignInPage } from "@choiceform/shared-auth"
|
|
84
|
+
import { auth } from "./lib/auth"
|
|
85
|
+
|
|
86
|
+
function LoginPage() {
|
|
87
|
+
return (
|
|
88
|
+
<SignInPage
|
|
89
|
+
auth={auth}
|
|
90
|
+
logo={<YourLogo />}
|
|
91
|
+
title="Your App Title"
|
|
92
|
+
description="Your app description"
|
|
93
|
+
provider="github"
|
|
94
|
+
redirectAfterLogin="/dashboard"
|
|
95
|
+
/>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## API 文档
|
|
101
|
+
|
|
102
|
+
### `initAuth(config)`
|
|
103
|
+
|
|
104
|
+
快速初始化认证系统(使用默认配置)。
|
|
105
|
+
|
|
106
|
+
**参数:**
|
|
107
|
+
|
|
108
|
+
- `baseURL`: Better Auth API 基础 URL(必需)
|
|
109
|
+
- `tokenStorageKey?`: Token 存储的 localStorage key(默认: `'auth-token'`)
|
|
110
|
+
- `defaultRedirectAfterLogin?`: 登录后默认跳转地址(默认: `'/explore'`)
|
|
111
|
+
- `signInPath?`: 登录页面路径(默认: `'/sign-in'`)
|
|
112
|
+
- `plugins?`: Better Auth 插件列表(默认包含 `adminClient()` 和 `organizationClient()`)
|
|
113
|
+
|
|
114
|
+
**返回:** `AuthInstance`
|
|
115
|
+
|
|
116
|
+
### `createAuth(config)`
|
|
117
|
+
|
|
118
|
+
创建认证系统实例(更灵活的配置选项)。
|
|
119
|
+
|
|
120
|
+
**参数:**
|
|
121
|
+
|
|
122
|
+
- `baseURL`: Better Auth API 基础 URL(必需)
|
|
123
|
+
- `tokenStorageKey?`: Token 存储的 localStorage key(默认: `'auth-token'`)
|
|
124
|
+
- `defaultRedirectAfterLogin?`: 登录后默认跳转地址(默认: `'/explore'`)
|
|
125
|
+
- `signInPath?`: 登录页面路径(默认: `'/sign-in'`)
|
|
126
|
+
- `plugins?`: Better Auth 插件列表
|
|
127
|
+
- `callbackURLBuilder?`: 自定义回调 URL 构建函数
|
|
128
|
+
- `getSessionEndpoint?`: 获取 session 的端点路径(默认: `'/v1/auth/get-session'`)
|
|
129
|
+
|
|
130
|
+
**返回:** `AuthInstance`
|
|
131
|
+
|
|
132
|
+
### `AuthInstance`
|
|
133
|
+
|
|
134
|
+
认证实例包含以下属性和方法:
|
|
135
|
+
|
|
136
|
+
**核心属性:**
|
|
137
|
+
|
|
138
|
+
- `authStore`: Legend State store(响应式状态)
|
|
139
|
+
- `authActions`: 认证操作(signIn, signOut, initialize 等)
|
|
140
|
+
- `authComputed`: 计算属性(isInitializing 等)
|
|
141
|
+
- `authClient`: Better Auth 客户端
|
|
142
|
+
- `tokenStorage`: Token 存储工具
|
|
143
|
+
|
|
144
|
+
**工具方法(已绑定到实例):**
|
|
145
|
+
|
|
146
|
+
- `getCurrentUser()`: 获取当前用户
|
|
147
|
+
- `getCurrentUserId()`: 获取当前用户ID
|
|
148
|
+
- `getCurrentUserIdSafe()`: 安全获取当前用户ID
|
|
149
|
+
- `isAuthenticated()`: 检查是否已认证
|
|
150
|
+
- `isLoading()`: 检查是否正在加载
|
|
151
|
+
- `isLoaded()`: 检查是否已加载
|
|
152
|
+
- `waitForAuth()`: 等待认证完成
|
|
153
|
+
- `getAuthToken()`: 获取认证 Token
|
|
154
|
+
- `getAuthHeaders()`: 获取认证 Headers
|
|
155
|
+
- `apiClient`: 预配置的 API 客户端(包含 get/post/put/delete 方法)
|
|
156
|
+
- `userManager`: 用户管理器(包含 getUser/getUserId 方法)
|
|
157
|
+
|
|
158
|
+
### `ProtectedRoute`
|
|
159
|
+
|
|
160
|
+
路由保护组件。
|
|
161
|
+
|
|
162
|
+
**Props:**
|
|
163
|
+
|
|
164
|
+
- `auth`: AuthInstance
|
|
165
|
+
- `children`: React.ReactNode
|
|
166
|
+
- `signInPath?`: 登录页面路径(默认: `'/sign-in'`)
|
|
167
|
+
- `loadingComponent?`: 加载中组件
|
|
168
|
+
- `loadingMessage?`: 加载中消息(默认: `'Checking authentication...'`)
|
|
169
|
+
|
|
170
|
+
### `SignInPage`
|
|
171
|
+
|
|
172
|
+
登录页面组件。
|
|
173
|
+
|
|
174
|
+
**Props:**
|
|
175
|
+
|
|
176
|
+
- `auth`: AuthInstance
|
|
177
|
+
- `className?`: 自定义样式类名
|
|
178
|
+
- `logo?`: Logo 组件
|
|
179
|
+
- `title?`: 标题
|
|
180
|
+
- `description?`: 描述
|
|
181
|
+
- `provider?`: OAuth 提供商(默认: `'github'`)
|
|
182
|
+
- `redirectAfterLogin?`: 登录后跳转地址(默认: `'/explore'`)
|
|
183
|
+
- `signInButtonText?`: 登录按钮文本(默认: `'Sign in with GitHub'`)
|
|
184
|
+
- `signingInButtonText?`: 登录中按钮文本(默认: `'Redirecting to GitHub...'`)
|
|
185
|
+
- `signInButtonIcon?`: 登录按钮图标
|
|
186
|
+
|
|
187
|
+
## 工具函数
|
|
188
|
+
|
|
189
|
+
### 使用已绑定的工具方法(推荐)
|
|
190
|
+
|
|
191
|
+
认证实例已包含所有工具方法,可以直接使用:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { auth } from "./lib/auth"
|
|
195
|
+
|
|
196
|
+
// 获取当前用户
|
|
197
|
+
const user = auth.getCurrentUser()
|
|
198
|
+
|
|
199
|
+
// 获取当前用户ID
|
|
200
|
+
const userId = auth.getCurrentUserId()
|
|
201
|
+
const userIdSafe = auth.getCurrentUserIdSafe()
|
|
202
|
+
|
|
203
|
+
// 检查认证状态
|
|
204
|
+
const authenticated = auth.isAuthenticated()
|
|
205
|
+
const loading = auth.isLoading()
|
|
206
|
+
const loaded = auth.isLoaded()
|
|
207
|
+
|
|
208
|
+
// 等待认证完成
|
|
209
|
+
await auth.waitForAuth()
|
|
210
|
+
|
|
211
|
+
// 获取认证 Token
|
|
212
|
+
const token = await auth.getAuthToken()
|
|
213
|
+
|
|
214
|
+
// 获取认证 Headers
|
|
215
|
+
const headers = await auth.getAuthHeaders()
|
|
216
|
+
|
|
217
|
+
// 使用预配置的 API 客户端
|
|
218
|
+
const response = await auth.apiClient.get("/api/users")
|
|
219
|
+
const data = await auth.apiClient.post("/api/data", { name: "test" })
|
|
220
|
+
|
|
221
|
+
// 使用用户管理器
|
|
222
|
+
const user = auth.userManager.getUser()
|
|
223
|
+
const userId = auth.userManager.getUserId()
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### 使用独立的工具函数
|
|
227
|
+
|
|
228
|
+
如果需要独立的工具函数(需要手动传入参数),可以从包中导入:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
import {
|
|
232
|
+
getCurrentUser,
|
|
233
|
+
getCurrentUserId,
|
|
234
|
+
isAuthenticated,
|
|
235
|
+
getAuthToken,
|
|
236
|
+
getAuthHeaders,
|
|
237
|
+
createApiClient,
|
|
238
|
+
createUserManager,
|
|
239
|
+
} from "@choiceform/shared-auth"
|
|
240
|
+
|
|
241
|
+
// 使用独立的工具函数(需要传入 authStore 等参数)
|
|
242
|
+
const user = getCurrentUser(auth.authStore)
|
|
243
|
+
const userId = getCurrentUserId(auth.authStore)
|
|
244
|
+
const authenticated = isAuthenticated(auth.authStore)
|
|
245
|
+
|
|
246
|
+
// 创建 API 客户端
|
|
247
|
+
const apiClient = createApiClient(
|
|
248
|
+
auth.authStore,
|
|
249
|
+
auth.tokenStorage,
|
|
250
|
+
auth.authActions,
|
|
251
|
+
auth.authClient
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
// 创建用户管理器
|
|
255
|
+
const userManager = createUserManager(auth.authStore)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## 类型定义
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import type {
|
|
262
|
+
SessionUser,
|
|
263
|
+
Session,
|
|
264
|
+
AuthState,
|
|
265
|
+
AuthConfig,
|
|
266
|
+
AuthInstance,
|
|
267
|
+
AuthActions,
|
|
268
|
+
TokenStorage,
|
|
269
|
+
} from "@choiceform/shared-auth"
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### 类型说明
|
|
273
|
+
|
|
274
|
+
- `SessionUser`: 用户信息类型
|
|
275
|
+
- `Session`: 会话信息类型
|
|
276
|
+
- `AuthState`: 认证状态类型(包含 user, isAuthenticated, loading 等)
|
|
277
|
+
- `AuthConfig`: 认证配置类型
|
|
278
|
+
- `AuthInstance`: 认证实例类型(`initAuth` 或 `createAuth` 的返回值)
|
|
279
|
+
- `AuthActions`: 认证操作类型
|
|
280
|
+
- `TokenStorage`: Token 存储工具类型
|
|
281
|
+
|
|
282
|
+
## 环境变量
|
|
283
|
+
|
|
284
|
+
### 必需的环境变量
|
|
285
|
+
|
|
286
|
+
- `VITE_AUTH_API_URL`: Better Auth API 基础 URL
|
|
287
|
+
|
|
288
|
+
### 示例配置
|
|
289
|
+
|
|
290
|
+
参考项目根目录的 `.env.example` 文件:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
# .env
|
|
294
|
+
VITE_AUTH_API_URL=http://localhost:4320
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### 向后兼容
|
|
298
|
+
|
|
299
|
+
如果你的项目仍在使用 `VITE_CORE_AI_API_URL`,代码会自动向后兼容:
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
baseURL: import.meta.env.VITE_AUTH_API_URL || import.meta.env.VITE_CORE_AI_API_URL || "http://localhost:4320"
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
但建议迁移到新的 `VITE_AUTH_API_URL`。
|
|
306
|
+
|
|
307
|
+
## 响应式状态使用
|
|
308
|
+
|
|
309
|
+
由于认证状态使用 Legend State 管理,你可以在 React 组件中使用 `use$` hook 来响应式地访问状态:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
import { use$ } from "@legendapp/state/react"
|
|
313
|
+
import { auth } from "./lib/auth"
|
|
314
|
+
|
|
315
|
+
function UserProfile() {
|
|
316
|
+
// 响应式获取用户信息
|
|
317
|
+
const user = use$(auth.authStore.user)
|
|
318
|
+
const isAuthenticated = use$(auth.authStore.isAuthenticated)
|
|
319
|
+
const loading = use$(auth.authStore.loading)
|
|
320
|
+
|
|
321
|
+
if (loading) {
|
|
322
|
+
return <div>Loading...</div>
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!isAuthenticated) {
|
|
326
|
+
return <div>Please sign in</div>
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<div>
|
|
331
|
+
<h1>Welcome, {user?.name}</h1>
|
|
332
|
+
<p>Email: {user?.email}</p>
|
|
333
|
+
</div>
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### 使用计算属性
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
import { use$ } from "@legendapp/state/react"
|
|
342
|
+
import { auth } from "./lib/auth"
|
|
343
|
+
|
|
344
|
+
function AuthStatus() {
|
|
345
|
+
// 使用计算属性检查初始化状态
|
|
346
|
+
const isInitializing = use$(auth.authComputed.isInitializing)
|
|
347
|
+
|
|
348
|
+
if (isInitializing) {
|
|
349
|
+
return <div>Initializing...</div>
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return <div>Ready</div>
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## 最佳实践
|
|
357
|
+
|
|
358
|
+
### 1. 在组件中使用响应式状态
|
|
359
|
+
|
|
360
|
+
优先使用 `use$` hook 来访问响应式状态,而不是直接调用 `getCurrentUser()` 等方法:
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
// ✅ 推荐:响应式更新
|
|
364
|
+
const user = use$(auth.authStore.user)
|
|
365
|
+
|
|
366
|
+
// ❌ 不推荐:非响应式
|
|
367
|
+
const user = auth.getCurrentUser()
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### 2. 使用 API 客户端进行 API 调用
|
|
371
|
+
|
|
372
|
+
使用预配置的 `apiClient` 可以自动处理认证头和处理 401 响应:
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
// ✅ 推荐:使用 apiClient
|
|
376
|
+
const response = await auth.apiClient.get("/api/users")
|
|
377
|
+
const data = await response.json()
|
|
378
|
+
|
|
379
|
+
// ❌ 不推荐:手动添加认证头
|
|
380
|
+
const headers = await auth.getAuthHeaders()
|
|
381
|
+
const response = await fetch("/api/users", { headers })
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### 3. 等待认证完成
|
|
385
|
+
|
|
386
|
+
在进行需要认证的操作前,使用 `waitForAuth()` 确保认证已初始化:
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
// ✅ 推荐:等待认证完成
|
|
390
|
+
await auth.waitForAuth()
|
|
391
|
+
if (auth.isAuthenticated()) {
|
|
392
|
+
// 执行需要认证的操作
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ❌ 不推荐:直接检查状态(可能尚未初始化)
|
|
396
|
+
if (auth.isAuthenticated()) {
|
|
397
|
+
// 可能不准确
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### 4. 错误处理
|
|
402
|
+
|
|
403
|
+
认证操作可能会失败,确保适当处理错误:
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
try {
|
|
407
|
+
await auth.authActions.signIn("github", redirectUrl)
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.error("Sign in failed:", error)
|
|
410
|
+
// 显示错误提示给用户
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### 5. 退出登录
|
|
415
|
+
|
|
416
|
+
退出登录时使用 `signOut` 方法:
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
async function handleSignOut() {
|
|
420
|
+
try {
|
|
421
|
+
await auth.authActions.signOut()
|
|
422
|
+
// 用户将被重定向到登录页面
|
|
423
|
+
} catch (error) {
|
|
424
|
+
console.error("Sign out failed:", error)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
## 常见问题
|
|
430
|
+
|
|
431
|
+
### Q: 如何在非 React 环境中使用?
|
|
432
|
+
|
|
433
|
+
A: 可以使用独立的工具函数,它们不依赖 React:
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
import { getCurrentUser, isAuthenticated } from "@choiceform/shared-auth"
|
|
437
|
+
|
|
438
|
+
const user = getCurrentUser(auth.authStore)
|
|
439
|
+
const authenticated = isAuthenticated(auth.authStore)
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### Q: 如何处理 Token 过期?
|
|
443
|
+
|
|
444
|
+
A: `apiClient` 会自动处理 401 响应,当检测到未授权时会调用 `handleUnauthorized`,通常会清除认证状态并重定向到登录页面。
|
|
445
|
+
|
|
446
|
+
### Q: 如何自定义认证流程?
|
|
447
|
+
|
|
448
|
+
A: 可以使用 `createAuth` 替代 `initAuth`,它提供更多配置选项,包括自定义回调 URL 构建函数等。
|
|
449
|
+
|
|
450
|
+
## 许可证
|
|
451
|
+
|
|
452
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-sync.d.ts","sourceRoot":"","sources":["../../src/components/auth-sync.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AA8C3C;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,QA8BxD"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* 将日期转换为 ISO 字符串
|
|
4
|
+
*/
|
|
5
|
+
function toISOString(date) {
|
|
6
|
+
if (!date)
|
|
7
|
+
return undefined;
|
|
8
|
+
return typeof date === "string" ? date : new Date(date).toISOString();
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 将 Better Auth session 用户数据映射为 SessionUser
|
|
12
|
+
*/
|
|
13
|
+
function mapSessionUserToSessionUser(sessionUser, sessionCreatedAt) {
|
|
14
|
+
return {
|
|
15
|
+
banExpires: sessionUser.banExpires,
|
|
16
|
+
banReason: sessionUser.banReason,
|
|
17
|
+
banned: sessionUser.banned,
|
|
18
|
+
createdAt: toISOString(sessionUser.createdAt) ?? "",
|
|
19
|
+
email: sessionUser.email,
|
|
20
|
+
emailVerified: sessionUser.emailVerified,
|
|
21
|
+
id: sessionUser.id,
|
|
22
|
+
image: sessionUser.image || undefined,
|
|
23
|
+
lastLoginAt: toISOString(sessionCreatedAt),
|
|
24
|
+
name: sessionUser.name,
|
|
25
|
+
role: sessionUser.role,
|
|
26
|
+
updatedAt: toISOString(sessionUser.updatedAt) ?? "",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Better Auth 认证状态同步组件
|
|
31
|
+
* 使用官方的 useSession hook 同步认证状态
|
|
32
|
+
*/
|
|
33
|
+
export function AuthSync({ auth }) {
|
|
34
|
+
const { authClient, authActions } = auth;
|
|
35
|
+
const { data: session, isPending, error } = authClient.useSession();
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
// 根据 session 状态更新 store
|
|
38
|
+
if (isPending) {
|
|
39
|
+
authActions.setLoading(true);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
authActions.setLoading(false);
|
|
43
|
+
if (error) {
|
|
44
|
+
authActions.setError(error.message || "Authentication error");
|
|
45
|
+
authActions.initialize(null, true);
|
|
46
|
+
}
|
|
47
|
+
else if (session) {
|
|
48
|
+
// 映射用户数据:使用 session 的创建时间作为最后登录时间
|
|
49
|
+
const user = mapSessionUserToSessionUser(session.user, session.session?.createdAt);
|
|
50
|
+
authActions.initialize(user, true);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// 没有 session,清空用户信息
|
|
54
|
+
authActions.initialize(null, true);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}, [session, isPending, error, authActions]);
|
|
58
|
+
// 这是一个隐形组件,不渲染任何内容
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { AuthInstance } from "../core";
|
|
3
|
+
interface ProtectedRouteProps {
|
|
4
|
+
auth: AuthInstance;
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
loadingComponent?: React.ComponentType<{
|
|
7
|
+
message?: string;
|
|
8
|
+
}>;
|
|
9
|
+
loadingMessage?: string;
|
|
10
|
+
signInPath?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 路由保护组件
|
|
14
|
+
* 等待认证初始化完成,根据认证状态进行路由保护
|
|
15
|
+
*/
|
|
16
|
+
export declare const ProtectedRoute: React.FC<ProtectedRouteProps>;
|
|
17
|
+
export default ProtectedRoute;
|
|
18
|
+
//# sourceMappingURL=protected-route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protected-route.d.ts","sourceRoot":"","sources":["../../src/components/protected-route.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C,UAAU,mBAAmB;IAC3B,IAAI,EAAE,YAAY,CAAA;IAClB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,gBAAgB,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC5D,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC,mBAAmB,CA8BxD,CAAA;AAED,eAAe,cAAc,CAAA"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { observer } from "@legendapp/state/react";
|
|
3
|
+
import { Navigate } from "react-router";
|
|
4
|
+
/**
|
|
5
|
+
* 路由保护组件
|
|
6
|
+
* 等待认证初始化完成,根据认证状态进行路由保护
|
|
7
|
+
*/
|
|
8
|
+
export const ProtectedRoute = observer(({ children, auth, signInPath = "/sign-in", loadingComponent, loadingMessage }) => {
|
|
9
|
+
const { authStore, authComputed } = auth;
|
|
10
|
+
// 检查各种状态
|
|
11
|
+
const isInitializing = authComputed.isInitializing.get();
|
|
12
|
+
const isAuthenticated = authStore.isAuthenticated.get();
|
|
13
|
+
// 检查初始化状态
|
|
14
|
+
if (isInitializing) {
|
|
15
|
+
if (loadingComponent) {
|
|
16
|
+
const LoadingComponent = loadingComponent;
|
|
17
|
+
return _jsx(LoadingComponent, { message: loadingMessage });
|
|
18
|
+
}
|
|
19
|
+
return _jsx("div", { children: loadingMessage || "Checking authentication..." });
|
|
20
|
+
}
|
|
21
|
+
// 检查认证状态
|
|
22
|
+
if (!isAuthenticated) {
|
|
23
|
+
return (_jsx(Navigate, { to: signInPath, replace: true }));
|
|
24
|
+
}
|
|
25
|
+
// 所有检查通过,渲染子组件
|
|
26
|
+
return _jsx(_Fragment, { children: children });
|
|
27
|
+
});
|
|
28
|
+
export default ProtectedRoute;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { AuthInstance } from "../core";
|
|
3
|
+
interface SignInPageProps {
|
|
4
|
+
auth: AuthInstance;
|
|
5
|
+
/**
|
|
6
|
+
* 自定义样式类名
|
|
7
|
+
*/
|
|
8
|
+
className?: string;
|
|
9
|
+
/**
|
|
10
|
+
* 描述
|
|
11
|
+
*/
|
|
12
|
+
description?: string;
|
|
13
|
+
/**
|
|
14
|
+
* 自定义 Logo 组件
|
|
15
|
+
*/
|
|
16
|
+
logo?: React.ReactNode;
|
|
17
|
+
/**
|
|
18
|
+
* OAuth 提供商
|
|
19
|
+
* @default 'github'
|
|
20
|
+
*/
|
|
21
|
+
provider?: string;
|
|
22
|
+
/**
|
|
23
|
+
* 登录成功后的重定向路径
|
|
24
|
+
* @default '/explore'
|
|
25
|
+
*/
|
|
26
|
+
redirectAfterLogin?: string;
|
|
27
|
+
/**
|
|
28
|
+
* 登录按钮图标
|
|
29
|
+
*/
|
|
30
|
+
signInButtonIcon?: React.ReactNode;
|
|
31
|
+
/**
|
|
32
|
+
* 登录按钮文本
|
|
33
|
+
*/
|
|
34
|
+
signInButtonText?: string;
|
|
35
|
+
/**
|
|
36
|
+
* 登录中按钮文本
|
|
37
|
+
*/
|
|
38
|
+
signingInButtonText?: string;
|
|
39
|
+
/**
|
|
40
|
+
* 标题
|
|
41
|
+
*/
|
|
42
|
+
title?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 登录页面组件
|
|
46
|
+
*/
|
|
47
|
+
export declare function SignInPage({ auth, redirectAfterLogin, provider, logo, title, description, signInButtonText, signingInButtonText, signInButtonIcon, className, }: SignInPageProps): import("react/jsx-runtime").JSX.Element;
|
|
48
|
+
export {};
|
|
49
|
+
//# sourceMappingURL=sign-in-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sign-in-page.d.ts","sourceRoot":"","sources":["../../src/components/sign-in-page.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAA;AAIlD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C,UAAU,eAAe;IACvB,IAAI,EAAE,YAAY,CAAA;IAClB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;OAEG;IACH,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B;;OAEG;IACH,gBAAgB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAClC;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;OAEG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,EACzB,IAAI,EACJ,kBAA+B,EAC/B,QAAmB,EACnB,IAAI,EACJ,KAAK,EACL,WAAW,EACX,gBAAwC,EACxC,mBAAgD,EAChD,gBAAgB,EAChB,SAAS,GACV,EAAE,eAAe,2CAyDjB"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { useNavigate } from "react-router";
|
|
4
|
+
import { use$ } from "@legendapp/state/react";
|
|
5
|
+
import { Button } from "@choiceform/design-system";
|
|
6
|
+
/**
|
|
7
|
+
* 登录页面组件
|
|
8
|
+
*/
|
|
9
|
+
export function SignInPage({ auth, redirectAfterLogin = "/explore", provider = "github", logo, title, description, signInButtonText = "Sign in with GitHub", signingInButtonText = "Redirecting to GitHub...", signInButtonIcon, className, }) {
|
|
10
|
+
const navigate = useNavigate();
|
|
11
|
+
const { authStore, authActions } = auth;
|
|
12
|
+
const { isAuthenticated, error } = use$(authStore);
|
|
13
|
+
const [isSigningIn, setIsSigningIn] = useState(false);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (isAuthenticated) {
|
|
16
|
+
navigate(redirectAfterLogin);
|
|
17
|
+
}
|
|
18
|
+
}, [isAuthenticated, navigate, redirectAfterLogin]);
|
|
19
|
+
const handleSignIn = async () => {
|
|
20
|
+
// 设置本地 loading 状态
|
|
21
|
+
setIsSigningIn(true);
|
|
22
|
+
try {
|
|
23
|
+
await authActions.signIn(provider, `${window.location.origin}${redirectAfterLogin}`);
|
|
24
|
+
// OAuth 会重定向,所以这里的代码可能不会执行
|
|
25
|
+
// loading 状态会在页面刷新后重置
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
// 如果出错,重置 loading 状态
|
|
29
|
+
setIsSigningIn(false);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
return (_jsx("div", { className: className || "flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900", children: _jsxs("div", { className: "w-full max-w-md space-y-8", children: [logo && (_jsxs("div", { className: "mb-8", children: [_jsx("div", { className: "flex items-center gap-2", children: logo }), title && (_jsxs("div", { className: "p-4", children: [title && _jsx("p", { className: "text-heading-large mt-2", children: title }), description && (_jsx("p", { className: "text-secondary-foreground text-body-large mt-2", children: description }))] }))] })), _jsxs("div", { className: "flex flex-col items-start justify-start gap-2 p-4", children: [_jsxs(Button, { onClick: handleSignIn, disabled: isSigningIn, loading: isSigningIn, size: "large", children: [signInButtonIcon && _jsx("span", { className: "flex items-center", children: signInButtonIcon }), _jsx("span", { children: isSigningIn ? signingInButtonText : signInButtonText })] }), error && _jsx("p", { className: "text-danger-foreground", children: error })] })] }) }));
|
|
33
|
+
}
|