befly 2.1.1 → 2.2.1
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 +1 -1
- package/apis/health/info.js +1 -1
- package/apis/tool/tokenCheck.js +1 -1
- package/bunfig.toml +3 -0
- package/checks/table.js +3 -3
- package/config/env.js +0 -2
- package/main.js +64 -35
- package/package.json +8 -22
- package/plugins/db.js +7 -587
- package/plugins/logger.js +2 -1
- package/plugins/redis.js +7 -64
- package/plugins/tool.js +3 -40
- package/scripts/syncDb.js +8 -16
- package/utils/api.js +1 -1
- package/utils/{util.js → index.js} +82 -24
- package/utils/jwt.js +63 -13
- package/utils/logger.js +1 -1
- package/utils/redisHelper.js +74 -0
- package/utils/sqlManager.js +471 -0
- package/utils/tool.js +31 -0
- package/utils/validate.js +2 -2
- package/.gitignore +0 -94
- package/libs/jwt.js +0 -97
- /package/utils/{curd.js → sqlBuilder.js} +0 -0
- /package/{libs → utils}/xml.js +0 -0
package/plugins/redis.js
CHANGED
|
@@ -1,78 +1,20 @@
|
|
|
1
1
|
import { redis } from 'bun';
|
|
2
2
|
import { Env } from '../config/env.js';
|
|
3
3
|
import { Logger } from '../utils/logger.js';
|
|
4
|
+
import { RedisHelper, getRedisClient } from '../utils/redisHelper.js';
|
|
4
5
|
|
|
5
6
|
export default {
|
|
6
7
|
after: ['_logger'],
|
|
7
8
|
async onInit(befly) {
|
|
8
9
|
try {
|
|
9
10
|
if (Env.REDIS_ENABLE === 1) {
|
|
10
|
-
|
|
11
|
+
const client = getRedisClient();
|
|
12
|
+
if ((await client.ping()) !== 'PONG') {
|
|
11
13
|
throw new Error('Redis 连接失败');
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
setObject: async (key, obj, ttl = null) => {
|
|
17
|
-
try {
|
|
18
|
-
const data = JSON.stringify(obj);
|
|
19
|
-
if (ttl) {
|
|
20
|
-
return await redis.setEx(`${process.env.REDIS_KEY_PREFIX}:${key}`, ttl, data);
|
|
21
|
-
}
|
|
22
|
-
return await redis.set(`${process.env.REDIS_KEY_PREFIX}:${key}`, data);
|
|
23
|
-
} catch (error) {
|
|
24
|
-
Logger.error({
|
|
25
|
-
msg: 'Redis setObject 错误',
|
|
26
|
-
message: error.message,
|
|
27
|
-
stack: error.stack
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
|
|
32
|
-
getObject: async (key) => {
|
|
33
|
-
try {
|
|
34
|
-
const data = await redis.get(`${process.env.REDIS_KEY_PREFIX}:${key}`);
|
|
35
|
-
return data ? JSON.parse(data) : null;
|
|
36
|
-
} catch (error) {
|
|
37
|
-
Logger.error({
|
|
38
|
-
msg: 'Redis getObject 错误',
|
|
39
|
-
message: error.message,
|
|
40
|
-
stack: error.stack
|
|
41
|
-
});
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
},
|
|
45
|
-
|
|
46
|
-
delObject: async (key) => {
|
|
47
|
-
try {
|
|
48
|
-
await redis.del(`${process.env.REDIS_KEY_PREFIX}:${key}`);
|
|
49
|
-
} catch (error) {
|
|
50
|
-
Logger.error({
|
|
51
|
-
msg: 'Redis delObject 错误',
|
|
52
|
-
message: error.message,
|
|
53
|
-
stack: error.stack
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
// 添加时序ID生成函数
|
|
59
|
-
genTimeID: async () => {
|
|
60
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
61
|
-
const key = `time_id_counter:${timestamp}`;
|
|
62
|
-
|
|
63
|
-
const counter = await redis.incr(key);
|
|
64
|
-
await redis.expire(key, 2);
|
|
65
|
-
|
|
66
|
-
// 前3位计数器 + 后3位随机数
|
|
67
|
-
const counterPrefix = (counter % 1000).toString().padStart(3, '0'); // 000-999
|
|
68
|
-
const randomSuffix = Math.floor(Math.random() * 1000)
|
|
69
|
-
.toString()
|
|
70
|
-
.padStart(3, '0'); // 000-999
|
|
71
|
-
const suffix = `${counterPrefix}${randomSuffix}`;
|
|
72
|
-
|
|
73
|
-
return Number(`${timestamp}${suffix}`);
|
|
74
|
-
}
|
|
75
|
-
};
|
|
16
|
+
// 返回工具对象,向下游以相同 API 暴露
|
|
17
|
+
return RedisHelper;
|
|
76
18
|
} else {
|
|
77
19
|
Logger.warn(`Redis 未启用,跳过初始化`);
|
|
78
20
|
return {};
|
|
@@ -83,7 +25,8 @@ export default {
|
|
|
83
25
|
message: err.message,
|
|
84
26
|
stack: err.stack
|
|
85
27
|
});
|
|
86
|
-
|
|
28
|
+
// 插件内禁止直接退出进程,抛出异常交由主流程统一处理
|
|
29
|
+
throw err;
|
|
87
30
|
}
|
|
88
31
|
}
|
|
89
32
|
};
|
package/plugins/tool.js
CHANGED
|
@@ -1,45 +1,8 @@
|
|
|
1
|
+
import { Tool } from '../utils/tool.js';
|
|
2
|
+
|
|
1
3
|
export default {
|
|
2
4
|
after: ['_redis', '_db'],
|
|
3
5
|
async onInit(befly) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
// 辅助函数:过滤掉 undefined 值和指定字段
|
|
7
|
-
const filterData = (obj, excludeFields = []) => {
|
|
8
|
-
return Object.fromEntries(Object.entries(obj).filter(([key, value]) => value !== undefined && !excludeFields.includes(key)));
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
return {
|
|
12
|
-
async updData(data) {
|
|
13
|
-
const updateData = {
|
|
14
|
-
...filterData(data, ['id', 'created_at', 'deleted_at']),
|
|
15
|
-
updated_at: Date.now()
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
return updateData;
|
|
19
|
-
},
|
|
20
|
-
async insData(data) {
|
|
21
|
-
const now = Date.now();
|
|
22
|
-
|
|
23
|
-
if (Array.isArray(data)) {
|
|
24
|
-
const data2 = await Promise.all(
|
|
25
|
-
data.map(async (item) => ({
|
|
26
|
-
...filterData(item),
|
|
27
|
-
id: await befly.redis.genTimeID(),
|
|
28
|
-
created_at: now,
|
|
29
|
-
updated_at: now
|
|
30
|
-
}))
|
|
31
|
-
);
|
|
32
|
-
return data2;
|
|
33
|
-
} else {
|
|
34
|
-
const data2 = {
|
|
35
|
-
...filterData(data),
|
|
36
|
-
id: await befly.redis.genTimeID(),
|
|
37
|
-
created_at: now,
|
|
38
|
-
updated_at: now
|
|
39
|
-
};
|
|
40
|
-
return data2;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
};
|
|
6
|
+
return new Tool(befly);
|
|
44
7
|
}
|
|
45
8
|
};
|
package/scripts/syncDb.js
CHANGED
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { SQL } from 'bun';
|
|
7
6
|
import { Env } from '../config/env.js';
|
|
8
7
|
import { Logger } from '../utils/logger.js';
|
|
9
|
-
import { parseFieldRule } from '../utils/
|
|
8
|
+
import { parseFieldRule, createSqlClient } from '../utils/index.js';
|
|
10
9
|
import { __dirtables, getProjectDir } from '../system.js';
|
|
11
|
-
import
|
|
10
|
+
import { checkTable } from '../checks/table.js';
|
|
12
11
|
|
|
13
12
|
const typeMapping = {
|
|
14
13
|
number: 'BIGINT',
|
|
@@ -54,16 +53,10 @@ const getColumnDefinition = (fieldName, rule) => {
|
|
|
54
53
|
return columnDef;
|
|
55
54
|
};
|
|
56
55
|
|
|
57
|
-
//
|
|
58
|
-
const toDollarParams = (query, params) => {
|
|
59
|
-
if (!params || params.length === 0) return query;
|
|
60
|
-
let i = 0;
|
|
61
|
-
return query.replace(/\?/g, () => `$${++i}`);
|
|
62
|
-
};
|
|
63
|
-
|
|
56
|
+
// 通用执行器:直接使用 Bun SQL 参数化(MySQL 使用 '?' 占位符)
|
|
64
57
|
const exec = async (client, query, params = []) => {
|
|
65
|
-
if (params.length > 0) {
|
|
66
|
-
return await client.unsafe(
|
|
58
|
+
if (params && params.length > 0) {
|
|
59
|
+
return await client.unsafe(query, params);
|
|
67
60
|
}
|
|
68
61
|
return await client.unsafe(query);
|
|
69
62
|
};
|
|
@@ -277,14 +270,13 @@ const SyncDb = async () => {
|
|
|
277
270
|
Logger.info('开始数据库表结构同步...');
|
|
278
271
|
|
|
279
272
|
// 验证表定义文件
|
|
280
|
-
const tableValidationResult = await
|
|
273
|
+
const tableValidationResult = await checkTable();
|
|
281
274
|
if (!tableValidationResult) {
|
|
282
275
|
throw new Error('表定义验证失败');
|
|
283
276
|
}
|
|
284
277
|
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
client = new SQL({ url, max: 1, bigint: true });
|
|
278
|
+
// 建立数据库连接并检查版本(统一工具函数)
|
|
279
|
+
client = await createSqlClient({ max: 1 });
|
|
288
280
|
const result = await client`SELECT VERSION() AS version`;
|
|
289
281
|
const version = result[0].version;
|
|
290
282
|
|
package/utils/api.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { SQL } from 'bun';
|
|
3
4
|
import { Env } from '../config/env.js';
|
|
5
|
+
import { Logger } from './logger.js';
|
|
4
6
|
|
|
5
7
|
export const setCorsOptions = (req) => {
|
|
6
8
|
return {
|
|
@@ -73,7 +75,7 @@ export const formatDate = (date = new Date(), format = 'YYYY-MM-DD HH:mm:ss') =>
|
|
|
73
75
|
* @param {number} endTime - 结束时间(可选,默认为当前时间)
|
|
74
76
|
* @returns {string} 时间差(如果小于1秒返回"xx 毫秒",否则返回"xx 秒")
|
|
75
77
|
*/
|
|
76
|
-
export const
|
|
78
|
+
export const calcPerfTime = (startTime, endTime = Bun.nanoseconds()) => {
|
|
77
79
|
const elapsedMs = (endTime - startTime) / 1_000_000;
|
|
78
80
|
|
|
79
81
|
if (elapsedMs < 1000) {
|
|
@@ -87,16 +89,12 @@ export const calculateElapsedTime = (startTime, endTime = Bun.nanoseconds()) =>
|
|
|
87
89
|
// 类型判断
|
|
88
90
|
export const isType = (value, type) => {
|
|
89
91
|
const actualType = Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
|
|
90
|
-
const expectedType = type.toLowerCase();
|
|
92
|
+
const expectedType = String(type).toLowerCase();
|
|
91
93
|
|
|
92
|
-
//
|
|
94
|
+
// 语义类型单独处理,其余走 actualType === expectedType
|
|
93
95
|
switch (expectedType) {
|
|
94
|
-
case 'null':
|
|
95
|
-
return value === null;
|
|
96
|
-
case 'undefined':
|
|
97
|
-
return value === undefined;
|
|
98
96
|
case 'nan':
|
|
99
|
-
return Number.isNaN(value);
|
|
97
|
+
return typeof value === 'number' && Number.isNaN(value);
|
|
100
98
|
case 'empty':
|
|
101
99
|
return value === '' || value === null || value === undefined;
|
|
102
100
|
case 'integer':
|
|
@@ -117,15 +115,14 @@ export const isType = (value, type) => {
|
|
|
117
115
|
return value !== Object(value);
|
|
118
116
|
case 'reference':
|
|
119
117
|
return value === Object(value);
|
|
120
|
-
case 'function':
|
|
121
|
-
return typeof value === 'function';
|
|
122
118
|
default:
|
|
123
119
|
return actualType === expectedType;
|
|
124
120
|
}
|
|
125
121
|
};
|
|
126
122
|
|
|
127
123
|
export const pickFields = (obj, keys) => {
|
|
128
|
-
|
|
124
|
+
// 仅对对象或数组进行字段挑选,其他类型返回空对象(保持原有行为)
|
|
125
|
+
if (!obj || (!isType(obj, 'object') && !isType(obj, 'array'))) {
|
|
129
126
|
return {};
|
|
130
127
|
}
|
|
131
128
|
|
|
@@ -140,25 +137,56 @@ export const pickFields = (obj, keys) => {
|
|
|
140
137
|
return result;
|
|
141
138
|
};
|
|
142
139
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
140
|
+
/**
|
|
141
|
+
* 从对象或数组数据中按“字段名”和“字段值”进行排除过滤。
|
|
142
|
+
* - 支持对象:移除指定字段名,以及值在排除值列表中的字段。
|
|
143
|
+
* - 支持数组:
|
|
144
|
+
* - 如果元素为对象,按同样规则清洗(移除字段名/字段值命中项)。
|
|
145
|
+
* - 如果元素为原始值(数字/字符串等),当元素值命中排除值则从数组中移除该元素。
|
|
146
|
+
*
|
|
147
|
+
* 约定:excludeKeys 与 excludeValues 均为数组类型。
|
|
148
|
+
*
|
|
149
|
+
* 示例:
|
|
150
|
+
* omitFields({ a:1, b:undefined, c:null }, ['a'], [undefined]) -> { c:null }
|
|
151
|
+
* omitFields([{ a:1, b:null }, null, 0], ['a'], [null]) -> [{}, 0]
|
|
152
|
+
*
|
|
153
|
+
* 注意:仅当第一个参数为对象或数组时执行过滤,否则原样返回。
|
|
154
|
+
*
|
|
155
|
+
* @template T
|
|
156
|
+
* @param {Record<string, any> | Array<any>} data - 原始数据(对象或数组)
|
|
157
|
+
* @param {string[]} [excludeKeys=[]] - 要排除的字段名(对象属性名)数组
|
|
158
|
+
* @param {any[]} [excludeValues=[]] - 要排除的字段值数组;当包含 undefined/null 等时,将移除这些值对应的字段或数组元素
|
|
159
|
+
* @returns {T} 过滤后的数据,类型与入参保持一致
|
|
160
|
+
*/
|
|
161
|
+
export const omitFields = (data, excludeKeys = [], excludeValues = []) => {
|
|
162
|
+
const shouldDropValue = (v) => excludeValues.some((x) => x === v);
|
|
163
|
+
|
|
164
|
+
const cleanObject = (obj) => {
|
|
165
|
+
if (!isType(obj, 'object')) return obj;
|
|
166
|
+
const result = {};
|
|
167
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
168
|
+
if (excludeKeys.includes(k)) continue;
|
|
169
|
+
if (shouldDropValue(v)) continue;
|
|
170
|
+
result[k] = v;
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
};
|
|
147
174
|
|
|
148
|
-
|
|
175
|
+
if (isType(data, 'array')) {
|
|
176
|
+
return /** @type {any} */ (data.filter((item) => !shouldDropValue(item)).map((item) => (isType(item, 'object') ? cleanObject(item) : item)));
|
|
177
|
+
}
|
|
149
178
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
result[key] = obj[key];
|
|
153
|
-
}
|
|
179
|
+
if (isType(data, 'object')) {
|
|
180
|
+
return /** @type {any} */ (cleanObject(data));
|
|
154
181
|
}
|
|
155
182
|
|
|
156
|
-
|
|
183
|
+
// 非对象/数组则原样返回(不处理)
|
|
184
|
+
return /** @type {any} */ (data);
|
|
157
185
|
};
|
|
158
186
|
|
|
159
187
|
export const isEmptyObject = (obj) => {
|
|
160
188
|
// 首先检查是否为对象
|
|
161
|
-
if (!obj
|
|
189
|
+
if (!isType(obj, 'object')) {
|
|
162
190
|
return false;
|
|
163
191
|
}
|
|
164
192
|
|
|
@@ -168,7 +196,7 @@ export const isEmptyObject = (obj) => {
|
|
|
168
196
|
|
|
169
197
|
export const isEmptyArray = (arr) => {
|
|
170
198
|
// 首先检查是否为数组
|
|
171
|
-
if (!
|
|
199
|
+
if (!isType(arr, 'array')) {
|
|
172
200
|
return false;
|
|
173
201
|
}
|
|
174
202
|
|
|
@@ -205,7 +233,8 @@ export const dirname2 = (importMetaUrl) => {
|
|
|
205
233
|
|
|
206
234
|
// 过滤日志字段的函数
|
|
207
235
|
export const filterLogFields = (body, excludeFields = '') => {
|
|
208
|
-
|
|
236
|
+
// 仅在对象或数组时进行过滤,保持与原 typeof === 'object' 行为一致(数组也会进入)
|
|
237
|
+
if (!body || (!isType(body, 'object') && !isType(body, 'array'))) return body;
|
|
209
238
|
|
|
210
239
|
// 如果是字符串,按逗号分割并清理空格
|
|
211
240
|
const fieldsArray = excludeFields
|
|
@@ -314,3 +343,32 @@ export const parseFieldRule = (rule) => {
|
|
|
314
343
|
|
|
315
344
|
return allParts;
|
|
316
345
|
};
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* 创建并校验 Bun SQL 客户端
|
|
349
|
+
* - 否则按 scripts/syncDb.js 的方式拼接 URL
|
|
350
|
+
* - 连接成功后返回 SQL 实例,失败会自动 close 并抛出
|
|
351
|
+
* @param {object} options 传给 new SQL 的参数(如 { max: 1, bigint: true })
|
|
352
|
+
*/
|
|
353
|
+
export async function createSqlClient(options = {}) {
|
|
354
|
+
const url = `mysql://${encodeURIComponent(Env.MYSQL_USER)}:${encodeURIComponent(Env.MYSQL_PASSWORD)}@${Env.MYSQL_HOST}:${Env.MYSQL_PORT}/${Env.MYSQL_DB}`;
|
|
355
|
+
|
|
356
|
+
const sql = new SQL({
|
|
357
|
+
url: url,
|
|
358
|
+
max: options.max ?? 1,
|
|
359
|
+
bigint: options.bigint ?? true,
|
|
360
|
+
...options
|
|
361
|
+
});
|
|
362
|
+
try {
|
|
363
|
+
const ver = await sql`SELECT VERSION() AS version`;
|
|
364
|
+
const version = ver?.[0]?.version;
|
|
365
|
+
Logger.info(`数据库连接成功,MySQL 版本: ${version}`);
|
|
366
|
+
return sql;
|
|
367
|
+
} catch (error) {
|
|
368
|
+
Logger.error('数据库连接测试失败:', error);
|
|
369
|
+
try {
|
|
370
|
+
await sql.close();
|
|
371
|
+
} catch {}
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
}
|
package/utils/jwt.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHmac } from 'crypto';
|
|
2
2
|
import { Env } from '../config/env.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -6,6 +6,54 @@ import { Env } from '../config/env.js';
|
|
|
6
6
|
* 提供JWT token的签名、验证和解码功能以及应用层的便捷接口
|
|
7
7
|
*/
|
|
8
8
|
export class Jwt {
|
|
9
|
+
// 原基础工具:算法映射
|
|
10
|
+
static ALGORITHMS = {
|
|
11
|
+
HS256: 'sha256',
|
|
12
|
+
HS384: 'sha384',
|
|
13
|
+
HS512: 'sha512'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// 原基础工具:Base64 URL 编解码
|
|
17
|
+
static base64UrlEncode(input) {
|
|
18
|
+
const base64 = Buffer.isBuffer(input) ? input.toString('base64') : Buffer.from(input, 'utf8').toString('base64');
|
|
19
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
20
|
+
}
|
|
21
|
+
static base64UrlDecode(str) {
|
|
22
|
+
const padding = 4 - (str.length % 4);
|
|
23
|
+
if (padding !== 4) str += '='.repeat(padding);
|
|
24
|
+
str = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
25
|
+
return Buffer.from(str, 'base64').toString('utf8');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 原基础工具:过期时间解析与签名/比较
|
|
29
|
+
static parseExpiration(expiresIn) {
|
|
30
|
+
if (typeof expiresIn === 'number') return expiresIn;
|
|
31
|
+
if (typeof expiresIn !== 'string') throw new Error('过期时间格式无效');
|
|
32
|
+
const numericValue = parseInt(expiresIn);
|
|
33
|
+
if (!isNaN(numericValue) && numericValue.toString() === expiresIn) return numericValue;
|
|
34
|
+
const match = expiresIn.match(/^(\d+)(ms|[smhdwy])$/);
|
|
35
|
+
if (!match) throw new Error('过期时间格式无效');
|
|
36
|
+
const value = parseInt(match[1]);
|
|
37
|
+
const unit = match[2];
|
|
38
|
+
if (unit === 'ms') return Math.floor(value / 1000);
|
|
39
|
+
const multipliers = { s: 1, m: 60, h: 3600, d: 86400, w: 604800, y: 31536000 };
|
|
40
|
+
return value * multipliers[unit];
|
|
41
|
+
}
|
|
42
|
+
static createSignature(algorithm, secret, data) {
|
|
43
|
+
const hashAlgorithm = this.ALGORITHMS[algorithm];
|
|
44
|
+
if (!hashAlgorithm) throw new Error(`不支持的算法: ${algorithm}`);
|
|
45
|
+
const hmac = createHmac(hashAlgorithm, secret);
|
|
46
|
+
hmac.update(data);
|
|
47
|
+
return this.base64UrlEncode(hmac.digest());
|
|
48
|
+
}
|
|
49
|
+
static constantTimeCompare(a, b) {
|
|
50
|
+
if (a.length !== b.length) return false;
|
|
51
|
+
let result = 0;
|
|
52
|
+
for (let i = 0; i < a.length; i++) {
|
|
53
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
54
|
+
}
|
|
55
|
+
return result === 0;
|
|
56
|
+
}
|
|
9
57
|
/**
|
|
10
58
|
* 签名JWT token
|
|
11
59
|
* @param {object} payload - JWT载荷数据
|
|
@@ -26,7 +74,7 @@ export class Jwt {
|
|
|
26
74
|
const now = Math.floor(Date.now() / 1000);
|
|
27
75
|
|
|
28
76
|
// 创建header
|
|
29
|
-
const header =
|
|
77
|
+
const header = Jwt.base64UrlEncode(
|
|
30
78
|
JSON.stringify({
|
|
31
79
|
alg: algorithm,
|
|
32
80
|
typ: 'JWT'
|
|
@@ -37,22 +85,22 @@ export class Jwt {
|
|
|
37
85
|
const jwtPayload = { ...payload, iat: now };
|
|
38
86
|
|
|
39
87
|
if (options.expiresIn || Env.JWT_EXPIRES_IN) {
|
|
40
|
-
const expSeconds =
|
|
88
|
+
const expSeconds = Jwt.parseExpiration(options.expiresIn || Env.JWT_EXPIRES_IN);
|
|
41
89
|
jwtPayload.exp = now + expSeconds;
|
|
42
90
|
}
|
|
43
91
|
if (options.issuer) jwtPayload.iss = options.issuer;
|
|
44
92
|
if (options.audience) jwtPayload.aud = options.audience;
|
|
45
93
|
if (options.subject) jwtPayload.sub = options.subject;
|
|
46
94
|
if (options.notBefore) {
|
|
47
|
-
jwtPayload.nbf = typeof options.notBefore === 'number' ? options.notBefore : now +
|
|
95
|
+
jwtPayload.nbf = typeof options.notBefore === 'number' ? options.notBefore : now + Jwt.parseExpiration(options.notBefore);
|
|
48
96
|
}
|
|
49
97
|
if (options.jwtId) jwtPayload.jti = options.jwtId;
|
|
50
98
|
|
|
51
|
-
const encodedPayload =
|
|
99
|
+
const encodedPayload = Jwt.base64UrlEncode(JSON.stringify(jwtPayload));
|
|
52
100
|
|
|
53
101
|
// 创建签名
|
|
54
102
|
const data = `${header}.${encodedPayload}`;
|
|
55
|
-
const signature =
|
|
103
|
+
const signature = Jwt.createSignature(algorithm, secret, data);
|
|
56
104
|
|
|
57
105
|
return `${data}.${signature}`;
|
|
58
106
|
}
|
|
@@ -80,20 +128,20 @@ export class Jwt {
|
|
|
80
128
|
|
|
81
129
|
try {
|
|
82
130
|
// 解析header和payload
|
|
83
|
-
const header = JSON.parse(
|
|
84
|
-
const payload = JSON.parse(
|
|
131
|
+
const header = JSON.parse(Jwt.base64UrlDecode(parts[0]));
|
|
132
|
+
const payload = JSON.parse(Jwt.base64UrlDecode(parts[1]));
|
|
85
133
|
const signature = parts[2];
|
|
86
134
|
|
|
87
135
|
// 验证算法
|
|
88
|
-
if (!
|
|
136
|
+
if (!Jwt.ALGORITHMS[header.alg]) {
|
|
89
137
|
throw new Error(`不支持的算法: ${header.alg}`);
|
|
90
138
|
}
|
|
91
139
|
|
|
92
140
|
// 验证签名
|
|
93
141
|
const data = `${parts[0]}.${parts[1]}`;
|
|
94
|
-
const expectedSignature =
|
|
142
|
+
const expectedSignature = Jwt.createSignature(header.alg, secret, data);
|
|
95
143
|
|
|
96
|
-
if (!
|
|
144
|
+
if (!Jwt.constantTimeCompare(signature, expectedSignature)) {
|
|
97
145
|
throw new Error('Token签名无效');
|
|
98
146
|
}
|
|
99
147
|
|
|
@@ -147,8 +195,8 @@ export class Jwt {
|
|
|
147
195
|
}
|
|
148
196
|
|
|
149
197
|
try {
|
|
150
|
-
const header = JSON.parse(
|
|
151
|
-
const payload = JSON.parse(
|
|
198
|
+
const header = JSON.parse(Jwt.base64UrlDecode(parts[0]));
|
|
199
|
+
const payload = JSON.parse(Jwt.base64UrlDecode(parts[1]));
|
|
152
200
|
|
|
153
201
|
return complete ? { header, payload, signature: parts[2] } : payload;
|
|
154
202
|
} catch (error) {
|
|
@@ -335,3 +383,5 @@ export class Jwt {
|
|
|
335
383
|
return timeToExpiry > 0 && timeToExpiry <= thresholdSeconds;
|
|
336
384
|
}
|
|
337
385
|
}
|
|
386
|
+
|
|
387
|
+
// 已使用 `export class Jwt` 具名导出
|
package/utils/logger.js
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { redis as bunRedis } from 'bun';
|
|
2
|
+
import { Env } from '../config/env.js';
|
|
3
|
+
import { Logger } from './logger.js';
|
|
4
|
+
|
|
5
|
+
const prefix = Env.REDIS_KEY_PREFIX ? `${Env.REDIS_KEY_PREFIX}:` : '';
|
|
6
|
+
|
|
7
|
+
let redisClient = bunRedis;
|
|
8
|
+
export const setRedisClient = (client) => {
|
|
9
|
+
redisClient = client || bunRedis;
|
|
10
|
+
};
|
|
11
|
+
export const getRedisClient = () => redisClient;
|
|
12
|
+
|
|
13
|
+
export const RedisHelper = {
|
|
14
|
+
async setObject(key, obj, ttl = null) {
|
|
15
|
+
try {
|
|
16
|
+
const data = JSON.stringify(obj);
|
|
17
|
+
const pkey = `${prefix}${key}`;
|
|
18
|
+
if (ttl) {
|
|
19
|
+
return await redisClient.setEx(pkey, ttl, data);
|
|
20
|
+
}
|
|
21
|
+
return await redisClient.set(pkey, data);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
Logger.error({
|
|
24
|
+
msg: 'Redis setObject 错误',
|
|
25
|
+
message: error.message,
|
|
26
|
+
stack: error.stack
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
async getObject(key) {
|
|
32
|
+
try {
|
|
33
|
+
const pkey = `${prefix}${key}`;
|
|
34
|
+
const data = await redisClient.get(pkey);
|
|
35
|
+
return data ? JSON.parse(data) : null;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
Logger.error({
|
|
38
|
+
msg: 'Redis getObject 错误',
|
|
39
|
+
message: error.message,
|
|
40
|
+
stack: error.stack
|
|
41
|
+
});
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
async delObject(key) {
|
|
47
|
+
try {
|
|
48
|
+
const pkey = `${prefix}${key}`;
|
|
49
|
+
await redisClient.del(pkey);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
Logger.error({
|
|
52
|
+
msg: 'Redis delObject 错误',
|
|
53
|
+
message: error.message,
|
|
54
|
+
stack: error.stack
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
async genTimeID() {
|
|
60
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
61
|
+
const key = `${prefix}time_id_counter:${timestamp}`;
|
|
62
|
+
|
|
63
|
+
const counter = await redisClient.incr(key);
|
|
64
|
+
await redisClient.expire(key, 2);
|
|
65
|
+
|
|
66
|
+
const counterPrefix = (counter % 1000).toString().padStart(3, '0');
|
|
67
|
+
const randomSuffix = Math.floor(Math.random() * 1000)
|
|
68
|
+
.toString()
|
|
69
|
+
.padStart(3, '0');
|
|
70
|
+
const suffix = `${counterPrefix}${randomSuffix}`;
|
|
71
|
+
|
|
72
|
+
return Number(`${timestamp}${suffix}`);
|
|
73
|
+
}
|
|
74
|
+
};
|