chanjs 2.7.4 → 2.7.5
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/USAGE.md +533 -0
- package/config/index.js +37 -6
- package/core/App.js +166 -0
- package/core/Container.js +77 -0
- package/core/Controller.js +29 -0
- package/core/Database.js +93 -0
- package/core/Repository.js +327 -0
- package/core/Service.js +11 -0
- package/core/bootstrap/error-handler.js +104 -0
- package/core/bootstrap/hook-runner.js +64 -0
- package/core/bootstrap/middleware.js +35 -0
- package/core/bootstrap/router-loader.js +53 -0
- package/core/errors.js +224 -0
- package/core/loader.js +89 -0
- package/core/registry.js +17 -0
- package/doc/Cache.md +279 -106
- package/doc/Common.md +590 -134
- package/doc/Controller.md +166 -95
- package/doc/Help.md +299 -698
- package/doc/QuickStart.md +116 -0
- package/doc/Repository.md +560 -0
- package/doc/Service.md +201 -527
- package/index.js +75 -37
- package/middleware/body.js +17 -0
- package/middleware/cookie.js +7 -15
- package/middleware/cors.js +9 -27
- package/middleware/favicon.js +7 -17
- package/middleware/header.js +15 -16
- package/middleware/index.js +11 -11
- package/middleware/log.js +26 -56
- package/middleware/static.js +15 -28
- package/middleware/template.js +75 -115
- package/middleware/validate.js +79 -0
- package/middleware/waf.js +174 -197
- package/package.json +9 -2
- package/response/code.js +73 -0
- package/response/index.js +9 -6
- package/response/response.js +82 -236
- package/security/checker.js +26 -74
- package/security/index.js +4 -9
- package/security/jwt.js +69 -142
- package/security/keywords.js +32 -136
- package/security/rate-limit.js +38 -80
- package/security/sign.js +83 -176
- package/security/xss-filter.js +21 -53
- package/storage/cache.js +57 -196
- package/storage/index.js +3 -6
- package/storage/redis.js +123 -181
- package/storage/store.js +163 -188
- package/utils/data-parse.js +42 -186
- package/utils/file.js +73 -244
- package/utils/filter.js +22 -25
- package/utils/html.js +49 -33
- package/utils/index.js +21 -7
- package/utils/ip.js +31 -71
- package/utils/logger.js +117 -0
- package/utils/pages.js +55 -0
- package/utils/paths.js +18 -0
- package/utils/request.js +94 -136
- package/utils/signal.js +87 -0
- package/utils/time.js +33 -75
- package/utils/tree.js +112 -104
- package/App.js +0 -533
- package/base/Aop.js +0 -195
- package/base/Container.js +0 -161
- package/base/Controller.js +0 -65
- package/base/Database.js +0 -133
- package/base/Event.js +0 -61
- package/base/Repository.js +0 -644
- package/common/api.js +0 -35
- package/common/code.js +0 -52
- package/common/email.js +0 -191
- package/common/index.js +0 -5
- package/common/pages.js +0 -120
- package/common/utils.js +0 -73
- package/config/code.js +0 -166
- package/config/paths.js +0 -60
- package/doc/Aop.md +0 -269
- package/doc/Email.md +0 -114
- package/doc/Event.md +0 -232
- package/global/env.js +0 -11
- package/global/import.js +0 -39
- package/global/index.js +0 -8
- package/helper/index.js +0 -79
- package/loader/index.js +0 -6
- package/loader/loader.js +0 -138
- package/middleware/compress.js +0 -185
- package/middleware/setBody.js +0 -32
- package/realtime/index.js +0 -7
- package/realtime/sse.js +0 -424
- package/realtime/websocket.js +0 -540
- package/schedule/index.js +0 -6
- package/schedule/schedule.js +0 -491
package/middleware/template.js
CHANGED
|
@@ -1,172 +1,132 @@
|
|
|
1
|
-
import
|
|
1
|
+
import artTemplate from "art-template";
|
|
2
2
|
import dayjs from "dayjs";
|
|
3
|
+
import { createRequire } from "module";
|
|
3
4
|
import relativeTime from "dayjs/plugin/relativeTime.js";
|
|
4
5
|
import "dayjs/locale/zh-cn.js";
|
|
5
|
-
import {
|
|
6
|
-
import { importjs } from "../global/import.js";
|
|
6
|
+
import { marked } from "marked";
|
|
7
7
|
import { filterXSS } from "../security/xss-filter.js";
|
|
8
|
+
import logger from "../utils/logger.js";
|
|
8
9
|
|
|
9
10
|
const require = createRequire(import.meta.url);
|
|
10
|
-
const { marked } = require('marked');
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
// art-template 过滤器注册(原 extend/art-template.js,合并至此)
|
|
14
|
-
// ============================================================
|
|
12
|
+
// 初始化日期工具
|
|
15
13
|
dayjs.extend(relativeTime);
|
|
16
|
-
dayjs.locale(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
template.defaults.native = false;
|
|
14
|
+
dayjs.locale("zh-cn");
|
|
15
|
+
// 关闭模板原生NodeJS语法,安全隔离
|
|
16
|
+
artTemplate.defaults.native = false;
|
|
20
17
|
|
|
21
18
|
/**
|
|
22
|
-
*
|
|
23
|
-
* @param {Date|string|number} date
|
|
24
|
-
* @param {string} format
|
|
25
|
-
* @returns {string}
|
|
19
|
+
* 日期格式化
|
|
20
|
+
* @param {Date|string|number} date
|
|
21
|
+
* @param {string} format
|
|
22
|
+
* @returns {string}
|
|
26
23
|
*/
|
|
27
|
-
|
|
24
|
+
artTemplate.defaults.imports.dateFormat = (date, format) => {
|
|
28
25
|
if (!date) return "";
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
} else {
|
|
32
|
-
return "";
|
|
33
|
-
}
|
|
34
|
-
return date.format(format);
|
|
26
|
+
const d = dayjs(["string", "number", "object"].includes(typeof date) ? date : null);
|
|
27
|
+
return d.isValid() ? d.format(format) : "";
|
|
35
28
|
};
|
|
36
29
|
|
|
37
30
|
/**
|
|
38
|
-
*
|
|
39
|
-
* @param {Date|string|number} date
|
|
40
|
-
* @returns {string}
|
|
31
|
+
* 相对时间(刚刚/几分钟前)
|
|
32
|
+
* @param {Date|string|number} date
|
|
33
|
+
* @returns {string}
|
|
41
34
|
*/
|
|
42
|
-
|
|
35
|
+
artTemplate.defaults.imports.timeAgo = date => {
|
|
43
36
|
if (!date) return "";
|
|
44
37
|
const d = dayjs(date);
|
|
45
|
-
|
|
46
|
-
return d.fromNow();
|
|
38
|
+
return d.isValid() ? d.fromNow() : "";
|
|
47
39
|
};
|
|
48
40
|
|
|
49
41
|
/**
|
|
50
|
-
*
|
|
51
|
-
* @param {string} str
|
|
52
|
-
* @param {number} length
|
|
53
|
-
* @returns {string}
|
|
42
|
+
* 字符串截断
|
|
43
|
+
* @param {string} str
|
|
44
|
+
* @param {number} [length=10]
|
|
45
|
+
* @returns {string}
|
|
54
46
|
*/
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
};
|
|
47
|
+
artTemplate.defaults.imports.truncate = (str, length = 10) =>
|
|
48
|
+
str?.length > length ? `${str.slice(0, length)}...` : str || "";
|
|
58
49
|
|
|
59
50
|
/**
|
|
60
|
-
*
|
|
61
|
-
* @param {
|
|
62
|
-
* @param {
|
|
63
|
-
* @returns {string}
|
|
64
|
-
* @description
|
|
65
|
-
* 安全改进:用 Object.prototype.hasOwnProperty.call 防止原型污染
|
|
51
|
+
* 安全序列化JSON,防原型污染
|
|
52
|
+
* @param {any} obj
|
|
53
|
+
* @param {string[]} [keys] 指定输出字段
|
|
54
|
+
* @returns {string}
|
|
66
55
|
*/
|
|
67
|
-
|
|
68
|
-
if (!obj) return
|
|
69
|
-
if (
|
|
70
|
-
const
|
|
71
|
-
keys.forEach(
|
|
72
|
-
|
|
73
|
-
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
74
|
-
filteredObj[key] = obj[key];
|
|
75
|
-
}
|
|
56
|
+
artTemplate.defaults.imports.safeStringify = (obj, keys) => {
|
|
57
|
+
if (!obj) return "null";
|
|
58
|
+
if (Array.isArray(keys) && keys.length) {
|
|
59
|
+
const filtered = {};
|
|
60
|
+
keys.forEach(k => {
|
|
61
|
+
if (Object.prototype.hasOwnProperty.call(obj, k)) filtered[k] = obj[k];
|
|
76
62
|
});
|
|
77
|
-
return JSON.stringify(
|
|
63
|
+
return JSON.stringify(filtered, null, 2);
|
|
78
64
|
}
|
|
79
65
|
return JSON.stringify(obj, null, 2);
|
|
80
66
|
};
|
|
81
67
|
|
|
82
68
|
/**
|
|
83
|
-
*
|
|
84
|
-
* @param {
|
|
85
|
-
* @param {string} separator
|
|
86
|
-
* @returns {string}
|
|
69
|
+
* 获取对象key列表
|
|
70
|
+
* @param {object} obj
|
|
71
|
+
* @param {string} [separator="\n"]
|
|
72
|
+
* @returns {string}
|
|
87
73
|
*/
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const sep = separator !== undefined ? separator : '\n';
|
|
91
|
-
return Object.keys(obj).join(sep);
|
|
92
|
-
};
|
|
74
|
+
artTemplate.defaults.imports.objKeys = (obj, separator = "\n") =>
|
|
75
|
+
typeof obj === "object" && obj ? Object.keys(obj).join(separator) : "";
|
|
93
76
|
|
|
94
77
|
/**
|
|
95
|
-
* Markdown
|
|
96
|
-
*
|
|
97
|
-
* @param {
|
|
98
|
-
* @param {
|
|
99
|
-
* @
|
|
100
|
-
* @returns {string} 渲染后的 HTML
|
|
101
|
-
* @description
|
|
102
|
-
* 安全改进:
|
|
103
|
-
* - 渲染后通过 filterXSS 过滤危险标签(script、onerror、onload 等)
|
|
104
|
-
* - 仅在 allowScript=1 时跳过过滤(受信任内容)
|
|
78
|
+
* Markdown渲染 + XSS过滤
|
|
79
|
+
* @param {string} content
|
|
80
|
+
* @param {"md"|"rich"} [editorType="rich"]
|
|
81
|
+
* @param {0|1} [allowScript=0] 1=信任内容不做XSS过滤
|
|
82
|
+
* @returns {string}
|
|
105
83
|
*/
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
84
|
+
artTemplate.defaults.imports.renderContent = (content, editorType = "rich", allowScript = 0) => {
|
|
85
|
+
if (typeof content !== "string") return content || "";
|
|
109
86
|
let html = content;
|
|
110
|
-
|
|
111
|
-
if (editorType ===
|
|
87
|
+
|
|
88
|
+
if (editorType === "md") {
|
|
112
89
|
try {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
html =
|
|
90
|
+
// marked v4+ 支持异步扩展,返回 string | Promise<string>
|
|
91
|
+
// 同步场景下直接取结果;若返回 Promise 则降级为空字符串
|
|
92
|
+
const result = marked.parse(content);
|
|
93
|
+
html = typeof result === "string" ? result : "";
|
|
94
|
+
} catch (e) {
|
|
95
|
+
logger.error(`[renderContent] MD解析失败: ${e.message}`);
|
|
117
96
|
}
|
|
118
97
|
}
|
|
119
|
-
|
|
98
|
+
|
|
120
99
|
if (Number(allowScript) !== 1) {
|
|
121
|
-
try {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
console.error('[renderContent] XSS 过滤失败:', err.message);
|
|
100
|
+
try { html = filterXSS(html); }
|
|
101
|
+
catch (e) {
|
|
102
|
+
logger.error(`[renderContent] XSS过滤失败: ${e.message}`);
|
|
125
103
|
}
|
|
126
104
|
}
|
|
127
105
|
return html;
|
|
128
106
|
};
|
|
129
107
|
|
|
130
|
-
// ============================================================
|
|
131
|
-
// 模板引擎中间件配置
|
|
132
|
-
// ============================================================
|
|
133
|
-
|
|
134
108
|
/**
|
|
135
|
-
*
|
|
136
|
-
* @param {
|
|
137
|
-
* @param {
|
|
138
|
-
* @param {Array<string>} config.views - 模板目录数组
|
|
139
|
-
* @param {string} config.NODE_ENV - 运行环境
|
|
140
|
-
* @description
|
|
141
|
-
* 为 Express 应用配置 art-template 模板引擎
|
|
142
|
-
* 支持多个模板目录,自动添加 web 模块视图目录
|
|
143
|
-
* @example
|
|
144
|
-
* setTemplate(app, {
|
|
145
|
-
* views: ['./views'],
|
|
146
|
-
* NODE_ENV: 'development'
|
|
147
|
-
* });
|
|
109
|
+
* art-template模板引擎中间件
|
|
110
|
+
* @param {express.Application} app
|
|
111
|
+
* @param {{views:string[],NODE_ENV:string}} config
|
|
148
112
|
*/
|
|
149
|
-
export
|
|
113
|
+
export const template = (app, config) => {
|
|
150
114
|
const { views, NODE_ENV } = config;
|
|
151
|
-
const isProduction =
|
|
152
|
-
|
|
153
|
-
|
|
115
|
+
const isProduction = ["production", "prd"].includes(NODE_ENV);
|
|
116
|
+
logger.info("模板缓存开启:", isProduction);
|
|
117
|
+
|
|
154
118
|
app.set("view options", {
|
|
155
119
|
debug: !isProduction,
|
|
156
120
|
cache: isProduction,
|
|
157
121
|
minimize: true,
|
|
158
122
|
});
|
|
159
123
|
app.set("view engine", "html");
|
|
160
|
-
app.set("views",
|
|
161
|
-
|
|
124
|
+
app.set("views", [...views]);
|
|
125
|
+
|
|
162
126
|
try {
|
|
163
|
-
app.engine(".html",
|
|
127
|
+
app.engine(".html", require("express-art-template"));
|
|
164
128
|
} catch (err) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// 兜底:直接返回模板路径,避免渲染崩溃
|
|
168
|
-
callback(null, `<pre>Template engine fallback: ${path}</pre>`);
|
|
169
|
-
};
|
|
170
|
-
app.engine(".html", engineFn);
|
|
129
|
+
logger.error(`[template] 模板引擎加载失败,启用降级渲染: ${err.message}`);
|
|
130
|
+
app.engine(".html", (path, _, cb) => cb(null, `<pre>模板降级: ${path}</pre>`));
|
|
171
131
|
}
|
|
172
|
-
};
|
|
132
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 声明式参数校验中间件(基于 zod)
|
|
3
|
+
* 在 Controller 上定义 static rules,框架自动校验 req.body/req.query/req.params
|
|
4
|
+
*/
|
|
5
|
+
import { ValidationError } from "../core/errors.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 写回校验结果:Express 5 的 req.query 为只读 getter,直接赋值会静默失败,
|
|
9
|
+
* 需用 defineProperty 定义自有属性遮蔽原型 getter。
|
|
10
|
+
* @private
|
|
11
|
+
*/
|
|
12
|
+
function setValidated(req, source, value) {
|
|
13
|
+
if (source === 'query') {
|
|
14
|
+
Object.defineProperty(req, 'query', {
|
|
15
|
+
value,
|
|
16
|
+
writable: true,
|
|
17
|
+
enumerable: true,
|
|
18
|
+
configurable: true,
|
|
19
|
+
});
|
|
20
|
+
} else {
|
|
21
|
+
req[source] = value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 创建 zod 校验中间件
|
|
27
|
+
* @param {import('zod').ZodSchema} schema - zod schema
|
|
28
|
+
* @param {"body"|"query"|"params"} [source='body'] - 校验来源
|
|
29
|
+
* @returns {Function} Express 中间件
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // 在 Controller 上定义 rules
|
|
33
|
+
* class ArticleController extends Controller {
|
|
34
|
+
* static rules = {
|
|
35
|
+
* create: z.object({ title: z.string().min(1), content: z.string() }),
|
|
36
|
+
* };
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* // router 中使用
|
|
40
|
+
* router.post('/create', validate(ArticleController.rules.create), ctrl.create.bind(ctrl));
|
|
41
|
+
*/
|
|
42
|
+
export function validate(schema, source = 'body') {
|
|
43
|
+
return (req, res, next) => {
|
|
44
|
+
const result = schema.safeParse(req[source]);
|
|
45
|
+
if (!result.success) {
|
|
46
|
+
const fields = result.error.issues?.map(i => i.path.join('.')) || [];
|
|
47
|
+
const msg = result.error.issues?.map(i => i.message).join('; ') || '参数校验失败';
|
|
48
|
+
const err = new ValidationError(msg, fields);
|
|
49
|
+
return next(err);
|
|
50
|
+
}
|
|
51
|
+
// 使用 setValidated 统一处理,解决 Express 5 req.query 只读问题
|
|
52
|
+
setValidated(req, source, result.data);
|
|
53
|
+
// 同时设置 req.validated 作为统一访问点(向后兼容)
|
|
54
|
+
req.validated = result.data;
|
|
55
|
+
next();
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 批量校验多个来源
|
|
61
|
+
* @param {Object} schemas - { body?, query?, params? } 各来源的 zod schema
|
|
62
|
+
* @returns {Function} Express 中间件
|
|
63
|
+
*/
|
|
64
|
+
export function validateAll(schemas) {
|
|
65
|
+
return (req, res, next) => {
|
|
66
|
+
for (const [source, schema] of Object.entries(schemas)) {
|
|
67
|
+
if (!schema) continue;
|
|
68
|
+
const result = schema.safeParse(req[source]);
|
|
69
|
+
if (!result.success) {
|
|
70
|
+
const fields = result.error.issues?.map(i => `${source}.${i.path.join('.')}`) || [];
|
|
71
|
+
const msg = result.error.issues?.map(i => i.message).join('; ') || '参数校验失败';
|
|
72
|
+
const err = new ValidationError(msg, fields);
|
|
73
|
+
return next(err);
|
|
74
|
+
}
|
|
75
|
+
setValidated(req, source, result.data);
|
|
76
|
+
}
|
|
77
|
+
next();
|
|
78
|
+
};
|
|
79
|
+
}
|