befly 3.8.19 → 3.8.20
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 +7 -6
- package/bunfig.toml +1 -1
- package/lib/database.ts +28 -25
- package/lib/dbHelper.ts +3 -3
- package/lib/jwt.ts +90 -99
- package/lib/logger.ts +44 -23
- package/lib/redisHelper.ts +19 -22
- package/loader/loadApis.ts +69 -114
- package/loader/loadHooks.ts +65 -0
- package/loader/loadPlugins.ts +50 -219
- package/main.ts +106 -133
- package/package.json +15 -7
- package/paths.ts +20 -0
- package/plugins/cache.ts +1 -3
- package/plugins/db.ts +8 -11
- package/plugins/logger.ts +5 -3
- package/plugins/redis.ts +10 -14
- package/router/api.ts +60 -106
- package/router/root.ts +15 -12
- package/router/static.ts +54 -58
- package/sync/syncAll.ts +58 -0
- package/sync/syncApi.ts +264 -0
- package/sync/syncDb/apply.ts +194 -0
- package/sync/syncDb/constants.ts +76 -0
- package/sync/syncDb/ddl.ts +194 -0
- package/sync/syncDb/helpers.ts +200 -0
- package/sync/syncDb/index.ts +164 -0
- package/sync/syncDb/schema.ts +201 -0
- package/sync/syncDb/sqlite.ts +50 -0
- package/sync/syncDb/table.ts +321 -0
- package/sync/syncDb/tableCreate.ts +146 -0
- package/sync/syncDb/version.ts +72 -0
- package/sync/syncDb.ts +19 -0
- package/sync/syncDev.ts +206 -0
- package/sync/syncMenu.ts +331 -0
- package/tsconfig.json +2 -4
- package/types/api.d.ts +6 -0
- package/types/befly.d.ts +152 -28
- package/types/context.d.ts +29 -3
- package/types/hook.d.ts +35 -0
- package/types/index.ts +14 -1
- package/types/plugin.d.ts +6 -7
- package/types/sync.d.ts +403 -0
- package/check.ts +0 -378
- package/env.ts +0 -106
- package/lib/middleware.ts +0 -275
- package/types/env.ts +0 -65
- package/types/util.d.ts +0 -45
- package/util.ts +0 -257
package/README.md
CHANGED
|
@@ -55,7 +55,6 @@ bun run main.ts
|
|
|
55
55
|
|
|
56
56
|
```typescript
|
|
57
57
|
// apis/user/hello.ts
|
|
58
|
-
import { Yes } from 'befly';
|
|
59
58
|
import type { ApiRoute } from 'befly';
|
|
60
59
|
|
|
61
60
|
export default {
|
|
@@ -63,9 +62,12 @@ export default {
|
|
|
63
62
|
auth: false, // 公开接口
|
|
64
63
|
fields: {},
|
|
65
64
|
handler: async (befly, ctx) => {
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
return {
|
|
66
|
+
msg: 'Hello, Befly!',
|
|
67
|
+
data: {
|
|
68
|
+
timestamp: Date.now()
|
|
69
|
+
}
|
|
70
|
+
};
|
|
69
71
|
}
|
|
70
72
|
} as ApiRoute;
|
|
71
73
|
```
|
|
@@ -77,7 +79,6 @@ export default {
|
|
|
77
79
|
### TypeScript 全面支持
|
|
78
80
|
|
|
79
81
|
```typescript
|
|
80
|
-
import { Yes } from 'befly';
|
|
81
82
|
import type { ApiRoute, BeflyContext } from 'befly';
|
|
82
83
|
import type { User } from './types/models';
|
|
83
84
|
|
|
@@ -97,7 +98,7 @@ export default {
|
|
|
97
98
|
where: { id }
|
|
98
99
|
});
|
|
99
100
|
|
|
100
|
-
return
|
|
101
|
+
return { msg: '查询成功', data: user };
|
|
101
102
|
}
|
|
102
103
|
} as ApiRoute;
|
|
103
104
|
```
|
package/bunfig.toml
CHANGED
package/lib/database.ts
CHANGED
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { SQL, RedisClient } from 'bun';
|
|
7
|
-
import { Env } from '../env.js';
|
|
8
7
|
import { Logger } from './logger.js';
|
|
9
8
|
import { DbHelper } from './dbHelper.js';
|
|
10
9
|
import { RedisHelper } from './redisHelper.js';
|
|
11
|
-
import type { BeflyContext } from '../types/befly.js';
|
|
10
|
+
import type { BeflyContext, DatabaseConfig, RedisConfig } from '../types/befly.js';
|
|
12
11
|
import type { SqlClientOptions } from '../types/database.js';
|
|
13
12
|
|
|
14
13
|
/**
|
|
@@ -26,50 +25,49 @@ export class Database {
|
|
|
26
25
|
|
|
27
26
|
/**
|
|
28
27
|
* 连接 SQL 数据库
|
|
29
|
-
* @param
|
|
28
|
+
* @param config - 数据库配置
|
|
30
29
|
* @returns SQL 客户端实例
|
|
31
30
|
*/
|
|
32
|
-
static async connectSql(
|
|
31
|
+
static async connectSql(config: DatabaseConfig): Promise<SQL> {
|
|
33
32
|
// 构建数据库连接字符串
|
|
34
|
-
const type =
|
|
35
|
-
const host =
|
|
36
|
-
const port =
|
|
37
|
-
const user = encodeURIComponent(
|
|
38
|
-
const password = encodeURIComponent(
|
|
39
|
-
const database = encodeURIComponent(
|
|
33
|
+
const type = config.type || 'mysql';
|
|
34
|
+
const host = config.host || '127.0.0.1';
|
|
35
|
+
const port = config.port || 3306;
|
|
36
|
+
const user = encodeURIComponent(config.username || 'root');
|
|
37
|
+
const password = encodeURIComponent(config.password || 'root');
|
|
38
|
+
const database = encodeURIComponent(config.database || 'befly_demo');
|
|
40
39
|
|
|
41
40
|
let finalUrl: string;
|
|
42
41
|
if (type === 'sqlite') {
|
|
43
42
|
finalUrl = database;
|
|
44
43
|
} else {
|
|
45
44
|
if (!host || !database) {
|
|
46
|
-
throw new Error('
|
|
45
|
+
throw new Error('数据库配置不完整,请检查配置参数');
|
|
47
46
|
}
|
|
48
47
|
finalUrl = `${type}://${user}:${password}@${host}:${port}/${database}`;
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
let sql: SQL;
|
|
52
51
|
|
|
53
|
-
if (
|
|
52
|
+
if (type === 'sqlite') {
|
|
54
53
|
sql = new SQL(finalUrl);
|
|
55
54
|
} else {
|
|
56
55
|
sql = new SQL({
|
|
57
56
|
url: finalUrl,
|
|
58
|
-
max:
|
|
59
|
-
bigint: false
|
|
60
|
-
...options
|
|
57
|
+
max: config.poolMax ?? 1,
|
|
58
|
+
bigint: false
|
|
61
59
|
});
|
|
62
60
|
}
|
|
63
61
|
|
|
64
62
|
try {
|
|
65
|
-
const timeout =
|
|
63
|
+
const timeout = 30000;
|
|
66
64
|
|
|
67
65
|
const healthCheckPromise = (async () => {
|
|
68
66
|
let version = '';
|
|
69
|
-
if (
|
|
67
|
+
if (type === 'sqlite') {
|
|
70
68
|
const v = await sql`SELECT sqlite_version() AS version`;
|
|
71
69
|
version = v?.[0]?.version;
|
|
72
|
-
} else if (
|
|
70
|
+
} else if (type === 'postgresql' || type === 'postgres') {
|
|
73
71
|
const v = await sql`SELECT version() AS version`;
|
|
74
72
|
version = v?.[0]?.version;
|
|
75
73
|
} else {
|
|
@@ -157,21 +155,26 @@ export class Database {
|
|
|
157
155
|
|
|
158
156
|
/**
|
|
159
157
|
* 连接 Redis
|
|
158
|
+
* @param config - Redis 配置
|
|
160
159
|
* @returns Redis 客户端实例
|
|
161
160
|
*/
|
|
162
|
-
static async connectRedis(): Promise<RedisClient> {
|
|
161
|
+
static async connectRedis(config: RedisConfig = {}): Promise<RedisClient> {
|
|
163
162
|
try {
|
|
164
163
|
// 构建 Redis URL
|
|
165
|
-
const
|
|
164
|
+
const host = config.host || '127.0.0.1';
|
|
165
|
+
const port = config.port || 6379;
|
|
166
|
+
const username = config.username || '';
|
|
167
|
+
const password = config.password || '';
|
|
168
|
+
const db = config.db || 0;
|
|
166
169
|
|
|
167
170
|
let auth = '';
|
|
168
|
-
if (
|
|
169
|
-
auth = `${
|
|
170
|
-
} else if (
|
|
171
|
-
auth = `:${
|
|
171
|
+
if (username && password) {
|
|
172
|
+
auth = `${username}:${password}@`;
|
|
173
|
+
} else if (password) {
|
|
174
|
+
auth = `:${password}@`;
|
|
172
175
|
}
|
|
173
176
|
|
|
174
|
-
const url = `redis://${auth}${
|
|
177
|
+
const url = `redis://${auth}${host}:${port}/${db}`;
|
|
175
178
|
|
|
176
179
|
const redis = new RedisClient(url, {
|
|
177
180
|
connectionTimeout: 30000,
|
package/lib/dbHelper.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { snakeCase } from 'es-toolkit/string';
|
|
7
7
|
import { SqlBuilder } from './sqlBuilder.js';
|
|
8
|
-
import { keysToCamel, arrayKeysToCamel, keysToSnake, fieldClear } from '
|
|
8
|
+
import { keysToCamel, arrayKeysToCamel, keysToSnake, fieldClear } from 'befly-util';
|
|
9
9
|
import { Logger } from '../lib/logger.js';
|
|
10
10
|
import type { WhereConditions } from '../types/common.js';
|
|
11
11
|
import type { BeflyContext } from '../types/befly.js';
|
|
@@ -204,8 +204,8 @@ export class DbHelper {
|
|
|
204
204
|
/**
|
|
205
205
|
* 清理数据或 where 条件(默认排除 null 和 undefined)
|
|
206
206
|
*/
|
|
207
|
-
|
|
208
|
-
return fieldClear(data || ({} as T), excludeValues, keepValues);
|
|
207
|
+
public cleanFields<T extends Record<string, any>>(data: T, excludeValues: any[] = [null, undefined], keepValues: Record<string, any> = {}): Partial<T> {
|
|
208
|
+
return fieldClear(data || ({} as T), { excludeValues, keepMap: keepValues });
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
/**
|
package/lib/jwt.ts
CHANGED
|
@@ -4,13 +4,28 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { createHmac } from 'crypto';
|
|
7
|
-
import { Env } from '../env.js';
|
|
8
7
|
import type { JwtPayload, JwtSignOptions, JwtVerifyOptions, JwtAlgorithm, JwtHeader, JwtDecoded } from '../types/jwt';
|
|
8
|
+
import type { AuthConfig } from '../types/befly';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* JWT 工具类
|
|
12
12
|
*/
|
|
13
13
|
export class Jwt {
|
|
14
|
+
/** 默认配置 */
|
|
15
|
+
private static config: AuthConfig = {
|
|
16
|
+
secret: 'befly-secret',
|
|
17
|
+
expiresIn: '7d',
|
|
18
|
+
algorithm: 'HS256'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 配置 JWT
|
|
23
|
+
* @param config - JWT 配置
|
|
24
|
+
*/
|
|
25
|
+
static configure(config: AuthConfig) {
|
|
26
|
+
this.config = { ...this.config, ...config };
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
/** 算法映射 */
|
|
15
30
|
private static readonly ALGORITHMS: Record<JwtAlgorithm, string> = {
|
|
16
31
|
HS256: 'sha256',
|
|
@@ -94,23 +109,15 @@ export class Jwt {
|
|
|
94
109
|
}
|
|
95
110
|
|
|
96
111
|
/**
|
|
97
|
-
*
|
|
98
|
-
* @param payload -
|
|
99
|
-
* @param options -
|
|
100
|
-
* @returns
|
|
112
|
+
* 签名生成 Token
|
|
113
|
+
* @param payload - 数据载荷
|
|
114
|
+
* @param options - 签名选项
|
|
115
|
+
* @returns Token 字符串
|
|
101
116
|
*/
|
|
102
|
-
static sign(payload: JwtPayload, options
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const secret = options?.secret || Env.JWT_SECRET;
|
|
108
|
-
if (!secret) {
|
|
109
|
-
throw new Error('JWT密钥未配置');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const opts = options || {};
|
|
113
|
-
const algorithm = (opts.algorithm || Env.JWT_ALGORITHM || 'HS256') as JwtAlgorithm;
|
|
117
|
+
static async sign(payload: JwtPayload, options: JwtSignOptions = {}): Promise<string> {
|
|
118
|
+
const secret = options.secret || this.config.secret || 'befly-secret';
|
|
119
|
+
const expiresIn = options.expiresIn || this.config.expiresIn || '7d';
|
|
120
|
+
const algorithm = (options.algorithm || this.config.algorithm || 'HS256') as JwtAlgorithm;
|
|
114
121
|
|
|
115
122
|
const now = Math.floor(Date.now() / 1000);
|
|
116
123
|
|
|
@@ -125,22 +132,18 @@ export class Jwt {
|
|
|
125
132
|
// 创建 payload
|
|
126
133
|
const jwtPayload: JwtPayload = { ...payload, iat: now };
|
|
127
134
|
|
|
128
|
-
if (
|
|
129
|
-
const expSeconds = Jwt.parseExpiration(
|
|
130
|
-
jwtPayload.exp = now + expSeconds;
|
|
131
|
-
} else {
|
|
132
|
-
// 使用默认过期时间
|
|
133
|
-
const defaultExpiry = Env.JWT_EXPIRES_IN || '7d';
|
|
134
|
-
const expSeconds = Jwt.parseExpiration(defaultExpiry);
|
|
135
|
+
if (expiresIn) {
|
|
136
|
+
const expSeconds = Jwt.parseExpiration(expiresIn);
|
|
135
137
|
jwtPayload.exp = now + expSeconds;
|
|
136
138
|
}
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
if (
|
|
140
|
-
if (
|
|
141
|
-
|
|
139
|
+
|
|
140
|
+
if (options.issuer) jwtPayload.iss = options.issuer;
|
|
141
|
+
if (options.audience) jwtPayload.aud = options.audience;
|
|
142
|
+
if (options.subject) jwtPayload.sub = options.subject;
|
|
143
|
+
if (options.notBefore) {
|
|
144
|
+
jwtPayload.nbf = typeof options.notBefore === 'number' ? options.notBefore : now + Jwt.parseExpiration(options.notBefore);
|
|
142
145
|
}
|
|
143
|
-
if (
|
|
146
|
+
if (options.jwtId) jwtPayload.jti = options.jwtId;
|
|
144
147
|
|
|
145
148
|
const encodedPayload = Jwt.base64UrlEncode(JSON.stringify(jwtPayload));
|
|
146
149
|
|
|
@@ -152,78 +155,66 @@ export class Jwt {
|
|
|
152
155
|
}
|
|
153
156
|
|
|
154
157
|
/**
|
|
155
|
-
* 验证
|
|
156
|
-
* @param token -
|
|
157
|
-
* @param options -
|
|
158
|
-
* @returns
|
|
158
|
+
* 验证 Token
|
|
159
|
+
* @param token - Token 字符串
|
|
160
|
+
* @param options - 验证选项
|
|
161
|
+
* @returns 解码后的载荷
|
|
159
162
|
*/
|
|
160
|
-
static verify(token: string, options
|
|
163
|
+
static async verify<T = JwtPayload>(token: string, options: JwtVerifyOptions = {}): Promise<T> {
|
|
161
164
|
if (!token || typeof token !== 'string') {
|
|
162
165
|
throw new Error('Token必须是非空字符串');
|
|
163
166
|
}
|
|
164
167
|
|
|
165
|
-
const secret = options
|
|
166
|
-
|
|
167
|
-
throw new Error('JWT密钥未配置');
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const opts = options || {};
|
|
168
|
+
const secret = options.secret || this.config.secret || 'befly-secret';
|
|
169
|
+
const algorithm = (options.algorithm || this.config.algorithm || 'HS256') as JwtAlgorithm;
|
|
171
170
|
|
|
172
171
|
const parts = token.split('.');
|
|
173
172
|
if (parts.length !== 3) {
|
|
174
|
-
throw new Error('
|
|
173
|
+
throw new Error('Token 格式无效');
|
|
175
174
|
}
|
|
176
175
|
|
|
177
|
-
|
|
178
|
-
// 解析 header 和 payload
|
|
179
|
-
const header = JSON.parse(Jwt.base64UrlDecode(parts[0])) as JwtHeader;
|
|
180
|
-
const payload = JSON.parse(Jwt.base64UrlDecode(parts[1])) as JwtPayload;
|
|
181
|
-
const signature = parts[2];
|
|
176
|
+
const [headerB64, payloadB64, signature] = parts;
|
|
182
177
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
178
|
+
// 验证签名
|
|
179
|
+
const data = `${headerB64}.${payloadB64}`;
|
|
180
|
+
const expectedSignature = Jwt.createSignature(algorithm, secret, data);
|
|
187
181
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
182
|
+
if (!Jwt.constantTimeCompare(signature, expectedSignature)) {
|
|
183
|
+
throw new Error('Token 签名无效');
|
|
184
|
+
}
|
|
191
185
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
186
|
+
// 解码 payload
|
|
187
|
+
const payloadStr = Jwt.base64UrlDecode(payloadB64);
|
|
188
|
+
let payload: JwtPayload;
|
|
189
|
+
try {
|
|
190
|
+
payload = JSON.parse(payloadStr);
|
|
191
|
+
} catch (e) {
|
|
192
|
+
throw new Error('Token 载荷无效');
|
|
193
|
+
}
|
|
195
194
|
|
|
196
|
-
|
|
195
|
+
// 验证过期时间
|
|
196
|
+
if (!options.ignoreExpiration) {
|
|
197
197
|
const now = Math.floor(Date.now() / 1000);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
throw new Error('Token已过期 (expired)');
|
|
201
|
-
}
|
|
202
|
-
if (!opts.ignoreNotBefore && payload.nbf && payload.nbf > now) {
|
|
203
|
-
throw new Error('Token尚未生效');
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// 验证 issuer、audience、subject
|
|
207
|
-
if (opts.issuer && payload.iss !== opts.issuer) {
|
|
208
|
-
throw new Error('Token发行者无效');
|
|
209
|
-
}
|
|
210
|
-
if (opts.audience) {
|
|
211
|
-
const audiences = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
|
|
212
|
-
if (!audiences.includes(opts.audience)) {
|
|
213
|
-
throw new Error('Token受众无效');
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
if (opts.subject && payload.sub !== opts.subject) {
|
|
217
|
-
throw new Error('Token主题无效');
|
|
198
|
+
if (payload.exp && payload.exp < now) {
|
|
199
|
+
throw new Error('Token 已过期');
|
|
218
200
|
}
|
|
201
|
+
}
|
|
219
202
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
throw error;
|
|
224
|
-
}
|
|
225
|
-
throw new Error('Token验证失败: ' + error.message);
|
|
203
|
+
// 验证其他声明
|
|
204
|
+
if (options.issuer && payload.iss !== options.issuer) {
|
|
205
|
+
throw new Error('Token issuer 无效');
|
|
226
206
|
}
|
|
207
|
+
if (options.audience && payload.aud !== options.audience) {
|
|
208
|
+
throw new Error('Token audience 无效');
|
|
209
|
+
}
|
|
210
|
+
if (options.subject && payload.sub !== options.subject) {
|
|
211
|
+
throw new Error('Token subject 无效');
|
|
212
|
+
}
|
|
213
|
+
if (options.jwtId && payload.jti !== options.jwtId) {
|
|
214
|
+
throw new Error('Token jwtId 无效');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return payload as T;
|
|
227
218
|
}
|
|
228
219
|
|
|
229
220
|
/**
|
|
@@ -341,14 +332,14 @@ export class Jwt {
|
|
|
341
332
|
/**
|
|
342
333
|
* 创建用户 token(快捷方法)
|
|
343
334
|
*/
|
|
344
|
-
static create(payload: JwtPayload): string {
|
|
335
|
+
static async create(payload: JwtPayload): Promise<string> {
|
|
345
336
|
return this.sign(payload);
|
|
346
337
|
}
|
|
347
338
|
|
|
348
339
|
/**
|
|
349
340
|
* 检查 token(快捷方法)
|
|
350
341
|
*/
|
|
351
|
-
static check(token: string): JwtPayload {
|
|
342
|
+
static async check(token: string): Promise<JwtPayload> {
|
|
352
343
|
return this.verify(token);
|
|
353
344
|
}
|
|
354
345
|
|
|
@@ -362,64 +353,64 @@ export class Jwt {
|
|
|
362
353
|
/**
|
|
363
354
|
* 签名用户认证 token
|
|
364
355
|
*/
|
|
365
|
-
static signUserToken(userInfo: JwtPayload, options?: JwtSignOptions): string {
|
|
356
|
+
static async signUserToken(userInfo: JwtPayload, options?: JwtSignOptions): Promise<string> {
|
|
366
357
|
return this.sign(userInfo, options);
|
|
367
358
|
}
|
|
368
359
|
|
|
369
360
|
/**
|
|
370
361
|
* 签名 API 访问 token
|
|
371
362
|
*/
|
|
372
|
-
static signAPIToken(payload: JwtPayload, options?: JwtSignOptions): string {
|
|
363
|
+
static async signAPIToken(payload: JwtPayload, options?: JwtSignOptions): Promise<string> {
|
|
373
364
|
return this.sign(payload, { audience: 'api', expiresIn: '1h', ...options });
|
|
374
365
|
}
|
|
375
366
|
|
|
376
367
|
/**
|
|
377
368
|
* 签名刷新 token
|
|
378
369
|
*/
|
|
379
|
-
static signRefreshToken(payload: JwtPayload, options?: JwtSignOptions): string {
|
|
370
|
+
static async signRefreshToken(payload: JwtPayload, options?: JwtSignOptions): Promise<string> {
|
|
380
371
|
return this.sign(payload, { audience: 'refresh', expiresIn: '30d', ...options });
|
|
381
372
|
}
|
|
382
373
|
|
|
383
374
|
/**
|
|
384
375
|
* 签名临时 token (用于重置密码等)
|
|
385
376
|
*/
|
|
386
|
-
static signTempToken(payload: JwtPayload, options?: JwtSignOptions): string {
|
|
377
|
+
static async signTempToken(payload: JwtPayload, options?: JwtSignOptions): Promise<string> {
|
|
387
378
|
return this.sign(payload, { audience: 'temporary', expiresIn: '15m', ...options });
|
|
388
379
|
}
|
|
389
380
|
|
|
390
381
|
/**
|
|
391
382
|
* 验证用户认证 token
|
|
392
383
|
*/
|
|
393
|
-
static verifyUserToken(token: string, options?: JwtVerifyOptions): JwtPayload {
|
|
384
|
+
static async verifyUserToken(token: string, options?: JwtVerifyOptions): Promise<JwtPayload> {
|
|
394
385
|
return this.verify(token, options);
|
|
395
386
|
}
|
|
396
387
|
|
|
397
388
|
/**
|
|
398
389
|
* 验证 API 访问 token
|
|
399
390
|
*/
|
|
400
|
-
static verifyAPIToken(token: string, options?: JwtVerifyOptions): JwtPayload {
|
|
391
|
+
static async verifyAPIToken(token: string, options?: JwtVerifyOptions): Promise<JwtPayload> {
|
|
401
392
|
return this.verify(token, { audience: 'api', ...options });
|
|
402
393
|
}
|
|
403
394
|
|
|
404
395
|
/**
|
|
405
396
|
* 验证刷新 token
|
|
406
397
|
*/
|
|
407
|
-
static verifyRefreshToken(token: string, options?: JwtVerifyOptions): JwtPayload {
|
|
398
|
+
static async verifyRefreshToken(token: string, options?: JwtVerifyOptions): Promise<JwtPayload> {
|
|
408
399
|
return this.verify(token, { audience: 'refresh', ...options });
|
|
409
400
|
}
|
|
410
401
|
|
|
411
402
|
/**
|
|
412
403
|
* 验证临时 token
|
|
413
404
|
*/
|
|
414
|
-
static verifyTempToken(token: string, options?: JwtVerifyOptions): JwtPayload {
|
|
405
|
+
static async verifyTempToken(token: string, options?: JwtVerifyOptions): Promise<JwtPayload> {
|
|
415
406
|
return this.verify(token, { audience: 'temporary', ...options });
|
|
416
407
|
}
|
|
417
408
|
|
|
418
409
|
/**
|
|
419
410
|
* 验证 token 并检查权限
|
|
420
411
|
*/
|
|
421
|
-
static verifyWithPermissions(token: string, requiredPermissions: string | string[], options?: JwtVerifyOptions): JwtPayload {
|
|
422
|
-
const payload = this.verify(token, options);
|
|
412
|
+
static async verifyWithPermissions(token: string, requiredPermissions: string | string[], options?: JwtVerifyOptions): Promise<JwtPayload> {
|
|
413
|
+
const payload = await this.verify(token, options);
|
|
423
414
|
|
|
424
415
|
if (!payload.permissions) {
|
|
425
416
|
throw new Error('Token中不包含权限信息');
|
|
@@ -438,8 +429,8 @@ export class Jwt {
|
|
|
438
429
|
/**
|
|
439
430
|
* 验证 token 并检查角色
|
|
440
431
|
*/
|
|
441
|
-
static verifyWithRoles(token: string, requiredRoles: string | string[], options?: JwtVerifyOptions): JwtPayload {
|
|
442
|
-
const payload = this.verify(token, options);
|
|
432
|
+
static async verifyWithRoles(token: string, requiredRoles: string | string[], options?: JwtVerifyOptions): Promise<JwtPayload> {
|
|
433
|
+
const payload = await this.verify(token, options);
|
|
443
434
|
|
|
444
435
|
if (!payload.role && !payload.roles) {
|
|
445
436
|
throw new Error('Token中不包含角色信息');
|
|
@@ -460,7 +451,7 @@ export class Jwt {
|
|
|
460
451
|
/**
|
|
461
452
|
* 软验证 token (忽略过期时间)
|
|
462
453
|
*/
|
|
463
|
-
static verifySoft(token: string, options?: JwtVerifyOptions): JwtPayload {
|
|
454
|
+
static async verifySoft(token: string, options?: JwtVerifyOptions): Promise<JwtPayload> {
|
|
464
455
|
return this.verify(token, { ignoreExpiration: true, ...options });
|
|
465
456
|
}
|
|
466
457
|
}
|
package/lib/logger.ts
CHANGED
|
@@ -5,8 +5,19 @@
|
|
|
5
5
|
|
|
6
6
|
import { join } from 'pathe';
|
|
7
7
|
import { appendFile, stat } from 'node:fs/promises';
|
|
8
|
-
import {
|
|
8
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
9
9
|
import type { LogLevel } from '../types/common.js';
|
|
10
|
+
import type { LoggerConfig } from '../types/befly.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 日志上下文存储
|
|
14
|
+
*/
|
|
15
|
+
export interface LogContext {
|
|
16
|
+
requestId?: string;
|
|
17
|
+
[key: string]: any;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const logContextStorage = new AsyncLocalStorage<LogContext>();
|
|
10
21
|
|
|
11
22
|
/**
|
|
12
23
|
* 日志消息类型
|
|
@@ -34,6 +45,23 @@ export class Logger {
|
|
|
34
45
|
/** 当前使用的日志文件缓存 */
|
|
35
46
|
private static currentFiles: Map<string, string> = new Map();
|
|
36
47
|
|
|
48
|
+
/** 日志配置 */
|
|
49
|
+
private static config: LoggerConfig = {
|
|
50
|
+
debug: 1,
|
|
51
|
+
excludeFields: 'password,token,secret',
|
|
52
|
+
dir: './logs',
|
|
53
|
+
console: 1,
|
|
54
|
+
maxSize: 10 * 1024 * 1024
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 配置日志器
|
|
59
|
+
* @param config - 日志配置
|
|
60
|
+
*/
|
|
61
|
+
static configure(config: LoggerConfig) {
|
|
62
|
+
this.config = { ...this.config, ...config };
|
|
63
|
+
}
|
|
64
|
+
|
|
37
65
|
/**
|
|
38
66
|
* 记录日志
|
|
39
67
|
* @param level - 日志级别
|
|
@@ -41,7 +69,7 @@ export class Logger {
|
|
|
41
69
|
*/
|
|
42
70
|
static async log(level: LogLevel, message: LogMessage): Promise<void> {
|
|
43
71
|
// debug 日志特殊处理:仅当 LOG_DEBUG=1 时才记录
|
|
44
|
-
if (level === 'debug' &&
|
|
72
|
+
if (level === 'debug' && this.config.debug !== 1) return;
|
|
45
73
|
|
|
46
74
|
// 格式化消息
|
|
47
75
|
const timestamp = formatDate();
|
|
@@ -56,10 +84,15 @@ export class Logger {
|
|
|
56
84
|
|
|
57
85
|
// 格式化日志消息
|
|
58
86
|
const levelStr = level.toUpperCase().padStart(5);
|
|
59
|
-
|
|
87
|
+
|
|
88
|
+
// 获取上下文中的 requestId
|
|
89
|
+
const store = logContextStorage.getStore();
|
|
90
|
+
const requestId = store?.requestId ? ` [${store.requestId}]` : '';
|
|
91
|
+
|
|
92
|
+
const logMessage = `[${timestamp}]${requestId} ${levelStr} - ${content}`;
|
|
60
93
|
|
|
61
94
|
// 控制台输出
|
|
62
|
-
if (
|
|
95
|
+
if (this.config.console === 1) {
|
|
63
96
|
console.log(logMessage);
|
|
64
97
|
}
|
|
65
98
|
|
|
@@ -82,6 +115,7 @@ export class Logger {
|
|
|
82
115
|
*/
|
|
83
116
|
static async writeToFile(message: string, level: LogLevel = 'info'): Promise<void> {
|
|
84
117
|
try {
|
|
118
|
+
const logDir = this.config.dir || './logs';
|
|
85
119
|
// 确定文件前缀
|
|
86
120
|
const prefix = level === 'debug' ? 'debug' : new Date().toISOString().split('T')[0];
|
|
87
121
|
|
|
@@ -91,7 +125,7 @@ export class Logger {
|
|
|
91
125
|
if (currentLogFile) {
|
|
92
126
|
try {
|
|
93
127
|
const stats = await stat(currentLogFile);
|
|
94
|
-
if (stats.size >=
|
|
128
|
+
if (stats.size >= (this.config.maxSize || 10 * 1024 * 1024)) {
|
|
95
129
|
this.currentFiles.delete(prefix);
|
|
96
130
|
currentLogFile = undefined;
|
|
97
131
|
}
|
|
@@ -104,7 +138,7 @@ export class Logger {
|
|
|
104
138
|
// 查找或创建新文件
|
|
105
139
|
if (!currentLogFile) {
|
|
106
140
|
const glob = new Bun.Glob(`${prefix}.*.log`);
|
|
107
|
-
const files = await Array.fromAsync(glob.scan(
|
|
141
|
+
const files = await Array.fromAsync(glob.scan(this.config.dir || 'logs'));
|
|
108
142
|
|
|
109
143
|
// 按索引排序并查找可用文件
|
|
110
144
|
const getIndex = (f: string) => parseInt(f.match(/\.(\d+)\.log$/)?.[1] || '0');
|
|
@@ -112,10 +146,11 @@ export class Logger {
|
|
|
112
146
|
|
|
113
147
|
let foundFile = false;
|
|
114
148
|
for (let i = files.length - 1; i >= 0; i--) {
|
|
115
|
-
const filePath = join(
|
|
149
|
+
const filePath = join(this.config.dir || 'logs', files[i]);
|
|
116
150
|
try {
|
|
117
151
|
const stats = await stat(filePath);
|
|
118
|
-
|
|
152
|
+
// 检查文件大小
|
|
153
|
+
if (stats.size < (this.config.maxSize || 10 * 1024 * 1024)) {
|
|
119
154
|
currentLogFile = filePath;
|
|
120
155
|
foundFile = true;
|
|
121
156
|
break;
|
|
@@ -128,7 +163,7 @@ export class Logger {
|
|
|
128
163
|
// 没有可用文件,创建新文件
|
|
129
164
|
if (!foundFile) {
|
|
130
165
|
const maxIndex = files.length > 0 ? Math.max(...files.map(getIndex)) : -1;
|
|
131
|
-
currentLogFile = join(
|
|
166
|
+
currentLogFile = join(this.config.dir || 'logs', `${prefix}.${maxIndex + 1}.log`);
|
|
132
167
|
}
|
|
133
168
|
|
|
134
169
|
this.currentFiles.set(prefix, currentLogFile);
|
|
@@ -194,18 +229,4 @@ export class Logger {
|
|
|
194
229
|
static clearCache(): void {
|
|
195
230
|
this.currentFiles.clear();
|
|
196
231
|
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* 打印当前运行环境
|
|
200
|
-
* 用于命令开始时提示用户当前环境
|
|
201
|
-
*/
|
|
202
|
-
static printEnv(): void {
|
|
203
|
-
console.log('========================================');
|
|
204
|
-
console.log('开始执行完整同步流程');
|
|
205
|
-
console.log(`当前环境: ${Env.NODE_ENV || 'development'}`);
|
|
206
|
-
console.log(`项目名称: ${Env.APP_NAME}`);
|
|
207
|
-
console.log(`数据库地址: ${Env.DB_HOST}`);
|
|
208
|
-
console.log(`数据库名称: ${Env.DB_NAME}`);
|
|
209
|
-
console.log('========================================\n');
|
|
210
|
-
}
|
|
211
232
|
}
|