befly 3.9.38 → 3.9.40
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 +37 -38
- package/befly.config.ts +62 -40
- package/checks/checkApi.ts +16 -16
- package/checks/checkApp.ts +19 -25
- package/checks/checkTable.ts +42 -42
- package/docs/README.md +42 -35
- package/docs/{api.md → api/api.md} +223 -231
- package/docs/cipher.md +71 -69
- package/docs/database.md +143 -141
- package/docs/{examples.md → guide/examples.md} +181 -181
- package/docs/guide/quickstart.md +331 -0
- package/docs/hooks/auth.md +38 -0
- package/docs/hooks/cors.md +28 -0
- package/docs/{hook.md → hooks/hook.md} +140 -57
- package/docs/hooks/parser.md +19 -0
- package/docs/hooks/rateLimit.md +47 -0
- package/docs/{redis.md → infra/redis.md} +84 -93
- package/docs/plugins/cipher.md +61 -0
- package/docs/plugins/database.md +128 -0
- package/docs/{plugin.md → plugins/plugin.md} +83 -81
- package/docs/quickstart.md +26 -26
- package/docs/{addon.md → reference/addon.md} +46 -46
- package/docs/{config.md → reference/config.md} +32 -80
- package/docs/{logger.md → reference/logger.md} +52 -52
- package/docs/{sync.md → reference/sync.md} +32 -35
- package/docs/{table.md → reference/table.md} +1 -1
- package/docs/{validator.md → reference/validator.md} +57 -57
- package/hooks/auth.ts +8 -4
- package/hooks/cors.ts +13 -13
- package/hooks/parser.ts +37 -17
- package/hooks/permission.ts +26 -14
- package/hooks/rateLimit.ts +276 -0
- package/hooks/validator.ts +8 -8
- package/lib/asyncContext.ts +43 -0
- package/lib/cacheHelper.ts +212 -77
- package/lib/cacheKeys.ts +38 -0
- package/lib/cipher.ts +30 -30
- package/lib/connect.ts +28 -28
- package/lib/dbHelper.ts +183 -102
- package/lib/jwt.ts +16 -16
- package/lib/logger.ts +610 -19
- package/lib/redisHelper.ts +185 -44
- package/lib/sqlBuilder.ts +90 -91
- package/lib/validator.ts +59 -39
- package/loader/loadApis.ts +48 -44
- package/loader/loadHooks.ts +40 -14
- package/loader/loadPlugins.ts +16 -17
- package/main.ts +57 -47
- package/package.json +47 -45
- package/paths.ts +15 -14
- package/plugins/cache.ts +5 -4
- package/plugins/cipher.ts +3 -3
- package/plugins/config.ts +2 -2
- package/plugins/db.ts +9 -9
- package/plugins/jwt.ts +3 -3
- package/plugins/logger.ts +8 -12
- package/plugins/redis.ts +8 -8
- package/plugins/tool.ts +6 -6
- package/router/api.ts +85 -56
- package/router/static.ts +12 -12
- package/sync/syncAll.ts +12 -12
- package/sync/syncApi.ts +55 -52
- package/sync/syncDb/apply.ts +20 -19
- package/sync/syncDb/constants.ts +25 -23
- package/sync/syncDb/ddl.ts +35 -36
- package/sync/syncDb/helpers.ts +6 -9
- package/sync/syncDb/schema.ts +10 -9
- package/sync/syncDb/sqlite.ts +7 -8
- package/sync/syncDb/table.ts +37 -35
- package/sync/syncDb/tableCreate.ts +21 -20
- package/sync/syncDb/types.ts +23 -20
- package/sync/syncDb/version.ts +10 -10
- package/sync/syncDb.ts +43 -36
- package/sync/syncDev.ts +74 -65
- package/sync/syncMenu.ts +190 -55
- package/tests/api-integration-array-number.test.ts +282 -0
- package/tests/befly-config-env.test.ts +78 -0
- package/tests/cacheHelper.test.ts +135 -104
- package/tests/cacheKeys.test.ts +41 -0
- package/tests/cipher.test.ts +90 -89
- package/tests/dbHelper-advanced.test.ts +140 -134
- package/tests/dbHelper-all-array-types.test.ts +316 -0
- package/tests/dbHelper-array-serialization.test.ts +258 -0
- package/tests/dbHelper-columns.test.ts +56 -55
- package/tests/dbHelper-execute.test.ts +45 -44
- package/tests/dbHelper-joins.test.ts +124 -119
- package/tests/fields-redis-cache.test.ts +29 -27
- package/tests/fields-validate.test.ts +38 -38
- package/tests/getClientIp.test.ts +54 -0
- package/tests/integration.test.ts +69 -67
- package/tests/jwt.test.ts +27 -26
- package/tests/logger.test.ts +267 -34
- package/tests/rateLimit-hook.test.ts +477 -0
- package/tests/redisHelper.test.ts +187 -188
- package/tests/redisKeys.test.ts +6 -73
- package/tests/scanConfig.test.ts +144 -0
- package/tests/sqlBuilder-advanced.test.ts +217 -215
- package/tests/sqlBuilder.test.ts +92 -91
- package/tests/sync-connection.test.ts +29 -29
- package/tests/syncDb-apply.test.ts +97 -96
- package/tests/syncDb-array-number.test.ts +160 -0
- package/tests/syncDb-constants.test.ts +48 -47
- package/tests/syncDb-ddl.test.ts +99 -98
- package/tests/syncDb-helpers.test.ts +29 -28
- package/tests/syncDb-schema.test.ts +61 -60
- package/tests/syncDb-types.test.ts +60 -59
- package/tests/syncMenu-paths.test.ts +68 -0
- package/tests/util.test.ts +42 -41
- package/tests/validator-array-number.test.ts +310 -0
- package/tests/validator-default.test.ts +373 -0
- package/tests/validator.test.ts +271 -266
- package/tsconfig.json +4 -5
- package/types/api.d.ts +7 -12
- package/types/befly.d.ts +60 -13
- package/types/cache.d.ts +8 -4
- package/types/common.d.ts +17 -9
- package/types/context.d.ts +2 -2
- package/types/crypto.d.ts +23 -0
- package/types/database.d.ts +19 -19
- package/types/hook.d.ts +2 -2
- package/types/jwt.d.ts +118 -0
- package/types/logger.d.ts +30 -0
- package/types/plugin.d.ts +4 -4
- package/types/redis.d.ts +7 -3
- package/types/roleApisCache.ts +23 -0
- package/types/sync.d.ts +10 -10
- package/types/table.d.ts +50 -9
- package/types/validate.d.ts +69 -0
- package/utils/addonHelper.ts +90 -0
- package/utils/arrayKeysToCamel.ts +18 -0
- package/utils/calcPerfTime.ts +13 -0
- package/utils/configTypes.ts +3 -0
- package/utils/cors.ts +19 -0
- package/utils/fieldClear.ts +75 -0
- package/utils/genShortId.ts +12 -0
- package/utils/getClientIp.ts +45 -0
- package/utils/keysToCamel.ts +22 -0
- package/utils/keysToSnake.ts +22 -0
- package/utils/modules.ts +98 -0
- package/utils/pickFields.ts +19 -0
- package/utils/process.ts +56 -0
- package/utils/regex.ts +225 -0
- package/utils/response.ts +115 -0
- package/utils/route.ts +23 -0
- package/utils/scanConfig.ts +142 -0
- package/utils/scanFiles.ts +48 -0
- package/.prettierignore +0 -2
- package/.prettierrc +0 -12
- package/docs/1-/345/237/272/346/234/254/344/273/213/347/273/215.md +0 -35
- package/docs/2-/345/210/235/346/255/245/344/275/223/351/252/214.md +0 -64
- package/docs/3-/347/254/254/344/270/200/344/270/252/346/216/245/345/217/243.md +0 -46
- package/docs/4-/346/223/215/344/275/234/346/225/260/346/215/256/345/272/223.md +0 -172
- package/hooks/requestLogger.ts +0 -84
- package/types/index.ts +0 -24
- package/util.ts +0 -283
|
@@ -33,7 +33,7 @@ Validator 是 Befly 的参数验证系统,提供:
|
|
|
33
33
|
### 基本结构
|
|
34
34
|
|
|
35
35
|
```typescript
|
|
36
|
-
import { Validator } from
|
|
36
|
+
import { Validator } from "../lib/validator.js";
|
|
37
37
|
|
|
38
38
|
class Validator {
|
|
39
39
|
// 验证数据对象
|
|
@@ -50,18 +50,18 @@ class Validator {
|
|
|
50
50
|
|
|
51
51
|
```typescript
|
|
52
52
|
const data = {
|
|
53
|
-
email:
|
|
53
|
+
email: "test@example.com",
|
|
54
54
|
age: 25,
|
|
55
|
-
name:
|
|
55
|
+
name: "John"
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
const rules = {
|
|
59
|
-
email: { name:
|
|
60
|
-
age: { name:
|
|
61
|
-
name: { name:
|
|
59
|
+
email: { name: "邮箱", type: "string", min: 5, max: 100, regexp: "@email" },
|
|
60
|
+
age: { name: "年龄", type: "number", min: 0, max: 150 },
|
|
61
|
+
name: { name: "姓名", type: "string", min: 2, max: 50 }
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
-
const result = Validator.validate(data, rules, [
|
|
64
|
+
const result = Validator.validate(data, rules, ["email", "name"]);
|
|
65
65
|
|
|
66
66
|
if (result.failed) {
|
|
67
67
|
console.log(result.firstError); // 第一条错误信息
|
|
@@ -77,14 +77,14 @@ if (result.failed) {
|
|
|
77
77
|
|
|
78
78
|
```typescript
|
|
79
79
|
const fieldDef = {
|
|
80
|
-
name:
|
|
81
|
-
type:
|
|
80
|
+
name: "年龄",
|
|
81
|
+
type: "number",
|
|
82
82
|
min: 0,
|
|
83
83
|
max: 150,
|
|
84
84
|
default: 0
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
-
const result = Validator.single(
|
|
87
|
+
const result = Validator.single("25", fieldDef);
|
|
88
88
|
|
|
89
89
|
if (!result.error) {
|
|
90
90
|
console.log(result.value); // 25 (已转换为 number)
|
|
@@ -199,18 +199,18 @@ if (!result.error) {
|
|
|
199
199
|
```typescript
|
|
200
200
|
// 使用别名
|
|
201
201
|
{
|
|
202
|
-
regexp:
|
|
202
|
+
regexp: "@email";
|
|
203
203
|
} // 邮箱格式
|
|
204
204
|
{
|
|
205
|
-
regexp:
|
|
205
|
+
regexp: "@phone";
|
|
206
206
|
} // 手机号格式
|
|
207
207
|
{
|
|
208
|
-
regexp:
|
|
208
|
+
regexp: "@url";
|
|
209
209
|
} // URL 格式
|
|
210
210
|
|
|
211
211
|
// 直接使用正则
|
|
212
212
|
{
|
|
213
|
-
regexp:
|
|
213
|
+
regexp: "^[a-z]+$";
|
|
214
214
|
} // 小写字母
|
|
215
215
|
```
|
|
216
216
|
|
|
@@ -319,7 +319,7 @@ const hook: Hook = {
|
|
|
319
319
|
const result = Validator.validate(ctx.body, ctx.api.fields, ctx.api.required || []);
|
|
320
320
|
|
|
321
321
|
if (result.code !== 0) {
|
|
322
|
-
ctx.response = ErrorResponse(ctx, result.firstError ||
|
|
322
|
+
ctx.response = ErrorResponse(ctx, result.firstError || "参数验证失败", 1, null, result.fieldErrors);
|
|
323
323
|
}
|
|
324
324
|
}
|
|
325
325
|
};
|
|
@@ -342,17 +342,17 @@ const hook: Hook = {
|
|
|
342
342
|
```typescript
|
|
343
343
|
// apis/user/login.ts
|
|
344
344
|
export default {
|
|
345
|
-
name:
|
|
346
|
-
method:
|
|
345
|
+
name: "用户登录",
|
|
346
|
+
method: "POST",
|
|
347
347
|
auth: false,
|
|
348
348
|
fields: {
|
|
349
|
-
email: { name:
|
|
350
|
-
password: { name:
|
|
349
|
+
email: { name: "邮箱", type: "string", min: 5, max: 100, regexp: "@email" },
|
|
350
|
+
password: { name: "密码", type: "string", min: 6, max: 100 }
|
|
351
351
|
},
|
|
352
|
-
required: [
|
|
352
|
+
required: ["email", "password"],
|
|
353
353
|
handler: async (befly, ctx) => {
|
|
354
354
|
// ctx.body.email 和 ctx.body.password 已验证
|
|
355
|
-
return Yes(
|
|
355
|
+
return Yes("登录成功");
|
|
356
356
|
}
|
|
357
357
|
} as ApiRoute;
|
|
358
358
|
```
|
|
@@ -362,16 +362,16 @@ export default {
|
|
|
362
362
|
可以引用表定义中的字段:
|
|
363
363
|
|
|
364
364
|
```typescript
|
|
365
|
-
import { adminTable } from
|
|
365
|
+
import { adminTable } from "../../../tables/admin.js";
|
|
366
366
|
|
|
367
367
|
export default {
|
|
368
|
-
name:
|
|
368
|
+
name: "创建管理员",
|
|
369
369
|
fields: {
|
|
370
370
|
email: adminTable.email, // 引用表字段
|
|
371
371
|
password: adminTable.password,
|
|
372
372
|
nickname: adminTable.nickname
|
|
373
373
|
},
|
|
374
|
-
required: [
|
|
374
|
+
required: ["email", "password"],
|
|
375
375
|
handler: async (befly, ctx) => {
|
|
376
376
|
// ...
|
|
377
377
|
}
|
|
@@ -381,14 +381,14 @@ export default {
|
|
|
381
381
|
### 使用公共字段
|
|
382
382
|
|
|
383
383
|
```typescript
|
|
384
|
-
import { Fields } from
|
|
384
|
+
import { Fields } from "../../../config/fields.js";
|
|
385
385
|
|
|
386
386
|
export default {
|
|
387
|
-
name:
|
|
387
|
+
name: "获取列表",
|
|
388
388
|
fields: {
|
|
389
389
|
...Fields.page, // 分页字段
|
|
390
390
|
...Fields.limit,
|
|
391
|
-
keyword: { name:
|
|
391
|
+
keyword: { name: "关键词", type: "string", max: 50 }
|
|
392
392
|
},
|
|
393
393
|
handler: async (befly, ctx) => {
|
|
394
394
|
// ...
|
|
@@ -461,18 +461,18 @@ interface SingleResult {
|
|
|
461
461
|
|
|
462
462
|
```typescript
|
|
463
463
|
const data = {
|
|
464
|
-
username:
|
|
464
|
+
username: "john",
|
|
465
465
|
age: 25,
|
|
466
|
-
email:
|
|
466
|
+
email: "john@example.com"
|
|
467
467
|
};
|
|
468
468
|
|
|
469
469
|
const rules = {
|
|
470
|
-
username: { name:
|
|
471
|
-
age: { name:
|
|
472
|
-
email: { name:
|
|
470
|
+
username: { name: "用户名", type: "string", min: 2, max: 20 },
|
|
471
|
+
age: { name: "年龄", type: "number", min: 0, max: 150 },
|
|
472
|
+
email: { name: "邮箱", type: "string", regexp: "@email" }
|
|
473
473
|
};
|
|
474
474
|
|
|
475
|
-
const result = Validator.validate(data, rules, [
|
|
475
|
+
const result = Validator.validate(data, rules, ["username", "email"]);
|
|
476
476
|
// result.code === 0
|
|
477
477
|
```
|
|
478
478
|
|
|
@@ -480,13 +480,13 @@ const result = Validator.validate(data, rules, ['username', 'email']);
|
|
|
480
480
|
|
|
481
481
|
```typescript
|
|
482
482
|
const data = {
|
|
483
|
-
age:
|
|
484
|
-
score:
|
|
483
|
+
age: "25", // 字符串
|
|
484
|
+
score: "98.5" // 字符串
|
|
485
485
|
};
|
|
486
486
|
|
|
487
487
|
const rules = {
|
|
488
|
-
age: { name:
|
|
489
|
-
score: { name:
|
|
488
|
+
age: { name: "年龄", type: "number", min: 0 },
|
|
489
|
+
score: { name: "分数", type: "number", min: 0, max: 100 }
|
|
490
490
|
};
|
|
491
491
|
|
|
492
492
|
// 验证通过,'25' 会被转换为 25
|
|
@@ -497,31 +497,31 @@ const result = Validator.validate(data, rules);
|
|
|
497
497
|
|
|
498
498
|
```typescript
|
|
499
499
|
const data = {
|
|
500
|
-
tags: [
|
|
500
|
+
tags: ["vue", "react", "angular"],
|
|
501
501
|
ids: [1, 2, 3]
|
|
502
502
|
};
|
|
503
503
|
|
|
504
504
|
const rules = {
|
|
505
|
-
tags: { name:
|
|
506
|
-
ids: { name:
|
|
505
|
+
tags: { name: "标签", type: "array_string", min: 1, max: 10 },
|
|
506
|
+
ids: { name: "ID列表", type: "array_string", min: 1 }
|
|
507
507
|
};
|
|
508
508
|
|
|
509
|
-
const result = Validator.validate(data, rules, [
|
|
509
|
+
const result = Validator.validate(data, rules, ["tags"]);
|
|
510
510
|
```
|
|
511
511
|
|
|
512
512
|
### 示例 4:正则验证
|
|
513
513
|
|
|
514
514
|
```typescript
|
|
515
515
|
const data = {
|
|
516
|
-
phone:
|
|
517
|
-
email:
|
|
518
|
-
code:
|
|
516
|
+
phone: "13812345678",
|
|
517
|
+
email: "test@example.com",
|
|
518
|
+
code: "ABC123"
|
|
519
519
|
};
|
|
520
520
|
|
|
521
521
|
const rules = {
|
|
522
|
-
phone: { name:
|
|
523
|
-
email: { name:
|
|
524
|
-
code: { name:
|
|
522
|
+
phone: { name: "手机号", type: "string", regexp: "@phone" },
|
|
523
|
+
email: { name: "邮箱", type: "string", regexp: "@email" },
|
|
524
|
+
code: { name: "验证码", type: "string", regexp: "^[A-Z0-9]{6}$" }
|
|
525
525
|
};
|
|
526
526
|
|
|
527
527
|
const result = Validator.validate(data, rules);
|
|
@@ -531,9 +531,9 @@ const result = Validator.validate(data, rules);
|
|
|
531
531
|
|
|
532
532
|
```typescript
|
|
533
533
|
// 验证并转换单个值
|
|
534
|
-
const ageResult = Validator.single(
|
|
535
|
-
name:
|
|
536
|
-
type:
|
|
534
|
+
const ageResult = Validator.single("25", {
|
|
535
|
+
name: "年龄",
|
|
536
|
+
type: "number",
|
|
537
537
|
min: 0,
|
|
538
538
|
max: 150
|
|
539
539
|
});
|
|
@@ -543,9 +543,9 @@ if (!ageResult.error) {
|
|
|
543
543
|
}
|
|
544
544
|
|
|
545
545
|
// 空值使用默认值
|
|
546
|
-
const emptyResult = Validator.single(
|
|
547
|
-
name:
|
|
548
|
-
type:
|
|
546
|
+
const emptyResult = Validator.single("", {
|
|
547
|
+
name: "数量",
|
|
548
|
+
type: "number",
|
|
549
549
|
default: 10
|
|
550
550
|
});
|
|
551
551
|
console.log(emptyResult.value); // 10
|
|
@@ -573,7 +573,7 @@ A: 目前只支持扁平对象验证。嵌套对象需要在 handler 中手动
|
|
|
573
573
|
|
|
574
574
|
### Q: 正则别名可以扩展吗?
|
|
575
575
|
|
|
576
|
-
A: 正则别名定义在 `befly
|
|
576
|
+
A: 正则别名定义在 `befly/lib/regex` 中,可以直接使用自定义正则字符串,不需要扩展别名。
|
|
577
577
|
|
|
578
578
|
### Q: 类型转换失败会怎样?
|
|
579
579
|
|
|
@@ -605,14 +605,14 @@ handler: async (befly, ctx) => {
|
|
|
605
605
|
|
|
606
606
|
// cleanData 只包含有效值
|
|
607
607
|
await befly.db.updData({
|
|
608
|
-
table:
|
|
608
|
+
table: "user",
|
|
609
609
|
data: cleanData,
|
|
610
610
|
where: { id: ctx.user.userId }
|
|
611
611
|
});
|
|
612
612
|
|
|
613
|
-
return Yes(
|
|
613
|
+
return Yes("更新成功");
|
|
614
614
|
};
|
|
615
615
|
```
|
|
616
616
|
|
|
617
617
|
> **注意**:数据库操作(insData、updData 等)会自动过滤 null/undefined 值,通常不需要手动调用 cleanFields。
|
|
618
|
-
> 详见 [database.md](
|
|
618
|
+
> 详见 [database.md](../plugins/database.md#nullundefined-值自动过滤)。
|
package/hooks/auth.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import type { Hook } from
|
|
1
|
+
import type { Hook } from "../types/hook.js";
|
|
2
|
+
|
|
3
|
+
import { setCtxUser } from "../lib/asyncContext.js";
|
|
2
4
|
|
|
3
5
|
const hook: Hook = {
|
|
4
6
|
order: 3,
|
|
5
7
|
handler: async (befly, ctx) => {
|
|
6
|
-
const authHeader = ctx.req.headers.get(
|
|
8
|
+
const authHeader = ctx.req.headers.get("authorization");
|
|
7
9
|
|
|
8
|
-
if (authHeader && authHeader.startsWith(
|
|
10
|
+
if (authHeader && authHeader.startsWith("Bearer ")) {
|
|
9
11
|
const token = authHeader.substring(7);
|
|
10
12
|
|
|
11
13
|
try {
|
|
12
14
|
const payload = await befly.jwt.verify(token);
|
|
13
15
|
ctx.user = payload;
|
|
14
|
-
|
|
16
|
+
|
|
17
|
+
setCtxUser(payload.id, payload.roleCode, payload.nickname, payload.roleType);
|
|
18
|
+
} catch {
|
|
15
19
|
ctx.user = {};
|
|
16
20
|
}
|
|
17
21
|
} else {
|
package/hooks/cors.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
import { setCorsOptions } from '../util.js';
|
|
3
|
-
import { beflyConfig } from '../befly.config.js';
|
|
4
|
-
|
|
1
|
+
import type { CorsConfig } from "../types/befly.js";
|
|
5
2
|
// 类型导入
|
|
6
|
-
import type { Hook } from
|
|
7
|
-
|
|
3
|
+
import type { Hook } from "../types/hook.js";
|
|
4
|
+
|
|
5
|
+
import { beflyConfig } from "../befly.config.js";
|
|
6
|
+
// 相对导入
|
|
7
|
+
import { setCorsOptions } from "../utils/cors.js";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* CORS 跨域处理钩子
|
|
@@ -17,15 +17,15 @@ const hook: Hook = {
|
|
|
17
17
|
|
|
18
18
|
// 合并默认配置和用户配置
|
|
19
19
|
const defaultConfig: CorsConfig = {
|
|
20
|
-
origin:
|
|
21
|
-
methods:
|
|
22
|
-
allowedHeaders:
|
|
23
|
-
exposedHeaders:
|
|
20
|
+
origin: "*",
|
|
21
|
+
methods: "GET, POST, PUT, DELETE, OPTIONS",
|
|
22
|
+
allowedHeaders: "Content-Type, Authorization, authorization, token",
|
|
23
|
+
exposedHeaders: "Content-Range, X-Content-Range, Authorization, authorization, token",
|
|
24
24
|
maxAge: 86400,
|
|
25
|
-
credentials:
|
|
25
|
+
credentials: "true"
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
const corsConfig = { ...defaultConfig, ...
|
|
28
|
+
const corsConfig = { ...defaultConfig, ...beflyConfig.cors };
|
|
29
29
|
|
|
30
30
|
// 设置 CORS 响应头
|
|
31
31
|
const headers = setCorsOptions(req, corsConfig);
|
|
@@ -33,7 +33,7 @@ const hook: Hook = {
|
|
|
33
33
|
ctx.corsHeaders = headers;
|
|
34
34
|
|
|
35
35
|
// 处理 OPTIONS 预检请求
|
|
36
|
-
if (req.method ===
|
|
36
|
+
if (req.method === "OPTIONS") {
|
|
37
37
|
ctx.response = new Response(null, {
|
|
38
38
|
status: 204,
|
|
39
39
|
headers: headers
|
package/hooks/parser.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
+
// 类型导入
|
|
2
|
+
import type { Hook } from "../types/hook.js";
|
|
3
|
+
|
|
1
4
|
// 外部依赖
|
|
2
|
-
import { isPlainObject, isEmpty } from
|
|
3
|
-
import {
|
|
4
|
-
import { XMLParser } from 'fast-xml-parser';
|
|
5
|
+
import { isPlainObject, isEmpty } from "es-toolkit/compat";
|
|
6
|
+
import { XMLParser } from "fast-xml-parser";
|
|
5
7
|
|
|
6
8
|
// 相对导入
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
// 类型导入
|
|
10
|
-
import type { Hook } from '../types/hook.js';
|
|
9
|
+
import { pickFields } from "../utils/pickFields.js";
|
|
10
|
+
import { ErrorResponse } from "../utils/response.js";
|
|
11
11
|
|
|
12
12
|
const xmlParser = new XMLParser();
|
|
13
13
|
|
|
@@ -31,7 +31,7 @@ const hook: Hook = {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// GET 请求:解析查询参数
|
|
34
|
-
if (ctx.req.method ===
|
|
34
|
+
if (ctx.req.method === "GET") {
|
|
35
35
|
const url = new URL(ctx.req.url);
|
|
36
36
|
const params = Object.fromEntries(url.searchParams);
|
|
37
37
|
if (isPlainObject(ctx.api.fields) && !isEmpty(ctx.api.fields)) {
|
|
@@ -39,16 +39,16 @@ const hook: Hook = {
|
|
|
39
39
|
} else {
|
|
40
40
|
ctx.body = params;
|
|
41
41
|
}
|
|
42
|
-
} else if (ctx.req.method ===
|
|
42
|
+
} else if (ctx.req.method === "POST") {
|
|
43
43
|
// POST 请求:解析请求体
|
|
44
|
-
const contentType = ctx.req.headers.get(
|
|
44
|
+
const contentType = ctx.req.headers.get("content-type") || "";
|
|
45
45
|
// 获取 URL 查询参数(POST 请求也可能带参数)
|
|
46
46
|
const url = new URL(ctx.req.url);
|
|
47
47
|
const queryParams = Object.fromEntries(url.searchParams);
|
|
48
48
|
|
|
49
49
|
try {
|
|
50
50
|
// JSON 格式
|
|
51
|
-
if (contentType.includes(
|
|
51
|
+
if (contentType.includes("application/json")) {
|
|
52
52
|
const body = (await ctx.req.json()) as Record<string, any>;
|
|
53
53
|
// 合并 URL 参数和请求体(请求体优先)
|
|
54
54
|
const merged = { ...queryParams, ...body };
|
|
@@ -57,13 +57,13 @@ const hook: Hook = {
|
|
|
57
57
|
} else {
|
|
58
58
|
ctx.body = merged;
|
|
59
59
|
}
|
|
60
|
-
} else if (contentType.includes(
|
|
60
|
+
} else if (contentType.includes("application/xml") || contentType.includes("text/xml")) {
|
|
61
61
|
// XML 格式
|
|
62
62
|
const text = await ctx.req.text();
|
|
63
63
|
const parsed = xmlParser.parse(text) as Record<string, any>;
|
|
64
64
|
// 提取根节点内容(如 xml),使 body 扁平化
|
|
65
65
|
const rootKey = Object.keys(parsed)[0];
|
|
66
|
-
const body = rootKey && typeof parsed[rootKey] ===
|
|
66
|
+
const body = rootKey && typeof parsed[rootKey] === "object" ? parsed[rootKey] : parsed;
|
|
67
67
|
// 合并 URL 参数和请求体(请求体优先)
|
|
68
68
|
const merged = { ...queryParams, ...body };
|
|
69
69
|
if (isPlainObject(ctx.api.fields) && !isEmpty(ctx.api.fields)) {
|
|
@@ -73,12 +73,32 @@ const hook: Hook = {
|
|
|
73
73
|
}
|
|
74
74
|
} else {
|
|
75
75
|
// 不支持的 Content-Type
|
|
76
|
-
ctx.response = ErrorResponse(
|
|
76
|
+
ctx.response = ErrorResponse(
|
|
77
|
+
ctx,
|
|
78
|
+
"无效的请求参数格式",
|
|
79
|
+
1,
|
|
80
|
+
null,
|
|
81
|
+
{
|
|
82
|
+
location: "content-type",
|
|
83
|
+
value: contentType
|
|
84
|
+
},
|
|
85
|
+
"parser"
|
|
86
|
+
);
|
|
77
87
|
return;
|
|
78
88
|
}
|
|
79
|
-
} catch
|
|
80
|
-
//
|
|
81
|
-
ctx.response = ErrorResponse(
|
|
89
|
+
} catch {
|
|
90
|
+
// 解析失败:属于客户端输入错误,返回安全 detail(不回传异常栈/原始 body)
|
|
91
|
+
ctx.response = ErrorResponse(
|
|
92
|
+
ctx,
|
|
93
|
+
"无效的请求参数格式",
|
|
94
|
+
1,
|
|
95
|
+
null,
|
|
96
|
+
{
|
|
97
|
+
location: "body",
|
|
98
|
+
reason: contentType.includes("application/json") ? "invalid_json" : contentType.includes("xml") ? "invalid_xml" : "invalid_body"
|
|
99
|
+
},
|
|
100
|
+
"parser"
|
|
101
|
+
);
|
|
82
102
|
return;
|
|
83
103
|
}
|
|
84
104
|
}
|
package/hooks/permission.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// 相对导入
|
|
2
|
-
import { ErrorResponse } from '../util.js';
|
|
3
|
-
import { RedisKeys } from 'befly-shared/redisKeys';
|
|
4
|
-
|
|
5
1
|
// 类型导入
|
|
6
|
-
import type { Hook } from
|
|
2
|
+
import type { Hook } from "../types/hook.js";
|
|
3
|
+
|
|
4
|
+
import { CacheKeys } from "../lib/cacheKeys.js";
|
|
5
|
+
import { Logger } from "../lib/logger.js";
|
|
6
|
+
// 相对导入
|
|
7
|
+
import { ErrorResponse } from "../utils/response.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* 权限检查钩子
|
|
@@ -24,32 +25,43 @@ const hook: Hook = {
|
|
|
24
25
|
|
|
25
26
|
// 2. 用户未登录
|
|
26
27
|
if (!ctx.user || !ctx.user.id) {
|
|
27
|
-
ctx.response = ErrorResponse(ctx,
|
|
28
|
+
ctx.response = ErrorResponse(ctx, "未登录", 1, null, null, "auth");
|
|
28
29
|
return;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
// 3. 开发者权限(最高权限)
|
|
32
|
-
if (ctx.user.roleCode ===
|
|
33
|
+
if (ctx.user.roleCode === "dev") {
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
// 4. 角色权限检查
|
|
37
38
|
let hasPermission = false;
|
|
38
39
|
if (ctx.user.roleCode && befly.redis) {
|
|
40
|
+
// apiPath 在 apiHandler 中已统一生成并写入 ctx.route
|
|
41
|
+
const apiPath = ctx.route;
|
|
42
|
+
const roleCode = ctx.user.roleCode;
|
|
43
|
+
|
|
39
44
|
try {
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
const roleApisKey = RedisKeys.roleApis(ctx.user.roleCode);
|
|
45
|
+
// 极简方案:每个角色一个 Set,直接判断成员是否存在
|
|
46
|
+
const roleApisKey = CacheKeys.roleApis(roleCode);
|
|
43
47
|
hasPermission = await befly.redis.sismember(roleApisKey, apiPath);
|
|
44
|
-
} catch (
|
|
45
|
-
// Redis
|
|
46
|
-
|
|
48
|
+
} catch (err: any) {
|
|
49
|
+
// Redis 异常:记录到 error 日志文件(不回传给客户端),并降级为拒绝访问
|
|
50
|
+
Logger.error(
|
|
51
|
+
{
|
|
52
|
+
event: "hook_permission_redis_error",
|
|
53
|
+
apiPath: apiPath,
|
|
54
|
+
roleCode: roleCode,
|
|
55
|
+
err: err
|
|
56
|
+
},
|
|
57
|
+
"hook permission redis error"
|
|
58
|
+
);
|
|
47
59
|
hasPermission = false;
|
|
48
60
|
}
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
if (!hasPermission) {
|
|
52
|
-
ctx.response = ErrorResponse(ctx,
|
|
64
|
+
ctx.response = ErrorResponse(ctx, "无权访问", 1, null, null, "permission");
|
|
53
65
|
return;
|
|
54
66
|
}
|
|
55
67
|
}
|