@linyjs/plugin-docs 0.1.2 → 0.1.4
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 +29 -5
- package/docs/api-reference/index.md +20 -0
- package/docs/best-practices/error-handling.md +1 -1
- package/docs/best-practices/naming-conventions.md +2 -1
- package/docs/best-practices/performance.md +4 -4
- package/docs/best-practices/service-definition.md +332 -0
- package/docs/faq.md +0 -1
- package/docs/getting-started/first-plugin.md +15 -15
- package/docs/getting-started/installation.md +1 -1
- package/docs/getting-started/quick-start.md +28 -22
- package/docs/guides/authentication.md +428 -0
- package/docs/guides/client-module.md +6 -0
- package/docs/guides/data-persistence.md +104 -43
- package/docs/guides/server-module.md +6 -0
- package/docs/migration/v0-to-v1.md +2 -2
- package/examples/analytics-plugin/src/client/index.tsx +43 -39
- package/examples/analytics-plugin/src/server/index.ts +20 -19
- package/examples/blog-plugin/README.md +7 -4
- package/examples/blog-plugin/package.json +1 -1
- package/examples/blog-plugin/src/client/index.tsx +44 -40
- package/examples/blog-plugin/src/server/index.ts +8 -8
- package/examples/hello-world/src/client/index.tsx +8 -8
- package/examples/hello-world/src/server/index.ts +8 -8
- package/package.json +10 -6
- package/src/cli.ts +12 -12
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# 用户认证
|
|
2
|
+
|
|
3
|
+
本指南介绍如何在 linyjs 插件中使用认证系统实现用户登录、注册和权限验证。
|
|
4
|
+
|
|
5
|
+
## 概述
|
|
6
|
+
|
|
7
|
+
linyjs 提供完整的用户认证系统,支持:
|
|
8
|
+
|
|
9
|
+
- 🔐 用户注册和登录
|
|
10
|
+
- 🪙 JWT Token 认证
|
|
11
|
+
- 🛡️ 路由级别的认证保护
|
|
12
|
+
- 👤 用户信息管理
|
|
13
|
+
|
|
14
|
+
## 配置认证服务
|
|
15
|
+
|
|
16
|
+
### 在服务端模块中注入依赖
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import type { IModule, IServiceDef } from '@linyjs/server-module-interface';
|
|
20
|
+
import { login, register, verifyToken } from '@linyjs/auth';
|
|
21
|
+
|
|
22
|
+
class AuthController {
|
|
23
|
+
// ... 控制器实现
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const authControllerDef: IServiceDef = {
|
|
27
|
+
serviceTag: 'authController',
|
|
28
|
+
serviceImpl: AuthController,
|
|
29
|
+
dependencies: [],
|
|
30
|
+
meta: { usageType: 'controller' }
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const myServerModule: IModule = {
|
|
34
|
+
name: 'myServer',
|
|
35
|
+
moduleDependencies: ['auth'],
|
|
36
|
+
serviceDefs: [authControllerDef]
|
|
37
|
+
};
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 基本认证操作
|
|
41
|
+
|
|
42
|
+
### 用户注册
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { register } from '@linyjs/auth';
|
|
46
|
+
|
|
47
|
+
class AuthController {
|
|
48
|
+
getRoutes() {
|
|
49
|
+
return [{
|
|
50
|
+
method: 'POST' as const,
|
|
51
|
+
path: '/api/auth/register',
|
|
52
|
+
middlewares: [],
|
|
53
|
+
responseType: 'json' as const,
|
|
54
|
+
handler: async (req: any) => {
|
|
55
|
+
const { username, email, password } = req.body;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const result = await register(username, email, password);
|
|
59
|
+
return {
|
|
60
|
+
success: true,
|
|
61
|
+
message: 'Registration successful',
|
|
62
|
+
data: result
|
|
63
|
+
};
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
message: error instanceof Error ? error.message : 'Registration failed'
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 用户登录
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { login } from '@linyjs/auth';
|
|
80
|
+
|
|
81
|
+
class AuthController {
|
|
82
|
+
getRoutes() {
|
|
83
|
+
return [{
|
|
84
|
+
method: 'POST' as const,
|
|
85
|
+
path: '/api/auth/login',
|
|
86
|
+
middlewares: [],
|
|
87
|
+
responseType: 'json' as const,
|
|
88
|
+
handler: async (req: any) => {
|
|
89
|
+
const { identifier, password } = req.body;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const result = await login(identifier, password);
|
|
93
|
+
return {
|
|
94
|
+
success: true,
|
|
95
|
+
message: 'Login successful',
|
|
96
|
+
data: result
|
|
97
|
+
};
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
message: error instanceof Error ? error.message : 'Login failed'
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Token 验证
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { verifyToken, getUserById } from '@linyjs/auth';
|
|
114
|
+
|
|
115
|
+
class AuthController {
|
|
116
|
+
getRoutes() {
|
|
117
|
+
return [{
|
|
118
|
+
method: 'GET' as const,
|
|
119
|
+
path: '/api/auth/me',
|
|
120
|
+
middlewares: [],
|
|
121
|
+
responseType: 'json' as const,
|
|
122
|
+
handler: async (req: any) => {
|
|
123
|
+
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
124
|
+
|
|
125
|
+
if (!token) {
|
|
126
|
+
return {
|
|
127
|
+
success: false,
|
|
128
|
+
message: 'No token provided'
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const payload = await verifyToken(token);
|
|
134
|
+
const user = await getUserById(payload.userId);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
success: true,
|
|
138
|
+
data: user
|
|
139
|
+
};
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return {
|
|
142
|
+
success: false,
|
|
143
|
+
message: 'Invalid token'
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 认证中间件
|
|
153
|
+
|
|
154
|
+
### 创建认证中间件
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { verifyToken, getUserById } from '@linyjs/auth';
|
|
158
|
+
|
|
159
|
+
async function authMiddleware(req: any, res: any, next: any) {
|
|
160
|
+
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
161
|
+
|
|
162
|
+
if (!token) {
|
|
163
|
+
return res.status(401).json({
|
|
164
|
+
success: false,
|
|
165
|
+
message: 'Authentication token required'
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const payload = await verifyToken(token);
|
|
171
|
+
const user = await getUserById(payload.userId);
|
|
172
|
+
|
|
173
|
+
if (!user) {
|
|
174
|
+
return res.status(401).json({
|
|
175
|
+
success: false,
|
|
176
|
+
message: 'User not found'
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 将用户信息添加到请求对象
|
|
181
|
+
req.user = user;
|
|
182
|
+
req.userId = user._id;
|
|
183
|
+
|
|
184
|
+
next();
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return res.status(401).json({
|
|
187
|
+
success: false,
|
|
188
|
+
message: 'Invalid authentication token'
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 使用认证中间件
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
class ProtectedController {
|
|
198
|
+
getRoutes() {
|
|
199
|
+
return [{
|
|
200
|
+
method: 'GET' as const,
|
|
201
|
+
path: '/api/protected',
|
|
202
|
+
middlewares: [authMiddleware], // 使用认证中间件
|
|
203
|
+
responseType: 'json' as const,
|
|
204
|
+
handler: async (req: any) => {
|
|
205
|
+
return {
|
|
206
|
+
success: true,
|
|
207
|
+
message: `Hello ${req.user.username}! You have access to this protected route.`
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## 客户端认证
|
|
216
|
+
|
|
217
|
+
### 登录表单示例
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import type { IModule } from '@linyjs/client-module-interface';
|
|
221
|
+
import React, { useState } from 'react';
|
|
222
|
+
|
|
223
|
+
function LoginForm() {
|
|
224
|
+
const [formData, setFormData] = useState({
|
|
225
|
+
identifier: '',
|
|
226
|
+
password: ''
|
|
227
|
+
});
|
|
228
|
+
const [error, setError] = useState('');
|
|
229
|
+
|
|
230
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
231
|
+
e.preventDefault();
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const response = await fetch('/api/auth/login', {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
headers: {
|
|
237
|
+
'Content-Type': 'application/json'
|
|
238
|
+
},
|
|
239
|
+
body: JSON.stringify(formData)
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const result = await response.json();
|
|
243
|
+
|
|
244
|
+
if (result.success) {
|
|
245
|
+
// 保存 token 到 localStorage 或 cookie
|
|
246
|
+
localStorage.setItem('token', result.data.token);
|
|
247
|
+
// 跳转或刷新页面
|
|
248
|
+
window.location.reload();
|
|
249
|
+
} else {
|
|
250
|
+
setError(result.message);
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
setError('Network error');
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<form onSubmit={handleSubmit}>
|
|
259
|
+
<div>
|
|
260
|
+
<label>用户名或邮箱:</label>
|
|
261
|
+
<input
|
|
262
|
+
type="text"
|
|
263
|
+
value={formData.identifier}
|
|
264
|
+
onChange={(e) => setFormData({ ...formData, identifier: e.target.value })}
|
|
265
|
+
required
|
|
266
|
+
/>
|
|
267
|
+
</div>
|
|
268
|
+
<div>
|
|
269
|
+
<label>密码:</label>
|
|
270
|
+
<input
|
|
271
|
+
type="password"
|
|
272
|
+
value={formData.password}
|
|
273
|
+
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
|
274
|
+
required
|
|
275
|
+
/>
|
|
276
|
+
</div>
|
|
277
|
+
{error && <div className="error">{error}</div>}
|
|
278
|
+
<button type="submit">登录</button>
|
|
279
|
+
</form>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
class LoginFormService {
|
|
284
|
+
getComponents() {
|
|
285
|
+
return {
|
|
286
|
+
LoginForm: {
|
|
287
|
+
component: LoginForm,
|
|
288
|
+
meta: { usageType: 'normal_component' }
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const loginFormDef: IServiceDef = {
|
|
295
|
+
serviceTag: 'loginForm',
|
|
296
|
+
serviceImpl: LoginFormService,
|
|
297
|
+
meta: { usageType: 'normal_service' }
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
export const authClientModule: IModule = {
|
|
301
|
+
name: 'authClient',
|
|
302
|
+
serviceDefs: [loginFormDef]
|
|
303
|
+
};
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## 认证状态管理
|
|
307
|
+
|
|
308
|
+
### 创建认证 Hook
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import { useState, useEffect } from 'react';
|
|
312
|
+
|
|
313
|
+
interface User {
|
|
314
|
+
_id: string;
|
|
315
|
+
username: string;
|
|
316
|
+
email: string;
|
|
317
|
+
createdAt: Date;
|
|
318
|
+
roles?: string[];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function useAuth() {
|
|
322
|
+
const [user, setUser] = useState<User | null>(null);
|
|
323
|
+
const [loading, setLoading] = useState(true);
|
|
324
|
+
|
|
325
|
+
useEffect(() => {
|
|
326
|
+
const token = localStorage.getItem('token');
|
|
327
|
+
|
|
328
|
+
if (!token) {
|
|
329
|
+
setLoading(false);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fetch('/api/auth/me', {
|
|
334
|
+
headers: {
|
|
335
|
+
'Authorization': `Bearer ${token}`
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
.then(response => response.json())
|
|
339
|
+
.then(result => {
|
|
340
|
+
if (result.success) {
|
|
341
|
+
setUser(result.data);
|
|
342
|
+
} else {
|
|
343
|
+
localStorage.removeItem('token');
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
.finally(() => {
|
|
347
|
+
setLoading(false);
|
|
348
|
+
});
|
|
349
|
+
}, []);
|
|
350
|
+
|
|
351
|
+
const login = async (identifier: string, password: string) => {
|
|
352
|
+
const response = await fetch('/api/auth/login', {
|
|
353
|
+
method: 'POST',
|
|
354
|
+
headers: {
|
|
355
|
+
'Content-Type': 'application/json'
|
|
356
|
+
},
|
|
357
|
+
body: JSON.stringify({ identifier, password })
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const result = await response.json();
|
|
361
|
+
|
|
362
|
+
if (result.success) {
|
|
363
|
+
localStorage.setItem('token', result.data.token);
|
|
364
|
+
setUser(result.data.user);
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return false;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const logout = () => {
|
|
372
|
+
localStorage.removeItem('token');
|
|
373
|
+
setUser(null);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const isAuthenticated = !!user;
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
user,
|
|
380
|
+
loading,
|
|
381
|
+
isAuthenticated,
|
|
382
|
+
login,
|
|
383
|
+
logout
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## 最佳实践
|
|
389
|
+
|
|
390
|
+
### 1. Token 存储
|
|
391
|
+
- 使用 `localStorage` 或 `cookie` 存储 token
|
|
392
|
+
- 考虑安全性时优先使用 `httpOnly` cookie
|
|
393
|
+
- 实现 token 自动刷新机制
|
|
394
|
+
|
|
395
|
+
### 2. 错误处理
|
|
396
|
+
- 捕获并处理认证相关的错误
|
|
397
|
+
- 提供用户友好的错误消息
|
|
398
|
+
- 记录详细的错误日志用于调试
|
|
399
|
+
|
|
400
|
+
### 3. 安全性
|
|
401
|
+
- 始终在服务端验证 token
|
|
402
|
+
- 不要在客户端存储敏感信息
|
|
403
|
+
- 使用强密码策略
|
|
404
|
+
- 考虑实现两步验证增强安全性
|
|
405
|
+
|
|
406
|
+
### 4. 用户体验
|
|
407
|
+
- 记住用户的登录状态
|
|
408
|
+
- 自动续期 token 避免频繁登录
|
|
409
|
+
- 在 token 过期时友好提示用户
|
|
410
|
+
|
|
411
|
+
## 故障排除
|
|
412
|
+
|
|
413
|
+
### 常见问题
|
|
414
|
+
|
|
415
|
+
1. **Token 无法验证**
|
|
416
|
+
- 检查 JWT_SECRET 是否正确配置
|
|
417
|
+
- 确认 token 未过期
|
|
418
|
+
- 验证 token 格式是否正确
|
|
419
|
+
|
|
420
|
+
2. **用户无法登录**
|
|
421
|
+
- 检查用户名/邮箱和密码是否正确
|
|
422
|
+
- 确认用户账户已激活
|
|
423
|
+
- 检查数据库连接是否正常
|
|
424
|
+
|
|
425
|
+
3. **中间件无法获取用户信息**
|
|
426
|
+
- 确认 token 在请求头中正确传递
|
|
427
|
+
- 检查 verifyToken 函数是否正常工作
|
|
428
|
+
- 验证 getUserById 是否能找到对应用户
|
|
@@ -545,6 +545,12 @@ function MyPage() {
|
|
|
545
545
|
|
|
546
546
|
## 最佳实践
|
|
547
547
|
|
|
548
|
+
### 0. 遵循 ServiceDef 代码风格规范
|
|
549
|
+
|
|
550
|
+
> 📐 **重要**: 请阅读 [ServiceDef 代码风格规范](../best-practices/service-definition.md),了解两条核心规则:
|
|
551
|
+
> 1. ServiceDef 必须声明为独立变量,禁止内联在 `serviceDefs` 数组中
|
|
552
|
+
> 2. ServiceDef 声明必须与实现类放在同一个文件中
|
|
553
|
+
|
|
548
554
|
### 1. 组件拆分
|
|
549
555
|
|
|
550
556
|
```
|