sumor 3.2.2 → 3.3.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 +372 -413
- package/README.zh-CN.md +576 -0
- package/dist/server/index.d.ts +2 -6
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -5
- package/dist/server/index.js.map +1 -1
- package/dist/server/middlewares/loadJwtUserMiddleware.d.ts +0 -1
- package/dist/server/middlewares/loadJwtUserMiddleware.d.ts.map +1 -1
- package/dist/server/middlewares/loadJwtUserMiddleware.js +4 -2
- package/dist/server/middlewares/loadJwtUserMiddleware.js.map +1 -1
- package/dist/server/mock/mockApiRoutes.d.ts +14 -0
- package/dist/server/mock/mockApiRoutes.d.ts.map +1 -0
- package/dist/server/mock/mockApiRoutes.js +151 -0
- package/dist/server/mock/mockApiRoutes.js.map +1 -0
- package/dist/server/mock/mockConfig.d.ts +38 -0
- package/dist/server/mock/mockConfig.d.ts.map +1 -0
- package/dist/server/mock/mockConfig.js +51 -0
- package/dist/server/mock/mockConfig.js.map +1 -0
- package/dist/server/mock/mockRoutes.d.ts +9 -0
- package/dist/server/mock/mockRoutes.d.ts.map +1 -0
- package/dist/server/mock/mockRoutes.js +103 -0
- package/dist/server/mock/mockRoutes.js.map +1 -0
- package/dist/server/mock/mockTokenUtils.d.ts +30 -0
- package/dist/server/mock/mockTokenUtils.d.ts.map +1 -0
- package/dist/server/mock/mockTokenUtils.js +81 -0
- package/dist/server/mock/mockTokenUtils.js.map +1 -0
- package/dist/server/routes.d.ts +1 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +29 -25
- package/dist/server/routes.js.map +1 -1
- package/dist/server/services/oauthService.d.ts +0 -8
- package/dist/server/services/oauthService.d.ts.map +1 -1
- package/dist/server/services/oauthService.js +0 -24
- package/dist/server/services/oauthService.js.map +1 -1
- package/dist/server/types/oauth.d.ts +0 -1
- package/dist/server/types/oauth.d.ts.map +1 -1
- package/dist/server/utils/config.d.ts.map +1 -1
- package/dist/server/utils/config.js +13 -0
- package/dist/server/utils/config.js.map +1 -1
- package/dist/web/OAuthStore.d.ts +11 -5
- package/dist/web/OAuthStore.d.ts.map +1 -1
- package/dist/web/OAuthStore.js +43 -64
- package/dist/web/OAuthStore.js.map +1 -1
- package/dist/web/UrlHelper.d.ts +1 -0
- package/dist/web/UrlHelper.d.ts.map +1 -1
- package/dist/web/UrlHelper.js +11 -0
- package/dist/web/UrlHelper.js.map +1 -1
- package/dist/web/api/login.d.ts +2 -2
- package/dist/web/api/login.d.ts.map +1 -1
- package/dist/web/api/login.js +12 -2
- package/dist/web/api/login.js.map +1 -1
- package/dist/web/api/logout.d.ts +1 -1
- package/dist/web/api/logout.d.ts.map +1 -1
- package/dist/web/api/logout.js +3 -2
- package/dist/web/api/logout.js.map +1 -1
- package/dist/web/axiosInstance.d.ts.map +1 -1
- package/dist/web/axiosInstance.js +15 -3
- package/dist/web/axiosInstance.js.map +1 -1
- package/package.json +2 -1
- package/dist/server/middlewares/isLoggedMiddleware.d.ts +0 -15
- package/dist/server/middlewares/isLoggedMiddleware.d.ts.map +0 -1
- package/dist/server/middlewares/isLoggedMiddleware.js +0 -35
- package/dist/server/middlewares/isLoggedMiddleware.js.map +0 -1
- package/dist/server/middlewares/isVerifiedMiddleware.d.ts +0 -16
- package/dist/server/middlewares/isVerifiedMiddleware.d.ts.map +0 -1
- package/dist/server/middlewares/isVerifiedMiddleware.js +0 -44
- package/dist/server/middlewares/isVerifiedMiddleware.js.map +0 -1
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
# Sumor - OAuth 认证框架
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/sumor)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
Sumor 是一个面向 Express.js 应用的完整 OAuth 2.0 认证框架,内置基于角色的访问控制(RBAC)。它将 OAuth 集成、令牌管理和权限路由保护简化为开箱即用的中间件与工具函数,适用于多服务架构。
|
|
7
|
+
|
|
8
|
+
[English Documentation](README.md)
|
|
9
|
+
|
|
10
|
+
## 核心特性
|
|
11
|
+
|
|
12
|
+
- **完整 OAuth 2.0 流程**:完整的授权码流程,自动令牌交换
|
|
13
|
+
- **会话与令牌管理**:基于 HTTP-only Cookie 的安全令牌刷新机制
|
|
14
|
+
- **JWT 验证**:通过 JWKS(JSON Web Key Set)进行内置 JWT 签名校验
|
|
15
|
+
- **基于角色的访问控制(RBAC)**:应用启动时同步权限定义,运行时进行权限/角色校验
|
|
16
|
+
- **TypeScript 优先**:完整的 TypeScript 支持与类型定义
|
|
17
|
+
- **Express 集成**:即插即用的中间件与预配置路由
|
|
18
|
+
- **Mock 模式**:无需真实 OAuth 服务即可在本地进行开发调试
|
|
19
|
+
- **Web 客户端 SDK**:浏览器端令牌刷新、用户状态管理与权限工具函数
|
|
20
|
+
|
|
21
|
+
## 安装
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install sumor
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 架构概览
|
|
28
|
+
|
|
29
|
+
Sumor 分为两个入口:
|
|
30
|
+
|
|
31
|
+
| 入口 | 用途 |
|
|
32
|
+
| ----------- | --------------------------- |
|
|
33
|
+
| `sumor` | 服务端(Node.js / Express) |
|
|
34
|
+
| `sumor/web` | 客户端(浏览器) |
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
浏览器应用
|
|
38
|
+
│ (1) 应用初始化时调用 refreshToken()
|
|
39
|
+
│ (2) login() → 跳转至 OAuth 授权页
|
|
40
|
+
│ (3) OAuth 回调 → /api/oauth/callback
|
|
41
|
+
│ (4) 后续请求携带 HttpOnly Cookie
|
|
42
|
+
▼
|
|
43
|
+
你的 Express 应用
|
|
44
|
+
├── oauthRoutes ← /api/oauth/*(令牌刷新、回调、登出)
|
|
45
|
+
├── loadJwtUserMiddleware ← 校验 JWT,注入 req.jwtUser
|
|
46
|
+
└── 你的业务路由 ← 访问 req.jwtUser,调用 OAuthService 方法
|
|
47
|
+
▼
|
|
48
|
+
OAuth 服务提供商
|
|
49
|
+
└── 签发 JWT,管理用户与权限,提供 JWKS 公钥
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 服务端用法
|
|
55
|
+
|
|
56
|
+
### 引入
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { OAuthService, loadJwtUserMiddleware, oauthRoutes } from 'sumor'
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 导出说明
|
|
63
|
+
|
|
64
|
+
| 导出 | 类型 | 说明 |
|
|
65
|
+
| ----------------------- | -------------- | ----------------------------- |
|
|
66
|
+
| `OAuthService` | 类 | 与 OAuth 提供商 API 交互 |
|
|
67
|
+
| `loadJwtUserMiddleware` | 中间件 | 校验 JWT 并注入 `req.jwtUser` |
|
|
68
|
+
| `oauthRoutes` | Express Router | 预配置的 OAuth 路由 |
|
|
69
|
+
|
|
70
|
+
### 基础配置
|
|
71
|
+
|
|
72
|
+
在 Express 应用中注册 OAuth 路由和中间件:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import express from 'express'
|
|
76
|
+
import { OAuthService, loadJwtUserMiddleware, oauthRoutes } from 'sumor'
|
|
77
|
+
|
|
78
|
+
const app = express()
|
|
79
|
+
|
|
80
|
+
// 注册预配置的 OAuth 路由(回调、令牌刷新、登出)
|
|
81
|
+
app.use('/api/oauth', oauthRoutes)
|
|
82
|
+
|
|
83
|
+
// JWT 中间件:校验令牌并为后续路由注入 req.jwtUser
|
|
84
|
+
app.use(loadJwtUserMiddleware)
|
|
85
|
+
|
|
86
|
+
// 你的受保护路由
|
|
87
|
+
app.use('/api/user', userRoutes)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
> **注意:** 请在 `loadJwtUserMiddleware` 之前注册 `oauthRoutes`,确保 OAuth 回调端点(`/api/oauth/callback`)无需认证即可访问。
|
|
91
|
+
|
|
92
|
+
### 启动时同步权限
|
|
93
|
+
|
|
94
|
+
在应用启动时,将权限定义注册到 OAuth 提供商:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
const oauthService = new OAuthService()
|
|
98
|
+
|
|
99
|
+
await oauthService.updatePermissions({
|
|
100
|
+
permissions: ['posts:view', 'posts:create', 'posts:edit', 'posts:delete'],
|
|
101
|
+
permissionLabels: [{ module: 'posts', zh: '文章管理', en: 'Posts Management' }]
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
权限字符串格式为 `<模块>:<操作>`。
|
|
106
|
+
|
|
107
|
+
### 在路由中访问用户信息
|
|
108
|
+
|
|
109
|
+
`loadJwtUserMiddleware` 执行后,`req.jwtUser` 包含解码后的 JWT 载荷:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
app.get('/api/profile', (req, res) => {
|
|
113
|
+
const { userId, roles, permissions, isVerified } = req.jwtUser
|
|
114
|
+
|
|
115
|
+
res.json({
|
|
116
|
+
userId,
|
|
117
|
+
roles: roles?.split(',') ?? [],
|
|
118
|
+
permissions: permissions?.split(',') ?? [],
|
|
119
|
+
isVerified: isVerified === 1
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**`req.jwtUser` 属性说明:**
|
|
125
|
+
|
|
126
|
+
| 属性 | 类型 | 说明 |
|
|
127
|
+
| ------------- | -------- | ------------------------------ |
|
|
128
|
+
| `userId` | `string` | 用户唯一标识 |
|
|
129
|
+
| `roles` | `string` | 逗号分隔的角色 ID |
|
|
130
|
+
| `permissions` | `string` | 逗号分隔的权限字符串 |
|
|
131
|
+
| `isVerified` | `number` | `1` 表示已认证,`0` 表示未认证 |
|
|
132
|
+
| `tenantId` | `string` | 多租户标识 |
|
|
133
|
+
| `jti` | `string` | JWT ID(会话标识符) |
|
|
134
|
+
| `exp` | `number` | 令牌过期时间戳 |
|
|
135
|
+
| `iat` | `number` | 令牌签发时间戳 |
|
|
136
|
+
|
|
137
|
+
### OAuthService 方法
|
|
138
|
+
|
|
139
|
+
在服务端任意位置创建实例,自动读取环境变量配置:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
const oauthService = new OAuthService()
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### `updatePermissions(config)`
|
|
146
|
+
|
|
147
|
+
将应用的权限定义同步到 OAuth 提供商。
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
await oauthService.updatePermissions({
|
|
151
|
+
permissions: ['resource:view', 'resource:edit'],
|
|
152
|
+
permissionLabels: [{ module: 'resource', zh: '资源管理', en: 'Resource Management' }]
|
|
153
|
+
})
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### `getUserInfo(userId)`
|
|
157
|
+
|
|
158
|
+
获取单个用户的详细信息。
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
const userInfo = await oauthService.getUserInfo('user-123')
|
|
162
|
+
// 返回:{ userId, username, nickname, email, avatar, ... }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### `getUsersInfo(userIds)`
|
|
166
|
+
|
|
167
|
+
批量获取多个用户的信息。
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const users = await oauthService.getUsersInfo(['user-1', 'user-2', 'user-3'])
|
|
171
|
+
// 返回:[{ userId, username, ... }, ...]
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
#### `searchUsers(searchTerm, limit)`
|
|
175
|
+
|
|
176
|
+
按名称或邮箱搜索用户。
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const results = await oauthService.searchUsers('alice', 20)
|
|
180
|
+
// 返回:[{ userId, username, email, ... }, ...]
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### `revokeSession(sessionId)`
|
|
184
|
+
|
|
185
|
+
登出时撤销(黑名单化)会话。
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
await oauthService.revokeSession(req.jwtUser.jti)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### `checkBlacklist(sessionId)`
|
|
192
|
+
|
|
193
|
+
检查会话是否已被撤销。
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
const isRevoked = await oauthService.checkBlacklist(sessionId)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### 预配置 OAuth 路由
|
|
200
|
+
|
|
201
|
+
`oauthRoutes` 自动注册以下端点:
|
|
202
|
+
|
|
203
|
+
| 方法 | 路径 | 需要认证 | 说明 |
|
|
204
|
+
| ------ | --------------------- | -------- | --------------------------------------- |
|
|
205
|
+
| `GET` | `/api/oauth/callback` | 否 | 使用授权码换取令牌 |
|
|
206
|
+
| `PUT` | `/api/oauth/token` | 否 | 刷新访问令牌,返回用户信息和 OAuth 地址 |
|
|
207
|
+
| `POST` | `/api/oauth/logout` | 是 | 撤销会话并清除 Cookie |
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## 客户端用法(`sumor/web`)
|
|
212
|
+
|
|
213
|
+
### 引入
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import {
|
|
217
|
+
refreshToken,
|
|
218
|
+
login,
|
|
219
|
+
logout,
|
|
220
|
+
hasPermission,
|
|
221
|
+
hasRole,
|
|
222
|
+
oauthUrl,
|
|
223
|
+
oauthStore,
|
|
224
|
+
axios
|
|
225
|
+
} from 'sumor/web'
|
|
226
|
+
import type { ApiResponse } from 'sumor/web'
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### 导出说明
|
|
230
|
+
|
|
231
|
+
| 导出 | 类型 | 说明 |
|
|
232
|
+
| --------------- | ---------- | ------------------------------------------ |
|
|
233
|
+
| `refreshToken` | 函数 | 刷新令牌并同步用户状态(应用初始化时调用) |
|
|
234
|
+
| `login` | 函数 | 跳转到 OAuth 登录页 |
|
|
235
|
+
| `logout` | 函数 | 调用登出端点并清除用户状态 |
|
|
236
|
+
| `hasPermission` | 函数 | 检查当前用户是否有某个权限 |
|
|
237
|
+
| `hasRole` | 函数 | 检查当前用户是否有某个角色 |
|
|
238
|
+
| `oauthStore` | 单例 | 内存中的用户和 OAuth 状态存储 |
|
|
239
|
+
| `oauthUrl` | 对象 | 生成 OAuth 提供商导航地址的工具函数 |
|
|
240
|
+
| `axios` | Axios 实例 | 预配置 Axios,自动处理 401 令牌刷新重试 |
|
|
241
|
+
| `ApiResponse` | 类型 | 标准 API 响应封装类型 |
|
|
242
|
+
|
|
243
|
+
### 应用初始化
|
|
244
|
+
|
|
245
|
+
在应用初始化时调用一次 `refreshToken()`,从已存储的 refresh token Cookie 中恢复用户状态:
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
// main.ts 或 App.vue 的 onMounted
|
|
249
|
+
import { refreshToken } from 'sumor/web'
|
|
250
|
+
|
|
251
|
+
await refreshToken()
|
|
252
|
+
|
|
253
|
+
// 此后可通过 oauthStore 访问用户状态
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
> 在 SSR 环境中,`refreshToken()` 会直接返回,不执行任何操作。
|
|
257
|
+
|
|
258
|
+
### 登录与登出
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { login, logout } from 'sumor/web'
|
|
262
|
+
|
|
263
|
+
// 跳转到 OAuth 授权页
|
|
264
|
+
login()
|
|
265
|
+
|
|
266
|
+
// 登出:撤销会话并清除用户状态
|
|
267
|
+
await logout()
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### 订阅用户状态变化
|
|
271
|
+
|
|
272
|
+
通过 `oauthStore` 读取当前用户并监听状态变化:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { oauthStore } from 'sumor/web'
|
|
276
|
+
|
|
277
|
+
// 读取当前用户
|
|
278
|
+
const user = oauthStore.getUser()
|
|
279
|
+
// { id, isVerified, roles, permissions } 或 null
|
|
280
|
+
|
|
281
|
+
// 订阅登录/登出事件
|
|
282
|
+
oauthStore.onUserChange(user => {
|
|
283
|
+
if (user) {
|
|
284
|
+
console.log('已登录:', user.id)
|
|
285
|
+
} else {
|
|
286
|
+
console.log('已登出')
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**`UserInfo` 属性说明:**
|
|
292
|
+
|
|
293
|
+
| 属性 | 类型 | 说明 |
|
|
294
|
+
| ------------- | -------- | -------------------- |
|
|
295
|
+
| `id` | `string` | 用户 ID |
|
|
296
|
+
| `isVerified` | `number` | `1` 表示已认证 |
|
|
297
|
+
| `roles` | `string` | 逗号分隔的角色 ID |
|
|
298
|
+
| `permissions` | `string` | 逗号分隔的权限字符串 |
|
|
299
|
+
|
|
300
|
+
### 权限与角色检查
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import { hasPermission, hasRole } from 'sumor/web'
|
|
304
|
+
|
|
305
|
+
// 检查特定权限(模块 + 操作)
|
|
306
|
+
if (hasPermission('posts', 'edit')) {
|
|
307
|
+
// 用户可以编辑文章
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 检查模块下的任意权限(通配符)
|
|
311
|
+
if (hasPermission('posts', '*')) {
|
|
312
|
+
// 用户有 posts 模块的任意权限
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 检查角色
|
|
316
|
+
if (hasRole('admin')) {
|
|
317
|
+
// 用户是管理员
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### OAuth 地址工具函数
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
import { oauthUrl } from 'sumor/web'
|
|
325
|
+
|
|
326
|
+
oauthUrl.avatar(userId) // 用户头像图片地址
|
|
327
|
+
oauthUrl.user() // 用户个人中心地址
|
|
328
|
+
oauthUrl.home() // OAuth 提供商首页地址
|
|
329
|
+
oauthUrl.site() // 站点管理页面地址
|
|
330
|
+
oauthUrl.feedback() // 意见反馈页面地址
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
在 [Mock 模式](#mock-模式) 下,这些地址会指向本地占位页面。
|
|
334
|
+
|
|
335
|
+
### 发送带认证的 HTTP 请求
|
|
336
|
+
|
|
337
|
+
导出的 `axios` 实例会在 401 响应时自动刷新令牌并重试请求:
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
import { axios } from 'sumor/web'
|
|
341
|
+
import type { ApiResponse } from 'sumor/web'
|
|
342
|
+
|
|
343
|
+
// GET
|
|
344
|
+
const { data } = await axios.get<ApiResponse<UserInfo>>('/api/user/info')
|
|
345
|
+
|
|
346
|
+
// POST
|
|
347
|
+
const { data } = await axios.post<ApiResponse<Item>>('/api/items', { name: '我的项目' })
|
|
348
|
+
|
|
349
|
+
// PUT
|
|
350
|
+
const { data } = await axios.put<ApiResponse<Item>>(`/api/items/${id}`, updates)
|
|
351
|
+
|
|
352
|
+
// DELETE
|
|
353
|
+
const { data } = await axios.delete<ApiResponse<null>>(`/api/items/${id}`)
|
|
354
|
+
|
|
355
|
+
// 带进度的文件上传
|
|
356
|
+
await axios.post<ApiResponse<UploadResult>>('/api/upload', formData, {
|
|
357
|
+
headers: { 'Content-Type': 'multipart/form-data' },
|
|
358
|
+
onUploadProgress: e => {
|
|
359
|
+
const percent = Math.round((e.loaded * 100) / (e.total ?? e.loaded))
|
|
360
|
+
console.log(`上传进度:${percent}%`)
|
|
361
|
+
}
|
|
362
|
+
})
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**`ApiResponse<T>` 类型:**
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
interface ApiResponse<T> {
|
|
369
|
+
code: string // 成功时为 'OK'
|
|
370
|
+
message: string
|
|
371
|
+
data: T
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## 环境变量配置
|
|
378
|
+
|
|
379
|
+
Sumor 完全通过环境变量读取配置,无需向构造函数传参。
|
|
380
|
+
|
|
381
|
+
### 必填环境变量
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
OAUTH_ENDPOINT=https://auth.example.com
|
|
385
|
+
OAUTH_CLIENT_KEY=your-app-client-id
|
|
386
|
+
OAUTH_CLIENT_SECRET=your-app-client-secret
|
|
387
|
+
OAUTH_REDIRECT_URI=http://localhost:3000/api/oauth/callback
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
| 变量 | 说明 |
|
|
391
|
+
| --------------------- | ------------------------------------- |
|
|
392
|
+
| `OAUTH_ENDPOINT` | OAuth 提供商的根地址 |
|
|
393
|
+
| `OAUTH_CLIENT_KEY` | OAuth 应用客户端 ID |
|
|
394
|
+
| `OAUTH_CLIENT_SECRET` | OAuth 应用客户端密钥 |
|
|
395
|
+
| `OAUTH_REDIRECT_URI` | 回调地址(须与 OAuth 提供商配置一致) |
|
|
396
|
+
|
|
397
|
+
JWT 签名校验使用 OAuth 提供商的 JWKS 公钥(从 `{OAUTH_ENDPOINT}/api/oauth/jwks` 获取),无需本地密钥。
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Mock 模式
|
|
402
|
+
|
|
403
|
+
Mock 模式允许在没有真实 OAuth 服务的情况下进行本地开发。通过设置 `OAUTH_MOCK=true` 启用。此模式下,Sumor 使用 HS256 签发本地 JWT 令牌,并注册额外的 Mock 专用端点。
|
|
404
|
+
|
|
405
|
+
### 工作原理
|
|
406
|
+
|
|
407
|
+
当 `OAUTH_MOCK=true` 时:
|
|
408
|
+
|
|
409
|
+
- `oauthRoutes` 在 `/api/oauth/mock/` 下注册额外的子路由
|
|
410
|
+
- `PUT /api/oauth/token` 路由在本地校验 Mock 令牌,不调用 OAuth 提供商
|
|
411
|
+
- Web 客户端的 `login()` 直接调用 `POST /api/oauth/mock/login`,不跳转到 OAuth 提供商
|
|
412
|
+
- `logout()` 调用 `POST /api/oauth/mock/logout` 在本地清除 Cookie
|
|
413
|
+
- `oauthUrl.*` 工具函数返回本地占位页面地址
|
|
414
|
+
|
|
415
|
+
### Mock 服务端环境变量
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
# 启用 Mock 模式
|
|
419
|
+
OAUTH_MOCK=true
|
|
420
|
+
|
|
421
|
+
# Mock 用户配置(均为可选,括号内为默认值)
|
|
422
|
+
OAUTH_MOCK_USER_ID=mock-user-001
|
|
423
|
+
OAUTH_MOCK_USER_ROLES=admin
|
|
424
|
+
OAUTH_MOCK_USER_PERMISSIONS=
|
|
425
|
+
OAUTH_MOCK_USER_IS_VERIFIED=1
|
|
426
|
+
|
|
427
|
+
# Mock 令牌响应中返回的地址
|
|
428
|
+
OAUTH_MOCK_ENDPOINT=http://localhost
|
|
429
|
+
OAUTH_MOCK_REDIRECT_URI=http://localhost
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
| 变量 | 默认值 | 说明 |
|
|
433
|
+
| ----------------------------- | ------------------ | ---------------------------- |
|
|
434
|
+
| `OAUTH_MOCK` | `false` | 设为 `true` 启用 Mock 模式 |
|
|
435
|
+
| `OAUTH_MOCK_USER_ID` | `mock-user-001` | Mock 用户 ID |
|
|
436
|
+
| `OAUTH_MOCK_USER_ROLES` | `admin` | 逗号分隔的 Mock 角色 |
|
|
437
|
+
| `OAUTH_MOCK_USER_PERMISSIONS` | _(空)_ | 逗号分隔的 Mock 权限 |
|
|
438
|
+
| `OAUTH_MOCK_USER_IS_VERIFIED` | `1` | Mock 认证状态(`0` 或 `1`) |
|
|
439
|
+
| `OAUTH_MOCK_ENDPOINT` | `http://localhost` | 令牌响应中返回的 endpoint 值 |
|
|
440
|
+
| `OAUTH_MOCK_REDIRECT_URI` | `http://localhost` | Mock 登录后跳转的 origin |
|
|
441
|
+
|
|
442
|
+
### Mock 专用路由(仅在 `OAUTH_MOCK=true` 时注册)
|
|
443
|
+
|
|
444
|
+
| 方法 | 路径 | 说明 |
|
|
445
|
+
| ------ | ----------------------------- | -------------------------------------------- |
|
|
446
|
+
| `POST` | `/api/oauth/mock/login` | 签发 Mock 令牌并返回用户信息(无需页面跳转) |
|
|
447
|
+
| `POST` | `/api/oauth/mock/logout` | 清除 Mock 令牌 Cookie |
|
|
448
|
+
| `GET` | `/api/oauth/mock/avatar/:id` | 返回 404(Mock 模式无头像服务) |
|
|
449
|
+
| `GET` | `/api/oauth/mock/nav/:target` | OAuth 提供商导航链接的本地占位页面 |
|
|
450
|
+
|
|
451
|
+
### Mock 模式示例配置
|
|
452
|
+
|
|
453
|
+
**.env.development:**
|
|
454
|
+
|
|
455
|
+
```bash
|
|
456
|
+
OAUTH_MOCK=true
|
|
457
|
+
OAUTH_MOCK_USER_ID=dev-user-1
|
|
458
|
+
OAUTH_MOCK_USER_ROLES=admin
|
|
459
|
+
OAUTH_MOCK_USER_PERMISSIONS=posts:view,posts:edit,posts:delete
|
|
460
|
+
OAUTH_MOCK_USER_IS_VERIFIED=1
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
应用代码无需任何修改 —— 相同的 `oauthRoutes`、`loadJwtUserMiddleware`、`login()` 和 `logout()` 调用在 Mock 模式和生产模式下均正常工作。
|
|
464
|
+
|
|
465
|
+
### 安全提示
|
|
466
|
+
|
|
467
|
+
Mock 令牌使用固定的 HS256 密钥签名,可通过载荷中的 `iss: 'mock-oauth'` 字段识别。**请勿在生产环境中使用 `OAUTH_MOCK=true`。**
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## 使用示例
|
|
472
|
+
|
|
473
|
+
### 服务端 — 路由权限校验
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
app.post('/api/posts', (req, res) => {
|
|
477
|
+
const permissions = req.jwtUser.permissions?.split(',') ?? []
|
|
478
|
+
|
|
479
|
+
if (!permissions.includes('posts:create')) {
|
|
480
|
+
return res.status(403).json({ code: 'FORBIDDEN', message: '权限不足' })
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// 创建文章...
|
|
484
|
+
res.json({ code: 'OK', data: { id: 'new-post-id' } })
|
|
485
|
+
})
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### 服务端 — 从 OAuth 提供商获取用户信息
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
app.get('/api/posts/:id/author', async (req, res) => {
|
|
492
|
+
const post = await db.findPost(req.params.id)
|
|
493
|
+
const author = await oauthService.getUserInfo(post.authorId)
|
|
494
|
+
|
|
495
|
+
res.json({ code: 'OK', data: author })
|
|
496
|
+
})
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### 客户端 — Vue 组件登录/登出
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
import { login, logout, oauthStore, hasPermission } from 'sumor/web'
|
|
503
|
+
import { ref, onMounted } from 'vue'
|
|
504
|
+
|
|
505
|
+
const user = ref(oauthStore.getUser())
|
|
506
|
+
|
|
507
|
+
onMounted(() => {
|
|
508
|
+
oauthStore.onUserChange(u => {
|
|
509
|
+
user.value = u
|
|
510
|
+
})
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
const canEdit = () => hasPermission('posts', 'edit')
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
```html
|
|
517
|
+
<template>
|
|
518
|
+
<button v-if="!user" @click="login">登录</button>
|
|
519
|
+
<button v-else @click="logout">登出</button>
|
|
520
|
+
<button v-if="canEdit()" @click="editPost">编辑</button>
|
|
521
|
+
</template>
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### 客户端 — Vue Router 路由守卫
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
import { refreshToken, hasPermission } from 'sumor/web'
|
|
528
|
+
|
|
529
|
+
// 挂载路由前恢复用户状态
|
|
530
|
+
await refreshToken()
|
|
531
|
+
|
|
532
|
+
router.beforeEach(to => {
|
|
533
|
+
if (to.meta.requiresPermission) {
|
|
534
|
+
const [module, operation] = (to.meta.requiresPermission as string).split(':')
|
|
535
|
+
if (!hasPermission(module, operation)) {
|
|
536
|
+
return '/403'
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
})
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## 安全注意事项
|
|
545
|
+
|
|
546
|
+
- 令牌存储在 **HTTP-only Cookie** 中,JavaScript 无法访问(XSS 防护)
|
|
547
|
+
- 生产环境请务必使用 **HTTPS**
|
|
548
|
+
- JWT 签名通过 OAuth 提供商的 JWKS 公钥进行校验
|
|
549
|
+
- 登出操作会立即在 OAuth 提供商侧**将会话加入黑名单**
|
|
550
|
+
- 请勿在非本地开发环境中开启 `OAUTH_MOCK=true`
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## 常见问题排查
|
|
555
|
+
|
|
556
|
+
### `req.jwtUser` 为 `undefined`
|
|
557
|
+
|
|
558
|
+
请确保 `loadJwtUserMiddleware` 在访问 `req.jwtUser` 的路由之前注册。
|
|
559
|
+
|
|
560
|
+
### 令牌校验失败
|
|
561
|
+
|
|
562
|
+
请确认 `OAUTH_ENDPOINT` 指向正确的 OAuth 提供商地址。Sumor 从 `{OAUTH_ENDPOINT}/api/oauth/jwks` 获取 JWKS 公钥。
|
|
563
|
+
|
|
564
|
+
### `refreshToken()` 执行后用户未设置
|
|
565
|
+
|
|
566
|
+
可能是 refresh token Cookie 不存在或已过期,需要用户重新通过 `login()` 登录。
|
|
567
|
+
|
|
568
|
+
### Mock 模式下受保护路由返回 401
|
|
569
|
+
|
|
570
|
+
请确认 `OAUTH_MOCK=true` 在服务器启动时已正确设置。修改该变量后需重启服务。
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
## 许可证
|
|
575
|
+
|
|
576
|
+
MIT License — 详见 [LICENSE](LICENSE)。
|
package/dist/server/index.d.ts
CHANGED
|
@@ -5,15 +5,11 @@
|
|
|
5
5
|
import OAuthService from './services/oauthService';
|
|
6
6
|
import oauthRoutes from './routes';
|
|
7
7
|
import { loadJwtUserMiddleware } from './middlewares/loadJwtUserMiddleware';
|
|
8
|
-
import { isLoggedMiddleware } from './middlewares/isLoggedMiddleware';
|
|
9
|
-
import { isVerifiedMiddleware } from './middlewares/isVerifiedMiddleware';
|
|
10
8
|
/**
|
|
11
9
|
* 导出所需接口
|
|
12
10
|
* - OAuthService: OAuth 服务类,用于权限同步和各种 API 调用
|
|
13
11
|
* - loadJwtUserMiddleware: JWT 用户加载中间件
|
|
14
|
-
* - oauthRoutes: OAuth
|
|
15
|
-
* - isLoggedMiddleware: 检查用户是否已登录的中间件
|
|
16
|
-
* - isVerifiedMiddleware: 检查用户是否已通过认证的中间件
|
|
12
|
+
* - oauthRoutes: OAuth 路由(OAUTH_MOCK=true 时内部已包含 /mock/* 子路由)
|
|
17
13
|
*/
|
|
18
|
-
export { OAuthService, loadJwtUserMiddleware,
|
|
14
|
+
export { OAuthService, loadJwtUserMiddleware, oauthRoutes };
|
|
19
15
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../server/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,YAAY,MAAM,yBAAyB,CAAA;AAClD,OAAO,WAAW,MAAM,UAAU,CAAA;AAClC,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAA;AAC3E
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../server/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,YAAY,MAAM,yBAAyB,CAAA;AAClD,OAAO,WAAW,MAAM,UAAU,CAAA;AAClC,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAA;AAC3E;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,WAAW,EAAE,CAAA"}
|
package/dist/server/index.js
CHANGED
|
@@ -7,15 +7,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
7
7
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
8
|
};
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.oauthRoutes = exports.
|
|
10
|
+
exports.oauthRoutes = exports.loadJwtUserMiddleware = exports.OAuthService = void 0;
|
|
11
11
|
const oauthService_1 = __importDefault(require("./services/oauthService"));
|
|
12
12
|
exports.OAuthService = oauthService_1.default;
|
|
13
13
|
const routes_1 = __importDefault(require("./routes"));
|
|
14
14
|
exports.oauthRoutes = routes_1.default;
|
|
15
15
|
const loadJwtUserMiddleware_1 = require("./middlewares/loadJwtUserMiddleware");
|
|
16
16
|
Object.defineProperty(exports, "loadJwtUserMiddleware", { enumerable: true, get: function () { return loadJwtUserMiddleware_1.loadJwtUserMiddleware; } });
|
|
17
|
-
const isLoggedMiddleware_1 = require("./middlewares/isLoggedMiddleware");
|
|
18
|
-
Object.defineProperty(exports, "isLoggedMiddleware", { enumerable: true, get: function () { return isLoggedMiddleware_1.isLoggedMiddleware; } });
|
|
19
|
-
const isVerifiedMiddleware_1 = require("./middlewares/isVerifiedMiddleware");
|
|
20
|
-
Object.defineProperty(exports, "isVerifiedMiddleware", { enumerable: true, get: function () { return isVerifiedMiddleware_1.isVerifiedMiddleware; } });
|
|
21
17
|
//# sourceMappingURL=index.js.map
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;;;;AAEH,2EAAkD;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;;;;AAEH,2EAAkD;AASzC,uBATF,sBAAY,CASE;AARrB,sDAAkC;AAQY,sBARvC,gBAAW,CAQuC;AAPzD,+EAA2E;AAOpD,sGAPd,6CAAqB,OAOc"}
|
|
@@ -12,7 +12,6 @@ import { Response, NextFunction } from 'express';
|
|
|
12
12
|
* 注入的 req.jwtUser 包含以下字段:
|
|
13
13
|
* - userId: 用户 ID(来自 JWT 的 sub 字段)
|
|
14
14
|
* - jti: JWT 唯一标识
|
|
15
|
-
* - roleLevel: 用户角色级别
|
|
16
15
|
* - isVerified: 用户认证状态标记(0=未认证, 1=已认证)
|
|
17
16
|
* - roles: 用户角色列表(逗号分隔的字符串)
|
|
18
17
|
* - permissions: 用户权限列表(逗号分隔的2段式权限字符串)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loadJwtUserMiddleware.d.ts","sourceRoot":"","sources":["../../../server/middlewares/loadJwtUserMiddleware.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"loadJwtUserMiddleware.d.ts","sourceRoot":"","sources":["../../../server/middlewares/loadJwtUserMiddleware.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAMhD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,IAAI,CAAC,CAuCf;AAED,eAAe,qBAAqB,CAAA"}
|
|
@@ -8,6 +8,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
8
8
|
exports.loadJwtUserMiddleware = loadJwtUserMiddleware;
|
|
9
9
|
const tokenModel_1 = require("../models/tokenModel");
|
|
10
10
|
const authUtils_1 = require("../utils/authUtils");
|
|
11
|
+
const mockConfig_1 = require("../mock/mockConfig");
|
|
12
|
+
const mockTokenUtils_1 = require("../mock/mockTokenUtils");
|
|
11
13
|
/**
|
|
12
14
|
* JWT 用户加载中间件
|
|
13
15
|
* 验证 Access Token 并将用户信息注入到 req.jwtUser
|
|
@@ -16,7 +18,6 @@ const authUtils_1 = require("../utils/authUtils");
|
|
|
16
18
|
* 注入的 req.jwtUser 包含以下字段:
|
|
17
19
|
* - userId: 用户 ID(来自 JWT 的 sub 字段)
|
|
18
20
|
* - jti: JWT 唯一标识
|
|
19
|
-
* - roleLevel: 用户角色级别
|
|
20
21
|
* - isVerified: 用户认证状态标记(0=未认证, 1=已认证)
|
|
21
22
|
* - roles: 用户角色列表(逗号分隔的字符串)
|
|
22
23
|
* - permissions: 用户权限列表(逗号分隔的2段式权限字符串)
|
|
@@ -37,7 +38,8 @@ async function loadJwtUserMiddleware(req, res, next) {
|
|
|
37
38
|
}
|
|
38
39
|
// 2. 验证 Token
|
|
39
40
|
try {
|
|
40
|
-
|
|
41
|
+
// Mock 模式下使用 mock token 验证
|
|
42
|
+
const claims = (0, mockConfig_1.isMockMode)() && (0, mockTokenUtils_1.isMockToken)(token) ? (0, mockTokenUtils_1.verifyMockToken)(token) : await (0, tokenModel_1.verifyToken)(token);
|
|
41
43
|
// 检查 sub(用户ID)是否存在
|
|
42
44
|
if (!claims.sub) {
|
|
43
45
|
req.jwtUser = null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loadJwtUserMiddleware.js","sourceRoot":"","sources":["../../../server/middlewares/loadJwtUserMiddleware.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;
|
|
1
|
+
{"version":3,"file":"loadJwtUserMiddleware.js","sourceRoot":"","sources":["../../../server/middlewares/loadJwtUserMiddleware.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;AA0BH,sDA2CC;AAlED,qDAAkD;AAClD,kDAAmD;AACnD,mDAA+C;AAC/C,2DAAqE;AAErE;;;;;;;;;;;;;;;;;GAiBG;AACI,KAAK,UAAU,qBAAqB,CACzC,GAAQ,EACR,GAAa,EACb,IAAkB;IAElB,IAAI,CAAC;QACH,qBAAqB;QACrB,MAAM,KAAK,GAAG,IAAA,0BAAc,EAAC,GAAG,CAAC,CAAA;QAEjC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,GAAG,CAAC,OAAO,GAAG,IAAI,CAAA;YAClB,OAAO,IAAI,EAAE,CAAA;QACf,CAAC;QAED,cAAc;QACd,IAAI,CAAC;YACH,2BAA2B;YAC3B,MAAM,MAAM,GACV,IAAA,uBAAU,GAAE,IAAI,IAAA,4BAAW,EAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAA,gCAAe,EAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,IAAA,wBAAW,EAAC,KAAK,CAAC,CAAA;YAExF,mBAAmB;YACnB,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;gBAChB,GAAG,CAAC,OAAO,GAAG,IAAI,CAAA;gBAClB,OAAO,IAAI,EAAE,CAAA;YACf,CAAC;YAED,oBAAoB;YACpB,GAAG,CAAC,OAAO,GAAG;gBACZ,GAAG,MAAM;gBACT,MAAM,EAAE,MAAM,CAAC,GAAG,EAAE,sBAAsB;gBAC1C,GAAG,EAAE,MAAM,CAAC,GAAG;aAChB,CAAA;YAED,IAAI,EAAE,CAAA;QACR,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,wBAAwB;YACxB,GAAG,CAAC,OAAO,GAAG,IAAI,CAAA;YAClB,IAAI,EAAE,CAAA;QACR,CAAC;IACH,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QACpB,GAAG,CAAC,OAAO,GAAG,IAAI,CAAA;QAClB,IAAI,CAAC,KAAK,CAAC,CAAA;IACb,CAAC;AACH,CAAC;AAED,kBAAe,qBAAqB,CAAA"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock API 路由(挂载到 /api/oauth/mock)
|
|
3
|
+
*
|
|
4
|
+
* 提供给 web SDK 在 mock 模式下直接调用的接口,无需页面跳转。
|
|
5
|
+
*
|
|
6
|
+
* 路由列表:
|
|
7
|
+
* POST /api/oauth/mock/login - 签发 mock token,返回用户信息
|
|
8
|
+
* POST /api/oauth/mock/logout - 清除 mock token cookie
|
|
9
|
+
* GET /api/oauth/mock/avatar/:id - 返回 404(mock 模式无头像服务)
|
|
10
|
+
* GET /api/oauth/mock/nav/:target - 导航占位页(home/site/user/feedback)
|
|
11
|
+
*/
|
|
12
|
+
declare const mockApiRoutes: import("express-serve-static-core").Router;
|
|
13
|
+
export default mockApiRoutes;
|
|
14
|
+
//# sourceMappingURL=mockApiRoutes.d.ts.map
|