befly 3.8.20 → 3.8.24

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.
@@ -0,0 +1,92 @@
1
+ // 内部依赖
2
+ import { existsSync } from 'node:fs';
3
+
4
+ // 外部依赖
5
+ import { isPlainObject } from 'es-toolkit/compat';
6
+ import { scanAddons, getAddonDir, addonDirExists, scanFiles } from 'befly-util';
7
+
8
+ // 相对导入
9
+ import { Logger } from '../lib/logger.js';
10
+ import { projectApiDir } from '../paths.js';
11
+
12
+ /**
13
+ * 检查所有 API 定义
14
+ */
15
+ export async function checkApi(): Promise<void> {
16
+ try {
17
+ // 收集所有 API 文件
18
+ const allApiFiles: Array<{ file: string; displayName: string; apiPath: string }> = [];
19
+
20
+ // 收集项目 API 文件
21
+ if (existsSync(projectApiDir)) {
22
+ const files = await scanFiles(projectApiDir);
23
+ for (const { filePath, relativePath } of files) {
24
+ allApiFiles.push({
25
+ file: filePath,
26
+ displayName: '用户',
27
+ apiPath: relativePath
28
+ });
29
+ }
30
+ }
31
+
32
+ // 收集组件 API 文件
33
+ const addons = scanAddons();
34
+ for (const addon of addons) {
35
+ if (!addonDirExists(addon, 'apis')) continue;
36
+ const addonApiDir = getAddonDir(addon, 'apis');
37
+
38
+ const files = await scanFiles(addonApiDir);
39
+ for (const { filePath, relativePath } of files) {
40
+ allApiFiles.push({
41
+ file: filePath,
42
+ displayName: `组件${addon}`,
43
+ apiPath: relativePath
44
+ });
45
+ }
46
+ }
47
+
48
+ // 合并进行验证逻辑
49
+ for (const item of allApiFiles) {
50
+ const { apiPath } = item;
51
+
52
+ try {
53
+ // Windows 下路径需要转换为正斜杠格式
54
+ const filePath = item.file.replace(/\\/g, '/');
55
+ const apiImport = await import(filePath);
56
+ const api = apiImport.default;
57
+
58
+ // 验证必填属性:name 和 handler
59
+ if (typeof api.name !== 'string' || api.name.trim() === '') {
60
+ Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 name 属性必须是非空字符串`);
61
+ continue;
62
+ }
63
+ if (typeof api.handler !== 'function') {
64
+ Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 handler 属性必须是函数`);
65
+ continue;
66
+ }
67
+
68
+ // 验证可选属性的类型(如果提供了)
69
+ if (api.method && !['GET', 'POST'].includes(api.method.toUpperCase())) {
70
+ Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 method 属性必须是有效的 HTTP 方法`);
71
+ }
72
+ if (api.auth !== undefined && typeof api.auth !== 'boolean') {
73
+ Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 auth 属性必须是布尔值 (true=需登录, false=公开)`);
74
+ }
75
+ if (api.fields && !isPlainObject(api.fields)) {
76
+ Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 fields 属性必须是对象`);
77
+ }
78
+ if (api.required && !Array.isArray(api.required)) {
79
+ Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 required 属性必须是数组`);
80
+ }
81
+ if (api.required && api.required.some((item: any) => typeof item !== 'string')) {
82
+ Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 required 属性必须是字符串数组`);
83
+ }
84
+ } catch (error: any) {
85
+ Logger.error(`[${item.displayName}] 接口 ${apiPath} 解析失败`, error);
86
+ }
87
+ }
88
+ } catch (error: any) {
89
+ Logger.error('API 定义检查过程中出错', error);
90
+ throw error;
91
+ }
92
+ }
@@ -0,0 +1,31 @@
1
+ // 内部依赖
2
+ import { join } from 'node:path';
3
+ import { existsSync, mkdirSync } from 'node:fs';
4
+
5
+ // 相对导入
6
+ import { Logger } from '../lib/logger.js';
7
+ import { projectApiDir, projectDir } from '../paths.js';
8
+
9
+ /**
10
+ * 检查项目结构
11
+ */
12
+ export async function checkApp(): Promise<void> {
13
+ try {
14
+ // 检查项目 apis 目录下是否存在名为 addon 的目录
15
+ if (existsSync(projectApiDir)) {
16
+ const addonDir = join(projectApiDir, 'addon');
17
+ if (existsSync(addonDir)) {
18
+ throw new Error('项目 apis 目录下不能存在名为 addon 的目录,addon 是保留名称,用于组件接口路由');
19
+ }
20
+ }
21
+
22
+ // 检查并创建 logs 目录
23
+ const logsDir = join(projectDir, 'logs');
24
+ if (!existsSync(logsDir)) {
25
+ mkdirSync(logsDir, { recursive: true });
26
+ }
27
+ } catch (error: any) {
28
+ Logger.error('项目结构检查过程中出错', error);
29
+ throw error;
30
+ }
31
+ }
@@ -0,0 +1,247 @@
1
+ // 内部依赖
2
+ import { existsSync } from 'node:fs';
3
+
4
+ // 外部依赖
5
+ import { basename } from 'pathe';
6
+ import { scanAddons, getAddonDir, scanFiles } from 'befly-util';
7
+
8
+ // 相对导入
9
+ import { Logger } from '../lib/logger.js';
10
+ import { projectTableDir } from '../paths.js';
11
+
12
+ // 类型导入
13
+ import type { FieldDefinition } from '../types/common.js';
14
+
15
+ /**
16
+ * 表文件信息接口
17
+ */
18
+ interface TableFileInfo {
19
+ /** 表文件路径 */
20
+ file: string;
21
+ /** 文件类型:project(项目)或 addon(组件) */
22
+ type: 'project' | 'addon';
23
+ /** 如果是 addon 类型,记录 addon 名称 */
24
+ addonName?: string;
25
+ /** 类型名称(用于日志) */
26
+ typeName: string;
27
+ }
28
+
29
+ /**
30
+ * 保留字段列表
31
+ */
32
+ const RESERVED_FIELDS = ['id', 'created_at', 'updated_at', 'deleted_at', 'state'] as const;
33
+
34
+ /**
35
+ * 允许的字段类型
36
+ */
37
+ const FIELD_TYPES = ['string', 'number', 'text', 'array_string', 'array_text'] as const;
38
+
39
+ /**
40
+ * 小驼峰命名正则
41
+ * 可选:以下划线开头(用于特殊文件,如通用字段定义)
42
+ * 必须以小写字母开头,后续可包含小写/数字,或多个 [大写+小写/数字] 片段
43
+ * 示例:userTable、testCustomers、common
44
+ */
45
+ const LOWER_CAMEL_CASE_REGEX = /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
46
+
47
+ /**
48
+ * 字段名称正则
49
+ * 必须为中文、数字、字母、下划线、短横线、空格
50
+ */
51
+ const FIELD_NAME_REGEX = /^[\u4e00-\u9fa5a-zA-Z0-9 _-]+$/;
52
+
53
+ /**
54
+ * VARCHAR 最大长度限制
55
+ */
56
+ const MAX_VARCHAR_LENGTH = 65535;
57
+
58
+ /**
59
+ * 检查表定义文件
60
+ * @throws 当检查失败时抛出异常
61
+ */
62
+ export async function checkTable(): Promise<void> {
63
+ try {
64
+ // 收集所有表文件
65
+ const allTableFiles: TableFileInfo[] = [];
66
+ let hasError = false;
67
+
68
+ // 收集项目表字段定义文件(如果目录存在)
69
+ if (existsSync(projectTableDir)) {
70
+ const files = await scanFiles(projectTableDir, '*.json', false);
71
+ for (const { filePath } of files) {
72
+ allTableFiles.push({
73
+ file: filePath,
74
+ type: 'project',
75
+ typeName: '项目'
76
+ });
77
+ }
78
+ }
79
+
80
+ // 收集 addon 表字段定义文件
81
+ const addons = scanAddons();
82
+ for (const addonName of addons) {
83
+ const addonTablesDir = getAddonDir(addonName, 'tables');
84
+
85
+ // 检查 addon tables 目录是否存在
86
+ if (!existsSync(addonTablesDir)) {
87
+ continue;
88
+ }
89
+
90
+ const files = await scanFiles(addonTablesDir, '*.json', false);
91
+ for (const { filePath } of files) {
92
+ allTableFiles.push({
93
+ file: filePath,
94
+ type: 'addon',
95
+ typeName: `组件${addonName}`,
96
+ addonName: addonName
97
+ });
98
+ }
99
+ }
100
+
101
+ // 合并进行验证逻辑
102
+ for (const item of allTableFiles) {
103
+ const fileName = basename(item.file);
104
+ const fileBaseName = basename(item.file, '.json');
105
+
106
+ try {
107
+ // 1) 文件名小驼峰校验
108
+ if (!LOWER_CAMEL_CASE_REGEX.test(fileBaseName)) {
109
+ Logger.warn(`${item.typeName}表 ${fileName} 文件名必须使用小驼峰命名(例如 testCustomers.json)`);
110
+ hasError = true;
111
+ continue;
112
+ }
113
+
114
+ // 动态导入 JSON 文件
115
+ const tableModule = await import(item.file, { with: { type: 'json' } });
116
+ const table = tableModule.default;
117
+
118
+ // 检查 table 中的每个验证规则
119
+ for (const [colKey, fieldDef] of Object.entries(table)) {
120
+ if (typeof fieldDef !== 'object' || fieldDef === null || Array.isArray(fieldDef)) {
121
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 规则必须为对象`);
122
+ hasError = true;
123
+ continue;
124
+ }
125
+
126
+ // 检查是否使用了保留字段
127
+ if (RESERVED_FIELDS.includes(colKey as any)) {
128
+ Logger.warn(`${item.typeName}表 ${fileName} 文件包含保留字段 ${colKey},` + `不能在表定义中使用以下字段: ${RESERVED_FIELDS.join(', ')}`);
129
+ hasError = true;
130
+ }
131
+
132
+ // 直接使用字段对象
133
+ const field = fieldDef as FieldDefinition;
134
+
135
+ // 检查必填字段:name, type
136
+ if (!field.name || typeof field.name !== 'string') {
137
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 name 或类型错误`);
138
+ hasError = true;
139
+ continue;
140
+ }
141
+ if (!field.type || typeof field.type !== 'string') {
142
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 type 或类型错误`);
143
+ hasError = true;
144
+ continue;
145
+ }
146
+
147
+ // 检查可选字段的类型
148
+ if (field.min !== undefined && !(field.min === null || typeof field.min === 'number')) {
149
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 min 类型错误,必须为 null 或数字`);
150
+ hasError = true;
151
+ }
152
+ if (field.max !== undefined && !(field.max === null || typeof field.max === 'number')) {
153
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 max 类型错误,必须为 null 或数字`);
154
+ hasError = true;
155
+ }
156
+ if (field.detail !== undefined && typeof field.detail !== 'string') {
157
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 detail 类型错误,必须为字符串`);
158
+ hasError = true;
159
+ }
160
+ if (field.index !== undefined && typeof field.index !== 'boolean') {
161
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 index 类型错误,必须为布尔值`);
162
+ hasError = true;
163
+ }
164
+ if (field.unique !== undefined && typeof field.unique !== 'boolean') {
165
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 unique 类型错误,必须为布尔值`);
166
+ hasError = true;
167
+ }
168
+ if (field.nullable !== undefined && typeof field.nullable !== 'boolean') {
169
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 nullable 类型错误,必须为布尔值`);
170
+ hasError = true;
171
+ }
172
+ if (field.unsigned !== undefined && typeof field.unsigned !== 'boolean') {
173
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 unsigned 类型错误,必须为布尔值`);
174
+ hasError = true;
175
+ }
176
+ if (field.regexp !== undefined && field.regexp !== null && typeof field.regexp !== 'string') {
177
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 regexp 类型错误,必须为 null 或字符串`);
178
+ hasError = true;
179
+ }
180
+
181
+ const { name: fieldName, type: fieldType, min: fieldMin, max: fieldMax, default: fieldDefault } = field;
182
+
183
+ // 字段名称必须为中文、数字、字母、下划线、短横线、空格
184
+ if (!FIELD_NAME_REGEX.test(fieldName)) {
185
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
186
+ hasError = true;
187
+ }
188
+
189
+ // 字段类型必须为string,number,text,array_string,array_text之一
190
+ if (!FIELD_TYPES.includes(fieldType as any)) {
191
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,` + `必须为${FIELD_TYPES.join('、')}之一`);
192
+ hasError = true;
193
+ }
194
+
195
+ // 约束:当最小值与最大值均为数字时,要求最小值 <= 最大值
196
+ if (fieldMin !== undefined && fieldMax !== undefined && fieldMin !== null && fieldMax !== null) {
197
+ if (fieldMin > fieldMax) {
198
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
199
+ hasError = true;
200
+ }
201
+ }
202
+
203
+ // 类型联动校验 + 默认值规则
204
+ if (fieldType === 'text') {
205
+ // text:min/max 应该为 null,默认值必须为 null
206
+ if (fieldMin !== undefined && fieldMin !== null) {
207
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 的 text 类型最小值应为 null,当前为 "${fieldMin}"`);
208
+ hasError = true;
209
+ }
210
+ if (fieldMax !== undefined && fieldMax !== null) {
211
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 的 text 类型最大长度应为 null,当前为 "${fieldMax}"`);
212
+ hasError = true;
213
+ }
214
+ if (fieldDefault !== undefined && fieldDefault !== null) {
215
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 text 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
216
+ hasError = true;
217
+ }
218
+ } else if (fieldType === 'string' || fieldType === 'array_string') {
219
+ if (fieldMax !== undefined && (fieldMax === null || typeof fieldMax !== 'number')) {
220
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
221
+ hasError = true;
222
+ } else if (fieldMax !== undefined && fieldMax > MAX_VARCHAR_LENGTH) {
223
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
224
+ hasError = true;
225
+ }
226
+ } else if (fieldType === 'number') {
227
+ // number 类型:default 如果存在,必须为 null 或 number
228
+ if (fieldDefault !== undefined && fieldDefault !== null && typeof fieldDefault !== 'number') {
229
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 number 类型,` + `默认值必须为数字或 null,当前为 "${fieldDefault}"`);
230
+ hasError = true;
231
+ }
232
+ }
233
+ }
234
+ } catch (error: any) {
235
+ Logger.error(`${item.typeName}表 ${fileName} 解析失败`, error);
236
+ hasError = true;
237
+ }
238
+ }
239
+
240
+ if (hasError) {
241
+ throw new Error('表结构检查失败');
242
+ }
243
+ } catch (error: any) {
244
+ Logger.error('数据表定义检查过程中出错', error);
245
+ throw error;
246
+ }
247
+ }
package/config.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Befly 默认配置
3
+ * 包含所有配置项的默认值
4
+ */
5
+ import type { BeflyOptions } from './types/befly.js';
6
+
7
+ export const defaultOptions: Required<Omit<BeflyOptions, 'devPassword'>> = {
8
+ // ========== 核心参数 ==========
9
+ nodeEnv: 'development',
10
+ appName: 'Befly App',
11
+ appPort: 3000,
12
+ appHost: '127.0.0.1',
13
+ devEmail: 'dev@qq.com',
14
+ bodyLimit: 1048576, // 1MB
15
+ tz: 'Asia/Shanghai',
16
+ dbCache: 0,
17
+
18
+ // ========== 日志配置 ==========
19
+ logger: {
20
+ debug: 1,
21
+ excludeFields: 'password,token,secret',
22
+ dir: './logs',
23
+ console: 1,
24
+ maxSize: 10485760 // 10MB
25
+ },
26
+
27
+ // ========== 数据库配置 ==========
28
+ db: {
29
+ enable: 0,
30
+ type: 'sqlite',
31
+ host: '127.0.0.1',
32
+ port: 3306,
33
+ username: 'root',
34
+ password: '',
35
+ database: 'befly',
36
+ poolMax: 1
37
+ },
38
+
39
+ // ========== Redis 配置 ==========
40
+ redis: {
41
+ host: '127.0.0.1',
42
+ port: 6379,
43
+ username: '',
44
+ password: '',
45
+ db: 0,
46
+ prefix: 'befly:'
47
+ },
48
+
49
+ // ========== 认证配置 ==========
50
+ auth: {
51
+ secret: 'befly-secret',
52
+ expiresIn: '7d',
53
+ algorithm: 'HS256'
54
+ },
55
+
56
+ // ========== CORS 配置 ==========
57
+ cors: {
58
+ origin: '*',
59
+ methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
60
+ allowedHeaders: 'Content-Type,Authorization',
61
+ exposedHeaders: '',
62
+ maxAge: 86400,
63
+ credentials: 'true'
64
+ },
65
+
66
+ // ========== 禁用配置 ==========
67
+ /** 禁用的钩子列表 */
68
+ disableHooks: [],
69
+ /** 禁用的插件列表 */
70
+ disablePlugins: []
71
+ };
package/hooks/auth.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { Hook } from '../types/hook.js';
2
+ import { Jwt } from '../lib/jwt.js';
3
+
4
+ const hook: Hook = {
5
+ after: ['cors'],
6
+ order: 5,
7
+ handler: async (befly, ctx, next) => {
8
+ // 初始化配置(如果有)
9
+ // 注意:Hook 没有 onInit,如果需要初始化,可以在 handler 首次执行时做,或者保留 Plugin 机制专门做初始化
10
+ // 这里 auth 插件原本有 onInit 来配置 Jwt,现在需要迁移
11
+ // 临时方案:直接在 handler 里判断是否配置过,或者让 Jwt 自身处理配置
12
+
13
+ const authHeader = ctx.req.headers.get('authorization');
14
+
15
+ if (authHeader && authHeader.startsWith('Bearer ')) {
16
+ const token = authHeader.substring(7);
17
+
18
+ try {
19
+ const payload = await Jwt.verify(token);
20
+ ctx.user = payload;
21
+ } catch (error: any) {
22
+ ctx.user = {};
23
+ }
24
+ } else {
25
+ ctx.user = {};
26
+ }
27
+ await next();
28
+ }
29
+ };
30
+ export default hook;
package/hooks/cors.ts ADDED
@@ -0,0 +1,48 @@
1
+ // 相对导入
2
+ import { setCorsOptions } from '../util.js';
3
+
4
+ // 类型导入
5
+ import type { Hook } from '../types/hook.js';
6
+ import type { CorsConfig } from '../types/befly.js';
7
+
8
+ /**
9
+ * CORS 跨域处理钩子
10
+ * 设置跨域响应头并处理 OPTIONS 预检请求
11
+ */
12
+ const hook: Hook = {
13
+ after: ['errorHandler'],
14
+ order: 2,
15
+ handler: async (befly, ctx, next) => {
16
+ const req = ctx.req;
17
+
18
+ // 合并默认配置和用户配置
19
+ const defaultConfig: CorsConfig = {
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
+ maxAge: 86400,
25
+ credentials: 'true'
26
+ };
27
+
28
+ const userConfig = (hook as any).config || {};
29
+ const config = { ...defaultConfig, ...userConfig };
30
+
31
+ // 设置 CORS 响应头
32
+ const headers = setCorsOptions(req, config);
33
+
34
+ ctx.corsHeaders = headers;
35
+
36
+ // 处理 OPTIONS 预检请求
37
+ if (req.method === 'OPTIONS') {
38
+ ctx.response = new Response(null, {
39
+ status: 204,
40
+ headers: headers
41
+ });
42
+ return;
43
+ }
44
+
45
+ await next();
46
+ }
47
+ };
48
+ export default hook;
@@ -0,0 +1,23 @@
1
+ // 相对导入
2
+ import { Logger } from '../lib/logger.js';
3
+ import { JsonResponse } from '../util.js';
4
+
5
+ // 类型导入
6
+ import type { Hook } from '../types/hook.js';
7
+
8
+ const hook: Hook = {
9
+ order: 1,
10
+ handler: async (befly, ctx, next) => {
11
+ try {
12
+ await next();
13
+ } catch (err: any) {
14
+ // 记录错误信息
15
+ const apiPath = ctx.api ? `${ctx.req.method}${new URL(ctx.req.url).pathname}` : ctx.req.url;
16
+ Logger.error(`Request Error: ${apiPath}`, err);
17
+
18
+ // 设置错误响应
19
+ ctx.response = JsonResponse(ctx, '内部服务错误');
20
+ }
21
+ }
22
+ };
23
+ export default hook;
@@ -0,0 +1,67 @@
1
+ // 外部依赖
2
+ import { isPlainObject, isEmpty } from 'es-toolkit/compat';
3
+ import { pickFields } from 'befly-util';
4
+
5
+ // 相对导入
6
+ import { Xml } from '../lib/xml.js';
7
+ import { JsonResponse } from '../util.js';
8
+
9
+ // 类型导入
10
+ import type { Hook } from '../types/hook.js';
11
+
12
+ /**
13
+ * 请求参数解析钩子
14
+ * - GET 请求:解析 URL 查询参数
15
+ * - POST 请求:解析 JSON 或 XML 请求体
16
+ * - 根据 API 定义的 fields 过滤字段
17
+ */
18
+ const hook: Hook = {
19
+ after: ['auth'],
20
+ order: 10,
21
+ handler: async (befly, ctx, next) => {
22
+ if (!ctx.api) return next();
23
+
24
+ // GET 请求:解析查询参数
25
+ if (ctx.req.method === 'GET') {
26
+ const url = new URL(ctx.req.url);
27
+ if (isPlainObject(ctx.api.fields) && !isEmpty(ctx.api.fields)) {
28
+ ctx.body = pickFields(Object.fromEntries(url.searchParams), Object.keys(ctx.api.fields));
29
+ } else {
30
+ ctx.body = Object.fromEntries(url.searchParams);
31
+ }
32
+ } else if (ctx.req.method === 'POST') {
33
+ // POST 请求:解析请求体
34
+ const contentType = ctx.req.headers.get('content-type') || '';
35
+ try {
36
+ // JSON 格式
37
+ if (contentType.includes('application/json')) {
38
+ const body = await ctx.req.json();
39
+ if (isPlainObject(ctx.api.fields) && !isEmpty(ctx.api.fields)) {
40
+ ctx.body = pickFields(body, Object.keys(ctx.api.fields));
41
+ } else {
42
+ ctx.body = body;
43
+ }
44
+ } else if (contentType.includes('application/xml') || contentType.includes('text/xml')) {
45
+ // XML 格式
46
+ const text = await ctx.req.text();
47
+ const body = await Xml.parse(text);
48
+ if (isPlainObject(ctx.api.fields) && !isEmpty(ctx.api.fields)) {
49
+ ctx.body = pickFields(body, Object.keys(ctx.api.fields));
50
+ } else {
51
+ ctx.body = body;
52
+ }
53
+ } else {
54
+ // 不支持的 Content-Type
55
+ ctx.response = JsonResponse(ctx, '无效的请求参数格式');
56
+ return;
57
+ }
58
+ } catch (e) {
59
+ // 解析失败
60
+ ctx.response = JsonResponse(ctx, '无效的请求参数格式');
61
+ return;
62
+ }
63
+ }
64
+ await next();
65
+ }
66
+ };
67
+ export default hook;
@@ -0,0 +1,54 @@
1
+ // 相对导入
2
+ import { JsonResponse } from '../util.js';
3
+
4
+ // 类型导入
5
+ import type { Hook } from '../types/hook.js';
6
+
7
+ /**
8
+ * 权限检查钩子
9
+ * - 接口无需权限(auth=false):直接通过
10
+ * - 用户未登录:返回 401
11
+ * - 开发者角色(dev):最高权限,直接通过
12
+ * - 其他角色:检查 Redis 中的角色权限集合
13
+ */
14
+ const hook: Hook = {
15
+ after: ['parser'],
16
+ order: 20,
17
+ handler: async (befly, ctx, next) => {
18
+ if (!ctx.api) return next();
19
+
20
+ // 1. 接口无需权限
21
+ if (ctx.api.auth === false) {
22
+ return next();
23
+ }
24
+
25
+ // 2. 用户未登录
26
+ if (!ctx.user || !ctx.user.userId) {
27
+ ctx.response = JsonResponse(ctx, '未登录', 401);
28
+ return;
29
+ }
30
+
31
+ // 3. 开发者权限(最高权限)
32
+ if (ctx.user.roleCode === 'dev') {
33
+ return next();
34
+ }
35
+
36
+ // 4. 角色权限检查
37
+ let hasPermission = false;
38
+ if (ctx.user.roleCode && befly.redis) {
39
+ // 验证角色权限
40
+ const apiPath = `${ctx.req.method}${new URL(ctx.req.url).pathname}`;
41
+ const roleApisKey = `role:apis:${ctx.user.roleCode}`;
42
+ const isMember = await befly.redis.sismember(roleApisKey, apiPath);
43
+ hasPermission = isMember === 1;
44
+ }
45
+
46
+ if (!hasPermission) {
47
+ ctx.response = JsonResponse(ctx, '无权访问', 403);
48
+ return;
49
+ }
50
+
51
+ await next();
52
+ }
53
+ };
54
+ export default hook;