befly 3.16.8 → 3.16.10
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/dist/befly.js +101 -12
- package/dist/befly.min.js +17 -17
- package/dist/checks/checkApi.js +18 -3
- package/dist/hooks/permission.d.ts +1 -0
- package/dist/hooks/permission.js +14 -0
- package/dist/lib/dbHelper.js +39 -8
- package/dist/sync/syncApi.js +26 -3
- package/dist/types/api.d.ts +8 -6
- package/dist/types/sync.d.ts +2 -2
- package/dist/utils/scanFiles.js +2 -2
- package/dist/utils/util.d.ts +6 -0
- package/dist/utils/util.js +21 -0
- package/package.json +2 -2
package/dist/checks/checkApi.js
CHANGED
|
@@ -84,9 +84,24 @@ export async function checkApi(apis) {
|
|
|
84
84
|
hasError = true;
|
|
85
85
|
}
|
|
86
86
|
const auth = record["auth"];
|
|
87
|
-
if (auth !== undefined
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
if (auth !== undefined) {
|
|
88
|
+
if (typeof auth === "boolean") {
|
|
89
|
+
// ok
|
|
90
|
+
}
|
|
91
|
+
else if (Array.isArray(auth)) {
|
|
92
|
+
if (auth.length === 0) {
|
|
93
|
+
Logger.warn(Object.assign({}, omit(record, ["handler"]), { msg: "接口的 auth 数组不能为空" }));
|
|
94
|
+
hasError = true;
|
|
95
|
+
}
|
|
96
|
+
else if (auth.some((item) => typeof item !== "string" || item.trim() === "")) {
|
|
97
|
+
Logger.warn(Object.assign({}, omit(record, ["handler"]), { msg: "接口的 auth 数组必须为非空字符串数组(roleType 列表)" }));
|
|
98
|
+
hasError = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
Logger.warn(Object.assign({}, omit(record, ["handler"]), { msg: "接口的 auth 属性必须是 boolean 或 string[]" }));
|
|
103
|
+
hasError = true;
|
|
104
|
+
}
|
|
90
105
|
}
|
|
91
106
|
const fields = record["fields"];
|
|
92
107
|
if (fields !== undefined && fields !== null && !isPlainObject(fields)) {
|
package/dist/hooks/permission.js
CHANGED
|
@@ -5,6 +5,7 @@ import { ErrorResponse } from "../utils/response";
|
|
|
5
5
|
/**
|
|
6
6
|
* 权限检查钩子
|
|
7
7
|
* - 接口无需权限(auth=false):直接通过
|
|
8
|
+
* - auth 为 roleType 白名单(string[]):登录后按 roleType 放行
|
|
8
9
|
* - 用户未登录:返回 401
|
|
9
10
|
* - 开发者角色(dev):最高权限,直接通过
|
|
10
11
|
* - 其他角色:检查 Redis 中的角色权限集合
|
|
@@ -20,6 +21,7 @@ const permissionHook = {
|
|
|
20
21
|
if (ctx.api.auth === false) {
|
|
21
22
|
return;
|
|
22
23
|
}
|
|
24
|
+
const authRule = ctx.api.auth;
|
|
23
25
|
// 2. 用户未登录
|
|
24
26
|
if (typeof ctx.user.id !== "number") {
|
|
25
27
|
ctx.response = ErrorResponse(ctx, "未登录", 1, null, null, "auth");
|
|
@@ -29,6 +31,18 @@ const permissionHook = {
|
|
|
29
31
|
if (ctx.user.roleCode === "dev") {
|
|
30
32
|
return;
|
|
31
33
|
}
|
|
34
|
+
// 3.5 auth 为角色类型白名单时,仅做 roleType 校验
|
|
35
|
+
if (Array.isArray(authRule)) {
|
|
36
|
+
const roleType = ctx.user.roleType;
|
|
37
|
+
if (typeof roleType !== "string" || !authRule.includes(roleType)) {
|
|
38
|
+
const apiNameLabel = typeof ctx.api.name === "string" && ctx.api.name.length > 0 ? ctx.api.name : null;
|
|
39
|
+
const apiPathLabel = typeof ctx.route === "string" && ctx.route.length > 0 ? ctx.route : null;
|
|
40
|
+
const apiLabel = apiNameLabel ? apiNameLabel : apiPathLabel ? apiPathLabel : "未知接口";
|
|
41
|
+
ctx.response = ErrorResponse(ctx, `无权访问 ${apiLabel} 接口`, 1, null, { apiLabel: apiLabel }, "permission");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
32
46
|
// 4. 角色权限检查
|
|
33
47
|
// apiPath 在 apiHandler 中已统一生成并写入 ctx.route
|
|
34
48
|
const apiPath = ctx.route;
|
package/dist/lib/dbHelper.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { CoreError } from "../types/coreError";
|
|
6
6
|
import { toNumberFromSql, toSqlParams } from "../utils/sqlUtil";
|
|
7
|
-
import { arrayKeysToCamel, isPlainObject, keysToCamel, snakeCase } from "../utils/util";
|
|
7
|
+
import { arrayKeysToCamel, canConvertToNumber, isPlainObject, keysToCamel, snakeCase } from "../utils/util";
|
|
8
8
|
import { DbUtils } from "./dbUtils";
|
|
9
9
|
import { Logger } from "./logger";
|
|
10
10
|
import { SqlBuilder } from "./sqlBuilder";
|
|
@@ -35,6 +35,20 @@ class DbSqlError extends Error {
|
|
|
35
35
|
this.sqlInfo = options.sqlInfo;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
class TransAbortError extends Error {
|
|
39
|
+
payload;
|
|
40
|
+
constructor(payload) {
|
|
41
|
+
super("TRANSACTION_ABORT");
|
|
42
|
+
this.payload = payload;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function isBeflyResponse(value) {
|
|
46
|
+
if (!isPlainObject(value)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const record = value;
|
|
50
|
+
return typeof record["code"] === "number" && typeof record["msg"] === "string";
|
|
51
|
+
}
|
|
38
52
|
/**
|
|
39
53
|
* 数据库助手类
|
|
40
54
|
*/
|
|
@@ -95,8 +109,9 @@ export class DbHelper {
|
|
|
95
109
|
}
|
|
96
110
|
}
|
|
97
111
|
if (bigintValue !== null) {
|
|
98
|
-
|
|
99
|
-
|
|
112
|
+
const convertedNumber = canConvertToNumber(bigintValue);
|
|
113
|
+
if (convertedNumber !== null) {
|
|
114
|
+
nextValue = convertedNumber;
|
|
100
115
|
}
|
|
101
116
|
}
|
|
102
117
|
}
|
|
@@ -892,7 +907,11 @@ export class DbHelper {
|
|
|
892
907
|
async trans(callback) {
|
|
893
908
|
if (this.isTransaction) {
|
|
894
909
|
// 已经在事务中,直接执行回调
|
|
895
|
-
|
|
910
|
+
const innerResult = await callback(this);
|
|
911
|
+
if (isBeflyResponse(innerResult) && innerResult.code !== 0) {
|
|
912
|
+
throw new TransAbortError(innerResult);
|
|
913
|
+
}
|
|
914
|
+
return innerResult;
|
|
896
915
|
}
|
|
897
916
|
const sql = this.sql;
|
|
898
917
|
if (!sql) {
|
|
@@ -903,10 +922,22 @@ export class DbHelper {
|
|
|
903
922
|
}
|
|
904
923
|
// 使用 Bun SQL 的 begin 方法开启事务
|
|
905
924
|
// begin 方法会自动处理 commit/rollback
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
925
|
+
try {
|
|
926
|
+
return await sql.begin(async (tx) => {
|
|
927
|
+
const trans = new DbHelper({ redis: this.redis, dbName: this.dbName, sql: tx, idMode: this.idMode });
|
|
928
|
+
const result = await callback(trans);
|
|
929
|
+
if (isBeflyResponse(result) && result.code !== 0) {
|
|
930
|
+
throw new TransAbortError(result);
|
|
931
|
+
}
|
|
932
|
+
return result;
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
catch (error) {
|
|
936
|
+
if (error instanceof TransAbortError) {
|
|
937
|
+
return error.payload;
|
|
938
|
+
}
|
|
939
|
+
throw error;
|
|
940
|
+
}
|
|
910
941
|
}
|
|
911
942
|
/**
|
|
912
943
|
* 执行原始 SQL
|
package/dist/sync/syncApi.js
CHANGED
|
@@ -16,6 +16,28 @@ const getApiParentPath = (apiPath) => {
|
|
|
16
16
|
return "";
|
|
17
17
|
return `/${parentSegments.join("/")}`;
|
|
18
18
|
};
|
|
19
|
+
const normalizeAuthForDb = (value) => {
|
|
20
|
+
if (value === false || value === 0 || value === "0" || value === "否") {
|
|
21
|
+
return "否";
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
const list = value
|
|
25
|
+
.filter((item) => typeof item === "string")
|
|
26
|
+
.map((item) => item.trim())
|
|
27
|
+
.filter((item) => item !== "");
|
|
28
|
+
if (list.length > 0) {
|
|
29
|
+
return list.join(",");
|
|
30
|
+
}
|
|
31
|
+
return "是";
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === "string") {
|
|
34
|
+
const trimmed = value.trim();
|
|
35
|
+
if (trimmed !== "") {
|
|
36
|
+
return trimmed;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return "是";
|
|
40
|
+
};
|
|
19
41
|
export async function syncApi(ctx, apis) {
|
|
20
42
|
const tableName = "addon_admin_api";
|
|
21
43
|
if (!ctx.db) {
|
|
@@ -57,15 +79,16 @@ export async function syncApi(ctx, apis) {
|
|
|
57
79
|
continue;
|
|
58
80
|
}
|
|
59
81
|
const addonName = typeof addonNameRaw === "string" ? addonNameRaw : "";
|
|
60
|
-
// auth
|
|
82
|
+
// auth:运行时支持 boolean/string[];DB 字段使用字符串(否/是/角色列表)。
|
|
61
83
|
// 统一在 syncApi 写库前做归一化,避免类型不一致导致每次启动都触发更新。
|
|
62
84
|
const authRaw = record["auth"];
|
|
63
|
-
const auth = authRaw
|
|
85
|
+
const auth = normalizeAuthForDb(authRaw);
|
|
64
86
|
const parentPath = getApiParentPath(path);
|
|
65
87
|
apiRouteKeys.add(path);
|
|
66
88
|
const item = allDbApiMap[path];
|
|
67
89
|
if (item) {
|
|
68
|
-
const
|
|
90
|
+
const existingAuth = normalizeAuthForDb(item.auth);
|
|
91
|
+
const shouldUpdate = name !== item.name || path !== item.path || addonName !== item.addonName || parentPath !== item.parentPath || auth !== existingAuth;
|
|
69
92
|
if (shouldUpdate) {
|
|
70
93
|
updData.push({
|
|
71
94
|
id: item.id,
|
package/dist/types/api.d.ts
CHANGED
|
@@ -22,11 +22,12 @@ export interface UserInfo {
|
|
|
22
22
|
[key: string]: JsonValue | undefined;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* 认证类型
|
|
26
26
|
* - false: 不需要认证
|
|
27
27
|
* - true: 需要认证(验证 token)
|
|
28
|
+
* - string[]: 需要认证,且仅允许 roleType 在数组中
|
|
28
29
|
*/
|
|
29
|
-
export type AuthType = boolean;
|
|
30
|
+
export type AuthType = boolean | string[];
|
|
30
31
|
/**
|
|
31
32
|
* API 处理器函数类型
|
|
32
33
|
*/
|
|
@@ -37,8 +38,8 @@ export type ApiHandler<TBody = Record<string, JsonValue>, R = JsonValue> = (befl
|
|
|
37
38
|
export interface ApiOptions<TBody = Record<string, JsonValue>, R = JsonValue> {
|
|
38
39
|
/** HTTP 方法 */
|
|
39
40
|
method: HttpMethod;
|
|
40
|
-
/**
|
|
41
|
-
auth?:
|
|
41
|
+
/** 认证规则(true/false/string[]) */
|
|
42
|
+
auth?: AuthType;
|
|
42
43
|
/** 字段定义(验证规则) */
|
|
43
44
|
fields?: TableDefinition;
|
|
44
45
|
/** 必填字段 */
|
|
@@ -59,8 +60,9 @@ export interface ApiRoute<TBody = Record<string, JsonValue>, R = JsonValue> {
|
|
|
59
60
|
/** 认证类型(可选,默认 true)
|
|
60
61
|
* - true: 需要登录
|
|
61
62
|
* - false: 公开访问(无需登录)
|
|
63
|
+
* - string[]: 需要登录,且 roleType 必须在数组中
|
|
62
64
|
*/
|
|
63
|
-
auth?:
|
|
65
|
+
auth?: AuthType;
|
|
64
66
|
/** 字段定义(验证规则)(可选,默认 {}) */
|
|
65
67
|
fields?: TableDefinition;
|
|
66
68
|
/** 必填字段(可选,默认 []) */
|
|
@@ -84,7 +86,7 @@ export interface ApiBuilder {
|
|
|
84
86
|
/** 设置描述 */
|
|
85
87
|
description(desc: string): this;
|
|
86
88
|
/** 设置认证 */
|
|
87
|
-
auth(auth:
|
|
89
|
+
auth(auth: AuthType): this;
|
|
88
90
|
/** 设置验证规则 */
|
|
89
91
|
rules(rules: KeyValue<string>): this;
|
|
90
92
|
/** 设置必填字段 */
|
package/dist/types/sync.d.ts
CHANGED
|
@@ -59,8 +59,8 @@ export interface MenuConfig {
|
|
|
59
59
|
export interface SyncApiItem {
|
|
60
60
|
type?: "api" | string;
|
|
61
61
|
name: string;
|
|
62
|
-
/**
|
|
63
|
-
auth?:
|
|
62
|
+
/** 是否需要登录:运行时支持 boolean/string[];同步写库为字符串(否/是/角色列表) */
|
|
63
|
+
auth?: boolean | string[] | string;
|
|
64
64
|
path: string;
|
|
65
65
|
parentPath: string;
|
|
66
66
|
addonName: string;
|
package/dist/utils/scanFiles.js
CHANGED
|
@@ -90,8 +90,8 @@ export async function scanFiles(dir, source, type, pattern) {
|
|
|
90
90
|
continue;
|
|
91
91
|
}
|
|
92
92
|
if (type === "api") {
|
|
93
|
-
//
|
|
94
|
-
// - checkApi 会校验 auth
|
|
93
|
+
// auth 默认 true:
|
|
94
|
+
// - checkApi 会校验 auth 类型(boolean | string[])
|
|
95
95
|
// - permission hook 以 ctx.api.auth === false 判断公开接口
|
|
96
96
|
// DB 存储的 0/1 由 syncApi 负责转换写入。
|
|
97
97
|
base["auth"] = true;
|
package/dist/utils/util.d.ts
CHANGED
|
@@ -31,6 +31,12 @@ export declare function isFiniteNumber(value: unknown): value is number;
|
|
|
31
31
|
* 判断值是否为有限整数。
|
|
32
32
|
*/
|
|
33
33
|
export declare function isIntegerNumber(value: unknown): value is number;
|
|
34
|
+
/**
|
|
35
|
+
* 判断 bigint 是否可安全转换为 number。
|
|
36
|
+
* - 超出安全整数范围则不可转换
|
|
37
|
+
* - 转换后若是科学计数法表示则不可转换
|
|
38
|
+
*/
|
|
39
|
+
export declare function canConvertToNumber(value: bigint): number | null;
|
|
34
40
|
/**
|
|
35
41
|
* 安全格式化值用于日志预览(截断长文本,避免抛错)。
|
|
36
42
|
*/
|
package/dist/utils/util.js
CHANGED
|
@@ -83,6 +83,27 @@ export function isFiniteNumber(value) {
|
|
|
83
83
|
export function isIntegerNumber(value) {
|
|
84
84
|
return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value);
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* 判断 bigint 是否可安全转换为 number。
|
|
88
|
+
* - 超出安全整数范围则不可转换
|
|
89
|
+
* - 转换后若是科学计数法表示则不可转换
|
|
90
|
+
*/
|
|
91
|
+
export function canConvertToNumber(value) {
|
|
92
|
+
const maxSafe = BigInt(Number.MAX_SAFE_INTEGER);
|
|
93
|
+
const minSafe = BigInt(Number.MIN_SAFE_INTEGER);
|
|
94
|
+
if (value > maxSafe || value < minSafe) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const asNumber = Number(value);
|
|
98
|
+
if (!Number.isSafeInteger(asNumber)) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const text = String(asNumber);
|
|
102
|
+
if (text.includes("e") || text.includes("E")) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return asNumber;
|
|
106
|
+
}
|
|
86
107
|
/**
|
|
87
108
|
* 安全格式化值用于日志预览(截断长文本,避免抛错)。
|
|
88
109
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly",
|
|
3
|
-
"version": "3.16.
|
|
4
|
-
"gitHead": "
|
|
3
|
+
"version": "3.16.10",
|
|
4
|
+
"gitHead": "3e9154027578c8eb3ba49141eef7c6ea3b385d7f",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
|
|
7
7
|
"keywords": [
|