befly 3.72.0 → 3.74.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/checks/api.js +50 -107
- package/checks/config.js +5 -133
- package/checks/hook.js +28 -31
- package/checks/menu.js +36 -50
- package/checks/plugin.js +28 -31
- package/checks/table.js +25 -109
- package/configs/beflyConfig.json +1 -3
- package/hooks/rateLimit.js +25 -186
- package/hooks/validator.js +2 -2
- package/index.js +48 -2
- package/lib/connect.js +0 -6
- package/lib/dbHelper.js +14 -40
- package/lib/validator.js +208 -304
- package/package.json +3 -3
- package/plugins/tool.js +16 -0
- package/router/api.js +4 -0
- package/schemas/config.js +67 -0
- package/utils/cors.js +2 -2
- package/utils/formatValidationIssues.js +28 -0
- package/utils/is.js +1 -1
- package/utils/formatZodIssues.js +0 -98
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { RUN_MODE_VALUES } from "../configs/constConfig.js";
|
|
2
|
+
|
|
3
|
+
const noTrimString = { type: "string", noTrim: true };
|
|
4
|
+
const boolInt = { type: "boolInt" };
|
|
5
|
+
const port = { type: "number", integer: true, min: 1, max: 65535 };
|
|
6
|
+
|
|
7
|
+
export const configSchema = {
|
|
8
|
+
root: {
|
|
9
|
+
runMode: { type: "values", values: RUN_MODE_VALUES },
|
|
10
|
+
appName: noTrimString,
|
|
11
|
+
appPort: port,
|
|
12
|
+
appHost: noTrimString,
|
|
13
|
+
apiHost: noTrimString,
|
|
14
|
+
devEmail: { type: "string", noTrim: true, check: "emailOrEmpty" },
|
|
15
|
+
devPassword: { type: "string", min: 6 },
|
|
16
|
+
bodyLimit: { type: "number", integer: true, min: 1 },
|
|
17
|
+
tz: { type: "string", check: "timeZone" },
|
|
18
|
+
excludeApisLog: { type: "stringArray", noTrim: true }
|
|
19
|
+
},
|
|
20
|
+
groups: {
|
|
21
|
+
upload: {
|
|
22
|
+
maxSize: { type: "number", integer: true, min: 1 },
|
|
23
|
+
publicDir: { ...noTrimString, min: 1 },
|
|
24
|
+
publicHost: noTrimString,
|
|
25
|
+
allowedExtensions: { ...noTrimString, pattern: /^\.[a-z0-9]+(?:,\.[a-z0-9]+)*$/ },
|
|
26
|
+
allowedMimeTypes: { ...noTrimString, pattern: /^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*(?:,[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*)*$/ },
|
|
27
|
+
forceDownloadExtensions: { ...noTrimString, pattern: /^(|\.[a-z0-9]+(?:,\.[a-z0-9]+)*)$/ }
|
|
28
|
+
},
|
|
29
|
+
logger: { debug: boolInt, excludeFields: { type: "stringArray", noTrim: true } },
|
|
30
|
+
mysql: {
|
|
31
|
+
hostname: noTrimString,
|
|
32
|
+
port: port,
|
|
33
|
+
username: noTrimString,
|
|
34
|
+
password: noTrimString,
|
|
35
|
+
database: noTrimString,
|
|
36
|
+
max: { type: "number", min: 1 },
|
|
37
|
+
beflyMode: { type: "values", values: ["manual", "auto"], optional: true },
|
|
38
|
+
idleTimeout: { type: "number", min: 1, optional: true },
|
|
39
|
+
maxLifetime: { type: "number", min: 0, optional: true },
|
|
40
|
+
connectionTimeout: { type: "number", min: 1, optional: true }
|
|
41
|
+
},
|
|
42
|
+
redis: {
|
|
43
|
+
hostname: noTrimString,
|
|
44
|
+
port: port,
|
|
45
|
+
username: noTrimString,
|
|
46
|
+
password: noTrimString,
|
|
47
|
+
db: { type: "number", integer: true, min: 0, max: 16 },
|
|
48
|
+
prefix: { ...noTrimString, check: "redisPrefix" }
|
|
49
|
+
},
|
|
50
|
+
session: { expireDays: { type: "number", integer: true, min: 1 } },
|
|
51
|
+
email: { host: noTrimString, port: port, secure: boolInt, user: noTrimString, pass: noTrimString, label: { ...noTrimString, optional: true } },
|
|
52
|
+
cors: {
|
|
53
|
+
methods: noTrimString,
|
|
54
|
+
allowedHeaders: noTrimString,
|
|
55
|
+
exposedHeaders: noTrimString,
|
|
56
|
+
maxAge: { type: "number", integer: true, min: 0 },
|
|
57
|
+
credentials: { type: "values", values: ["true", "false", true, false] }
|
|
58
|
+
},
|
|
59
|
+
rateLimit: {
|
|
60
|
+
enable: boolInt,
|
|
61
|
+
defaultLimit: { type: "number", integer: true, min: 1 },
|
|
62
|
+
defaultWindow: { type: "number", integer: true, min: 1 },
|
|
63
|
+
key: noTrimString,
|
|
64
|
+
skipRoutes: { type: "stringArray", noTrim: true }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
package/utils/cors.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @returns CORS 响应头对象
|
|
6
6
|
*/
|
|
7
7
|
export function setCorsOptions(req, config) {
|
|
8
|
-
const origin =
|
|
8
|
+
const origin = req.headers.get("origin");
|
|
9
9
|
const allowCredentials = config.credentials === true || config.credentials === "true" ? "true" : "false";
|
|
10
10
|
const requestedHeaders = req.headers.get("Access-Control-Request-Headers") || "";
|
|
11
11
|
|
|
@@ -42,7 +42,7 @@ export function setCorsOptions(req, config) {
|
|
|
42
42
|
const allowHeaders = allowedHeaderItems.join(", ");
|
|
43
43
|
|
|
44
44
|
return {
|
|
45
|
-
"Access-Control-Allow-Origin": origin
|
|
45
|
+
...(origin ? { "Access-Control-Allow-Origin": origin, Vary: "Origin" } : {}),
|
|
46
46
|
"Access-Control-Allow-Methods": config.methods,
|
|
47
47
|
"Access-Control-Allow-Headers": allowHeaders,
|
|
48
48
|
"Access-Control-Expose-Headers": config.exposedHeaders,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function pickFileLabel(item, itemLabel, index) {
|
|
2
|
+
if (item) {
|
|
3
|
+
if (item.filePath) return item.filePath;
|
|
4
|
+
if (item.relativePath) return item.relativePath;
|
|
5
|
+
if (item.fileName) return item.fileName;
|
|
6
|
+
if (item.name) return item.name;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return typeof index === "number" ? `${itemLabel}[${index}]` : itemLabel;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatValidationIssues(issues, options = {}) {
|
|
13
|
+
const items = Array.isArray(options.items) ? options.items : null;
|
|
14
|
+
const item = options.item || null;
|
|
15
|
+
const itemLabel = options.itemLabel || "item";
|
|
16
|
+
|
|
17
|
+
return issues.map((issue) => {
|
|
18
|
+
const path = Array.isArray(issue.path) ? issue.path : [];
|
|
19
|
+
const hasIndex = items && typeof path[0] === "number";
|
|
20
|
+
const index = hasIndex ? path[0] : null;
|
|
21
|
+
const fieldPath = (hasIndex ? path.slice(1) : path).join(".") || "(root)";
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
file: pickFileLabel(hasIndex ? items[index] : item, itemLabel, index),
|
|
25
|
+
expected: `字段 ${fieldPath} ${issue.message}`
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
}
|
package/utils/is.js
CHANGED
package/utils/formatZodIssues.js
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
function formatValue(value) {
|
|
2
|
-
if (value === undefined) return "undefined";
|
|
3
|
-
if (value === null) return "null";
|
|
4
|
-
if (typeof value === "string") return value;
|
|
5
|
-
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
6
|
-
if (typeof value === "function") return "[Function]";
|
|
7
|
-
if (Array.isArray(value)) return value.map((item) => formatValue(item)).join(", ");
|
|
8
|
-
|
|
9
|
-
try {
|
|
10
|
-
return JSON.stringify(value);
|
|
11
|
-
} catch {
|
|
12
|
-
return String(value);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function pickFileLabel(item, itemLabel, index) {
|
|
17
|
-
if (item) {
|
|
18
|
-
if (item.filePath) return item.filePath;
|
|
19
|
-
if (item.relativePath) return item.relativePath;
|
|
20
|
-
if (item.fileName) return item.fileName;
|
|
21
|
-
if (item.name) return item.name;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (typeof index === "number") return `${itemLabel}[${index}]`;
|
|
25
|
-
|
|
26
|
-
return itemLabel;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function buildExpectedMessage(issue, fieldPath) {
|
|
30
|
-
if (issue.code === "invalid_enum_value" && Array.isArray(issue.options)) {
|
|
31
|
-
return `字段 ${fieldPath} 仅允许 ${issue.options.join("|")}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (issue.code === "invalid_literal") {
|
|
35
|
-
return `字段 ${fieldPath} 仅允许 ${formatValue(issue.expected)}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (issue.code === "invalid_type") {
|
|
39
|
-
return `字段 ${fieldPath} 必须是 ${issue.expected}`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (issue.code === "too_small") {
|
|
43
|
-
if (typeof issue.minimum === "number") {
|
|
44
|
-
const sign = issue.inclusive ? ">=" : ">";
|
|
45
|
-
return `字段 ${fieldPath} ${sign}${issue.minimum}`;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (issue.code === "too_big") {
|
|
50
|
-
if (typeof issue.maximum === "number") {
|
|
51
|
-
const sign = issue.inclusive ? "<=" : "<";
|
|
52
|
-
return `字段 ${fieldPath} ${sign}${issue.maximum}`;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (issue.message) {
|
|
57
|
-
return `字段 ${fieldPath} ${issue.message}`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return `字段 ${fieldPath} 无效`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function formatZodIssues(issues, options = {}) {
|
|
64
|
-
const result = [];
|
|
65
|
-
const items = Array.isArray(options.items) ? options.items : null;
|
|
66
|
-
const item = options.item || null;
|
|
67
|
-
const itemLabel = options.itemLabel || "item";
|
|
68
|
-
|
|
69
|
-
for (const issue of issues) {
|
|
70
|
-
const issuePath = Array.isArray(issue.path) ? issue.path : [];
|
|
71
|
-
const hasIndex = items && typeof issuePath[0] === "number";
|
|
72
|
-
const index = hasIndex ? issuePath[0] : null;
|
|
73
|
-
const path = hasIndex ? issuePath.slice(1) : issuePath;
|
|
74
|
-
|
|
75
|
-
const targetItem = hasIndex ? items[index] : item;
|
|
76
|
-
const fileLabel = pickFileLabel(targetItem, itemLabel, index);
|
|
77
|
-
const fieldPath = path.length > 0 ? path.join(".") : "(root)";
|
|
78
|
-
|
|
79
|
-
const expectedMessage = buildExpectedMessage(issue, fieldPath);
|
|
80
|
-
|
|
81
|
-
if (issue.code === "unrecognized_keys" && Array.isArray(issue.keys)) {
|
|
82
|
-
for (const key of issue.keys) {
|
|
83
|
-
result.push({
|
|
84
|
-
file: fileLabel,
|
|
85
|
-
expected: `字段 ${key} 禁止出现`
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
result.push({
|
|
92
|
-
file: fileLabel,
|
|
93
|
-
expected: expectedMessage
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return result;
|
|
98
|
-
}
|