@fuzionx/framework 0.1.38 → 0.1.41
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 +501 -501
- package/bin/fx.js +12 -12
- package/cli/db-sync.js +99 -99
- package/cli/index.js +493 -493
- package/cli/templates/make/app/controllers/HomeController.js +14 -14
- package/cli/templates/make/app/routes/api.js +7 -7
- package/cli/templates/make/app/routes/web.js +5 -5
- package/cli/templates/make/app/views/default/errors/404.html +11 -11
- package/cli/templates/make/app/views/default/errors/500.html +14 -14
- package/cli/templates/make/app/views/default/layouts/main.html +22 -22
- package/cli/templates/make/app/views/default/pages/home.html +11 -11
- package/cli/templates/make/controller.js.tpl +40 -40
- package/cli/templates/make/event.js.tpl +8 -8
- package/cli/templates/make/job.js.tpl +10 -10
- package/cli/templates/make/middleware.js.tpl +10 -10
- package/cli/templates/make/model.js.tpl +15 -15
- package/cli/templates/make/service.js.tpl +15 -15
- package/cli/templates/make/task.js.tpl +15 -15
- package/cli/templates/make/test.js.tpl +7 -7
- package/cli/templates/make/worker.js.tpl +14 -14
- package/cli/templates/make/ws.js.tpl +18 -18
- package/index.js +67 -67
- package/lib/core/AppError.js +46 -46
- package/lib/core/Application.js +1006 -998
- package/lib/core/AutoLoader.js +226 -226
- package/lib/core/Base.js +64 -64
- package/lib/core/Config.js +228 -228
- package/lib/core/Context.js +484 -460
- package/lib/database/ConnectionManager.js +208 -208
- package/lib/database/MariaModel.js +29 -29
- package/lib/database/Model.js +247 -247
- package/lib/database/ModelRegistry.js +72 -72
- package/lib/database/MongoModel.js +232 -232
- package/lib/database/Pagination.js +37 -37
- package/lib/database/PostgreModel.js +29 -29
- package/lib/database/QueryBuilder.js +172 -172
- package/lib/database/SQLiteModel.js +27 -27
- package/lib/database/SqlModel.js +257 -257
- package/lib/database/SqlQueryBuilder.js +332 -321
- package/lib/helpers/CryptoHelper.js +48 -48
- package/lib/helpers/FileHelper.js +61 -61
- package/lib/helpers/HashHelper.js +39 -39
- package/lib/helpers/I18nHelper.js +174 -174
- package/lib/helpers/Logger.js +108 -105
- package/lib/helpers/MediaHelper.js +84 -84
- package/lib/http/Controller.js +34 -34
- package/lib/http/ErrorHandler.js +136 -136
- package/lib/http/Middleware.js +43 -43
- package/lib/http/Router.js +109 -109
- package/lib/http/Validation.js +125 -124
- package/lib/middleware/apiAuth.js +79 -79
- package/lib/middleware/auth.js +42 -42
- package/lib/middleware/bodyParser.js +19 -19
- package/lib/middleware/cors.js +47 -47
- package/lib/middleware/csrf.js +32 -32
- package/lib/middleware/index.js +13 -13
- package/lib/middleware/session.js +27 -27
- package/lib/middleware/theme.js +20 -20
- package/lib/realtime/RoomManager.js +85 -85
- package/lib/realtime/WsHandler.js +107 -107
- package/lib/schedule/Job.js +38 -38
- package/lib/schedule/Queue.js +103 -102
- package/lib/schedule/Scheduler.js +171 -170
- package/lib/schedule/Task.js +39 -39
- package/lib/schedule/WorkerPool.js +225 -225
- package/lib/services/EventBus.js +94 -94
- package/lib/services/Service.js +261 -261
- package/lib/services/Storage.js +112 -112
- package/lib/utilities/ArrUtil.js +112 -112
- package/lib/utilities/DateUtil.js +98 -98
- package/lib/utilities/FunctionUtil.js +119 -119
- package/lib/utilities/NumUtil.js +75 -75
- package/lib/utilities/ObjectUtil.js +170 -170
- package/lib/utilities/PaginationUtil.js +81 -81
- package/lib/utilities/StrUtil.js +105 -105
- package/lib/utilities/index.js +18 -18
- package/lib/view/OpenAPI.js +231 -231
- package/lib/view/View.js +83 -83
- package/package.json +2 -2
- package/testing/index.js +232 -232
- package/cli/fx.js +0 -3
package/lib/http/Validation.js
CHANGED
|
@@ -1,124 +1,125 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validation — 간단문법 → Joi 변환 파서
|
|
3
|
-
*
|
|
4
|
-
* 'required|min:2|max:50' → Joi.string().required().min(2).max(50)
|
|
5
|
-
*
|
|
6
|
-
* @see docs/framework/09-validation.md
|
|
7
|
-
*/
|
|
8
|
-
import { ValidationError } from '../core/AppError.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 간단 규칙 문자열을 Joi 스키마로 변환
|
|
12
|
-
* @param {object} rules - { name: 'required|min:2', email: 'required|email' }
|
|
13
|
-
* @param {object} Joi - Joi 모듈 참조
|
|
14
|
-
* @returns {import('joi').ObjectSchema}
|
|
15
|
-
*/
|
|
16
|
-
export function parseRules(rules, Joi) {
|
|
17
|
-
const shape = {};
|
|
18
|
-
|
|
19
|
-
for (const [field, rule] of Object.entries(rules)) {
|
|
20
|
-
const parts = typeof rule === 'string' ? rule.split('|') : [];
|
|
21
|
-
let schema = Joi.any();
|
|
22
|
-
|
|
23
|
-
for (const part of parts) {
|
|
24
|
-
const [name, ...args] = part.split(':');
|
|
25
|
-
|
|
26
|
-
switch (name) {
|
|
27
|
-
case 'string':
|
|
28
|
-
schema = Joi.string(); break;
|
|
29
|
-
case 'number':
|
|
30
|
-
schema = Joi.number(); break;
|
|
31
|
-
case 'boolean':
|
|
32
|
-
schema = Joi.boolean(); break;
|
|
33
|
-
case 'required':
|
|
34
|
-
schema = schema.required(); break;
|
|
35
|
-
case 'optional':
|
|
36
|
-
schema = schema.optional(); break;
|
|
37
|
-
case 'min':
|
|
38
|
-
schema = schema.min(Number(args[0])); break;
|
|
39
|
-
case 'max':
|
|
40
|
-
schema = schema.max(Number(args[0])); break;
|
|
41
|
-
case 'email':
|
|
42
|
-
schema
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
*
|
|
81
|
-
* @param {object}
|
|
82
|
-
* @
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Validation — 간단문법 → Joi 변환 파서
|
|
3
|
+
*
|
|
4
|
+
* 'required|min:2|max:50' → Joi.string().required().min(2).max(50)
|
|
5
|
+
*
|
|
6
|
+
* @see docs/framework/09-validation.md
|
|
7
|
+
*/
|
|
8
|
+
import { ValidationError } from '../core/AppError.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 간단 규칙 문자열을 Joi 스키마로 변환
|
|
12
|
+
* @param {object} rules - { name: 'required|min:2', email: 'required|email' }
|
|
13
|
+
* @param {object} Joi - Joi 모듈 참조
|
|
14
|
+
* @returns {import('joi').ObjectSchema}
|
|
15
|
+
*/
|
|
16
|
+
export function parseRules(rules, Joi) {
|
|
17
|
+
const shape = {};
|
|
18
|
+
|
|
19
|
+
for (const [field, rule] of Object.entries(rules)) {
|
|
20
|
+
const parts = typeof rule === 'string' ? rule.split('|') : [];
|
|
21
|
+
let schema = Joi.any();
|
|
22
|
+
|
|
23
|
+
for (const part of parts) {
|
|
24
|
+
const [name, ...args] = part.split(':');
|
|
25
|
+
|
|
26
|
+
switch (name) {
|
|
27
|
+
case 'string':
|
|
28
|
+
schema = Joi.string(); break;
|
|
29
|
+
case 'number':
|
|
30
|
+
schema = Joi.number(); break;
|
|
31
|
+
case 'boolean':
|
|
32
|
+
schema = Joi.boolean(); break;
|
|
33
|
+
case 'required':
|
|
34
|
+
schema = schema.required(); break;
|
|
35
|
+
case 'optional':
|
|
36
|
+
schema = schema.optional(); break;
|
|
37
|
+
case 'min':
|
|
38
|
+
schema = schema.min(Number(args[0])); break;
|
|
39
|
+
case 'max':
|
|
40
|
+
schema = schema.max(Number(args[0])); break;
|
|
41
|
+
case 'email':
|
|
42
|
+
// 기존 schema가 Joi.any()면 string()으로 교체, 이미 string()이면 유지
|
|
43
|
+
schema = (schema.type === 'any' ? Joi.string() : schema).email(); break;
|
|
44
|
+
case 'url':
|
|
45
|
+
schema = (schema.type === 'any' ? Joi.string() : schema).uri(); break;
|
|
46
|
+
case 'in':
|
|
47
|
+
schema = schema.valid(...args[0].split(',')); break;
|
|
48
|
+
case 'integer':
|
|
49
|
+
schema = schema.integer ? schema.integer() : Joi.number().integer(); break;
|
|
50
|
+
case 'positive':
|
|
51
|
+
schema = schema.positive ? schema.positive() : Joi.number().positive(); break;
|
|
52
|
+
case 'date':
|
|
53
|
+
schema = schema.type === 'any' ? Joi.date() : schema; break;
|
|
54
|
+
case 'alpha':
|
|
55
|
+
schema = (schema.type === 'any' ? Joi.string() : schema).alphanum(); break;
|
|
56
|
+
default:
|
|
57
|
+
// 알 수 없는 규칙은 무시
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
shape[field] = schema;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return Joi.object(shape);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validation 클래스 — 라우트 검증 실행기
|
|
70
|
+
*/
|
|
71
|
+
export default class Validation {
|
|
72
|
+
/**
|
|
73
|
+
* @param {object} Joi - Joi 인스턴스
|
|
74
|
+
*/
|
|
75
|
+
constructor(Joi) {
|
|
76
|
+
this._Joi = Joi;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 라우트 validate 옵션 실행
|
|
81
|
+
* @param {object} ctx
|
|
82
|
+
* @param {object} validateOpts - { body: schema, query: schema, params: schema }
|
|
83
|
+
* @returns {object} - { body, query, params } 정제된 값
|
|
84
|
+
*/
|
|
85
|
+
run(ctx, validateOpts) {
|
|
86
|
+
const result = {};
|
|
87
|
+
|
|
88
|
+
if (validateOpts.body) {
|
|
89
|
+
result.body = this._validate(ctx.body, validateOpts.body);
|
|
90
|
+
}
|
|
91
|
+
if (validateOpts.query) {
|
|
92
|
+
result.query = this._validate(ctx.query, validateOpts.query);
|
|
93
|
+
}
|
|
94
|
+
if (validateOpts.params) {
|
|
95
|
+
result.params = this._validate(ctx.params, validateOpts.params);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @private
|
|
103
|
+
*/
|
|
104
|
+
_validate(data, schema) {
|
|
105
|
+
// 간단문법 객체 → Joi 변환
|
|
106
|
+
if (schema && typeof schema === 'object' && !schema.validate) {
|
|
107
|
+
schema = parseRules(schema, this._Joi);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const { error, value } = schema.validate(data, {
|
|
111
|
+
abortEarly: false,
|
|
112
|
+
stripUnknown: true,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (error) {
|
|
116
|
+
const fields = {};
|
|
117
|
+
for (const detail of error.details) {
|
|
118
|
+
fields[detail.path.join('.')] = detail.message;
|
|
119
|
+
}
|
|
120
|
+
throw new ValidationError('Validation failed', fields);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* apiAuth — JWT Bearer 토큰 인증 미들웨어
|
|
3
|
-
*
|
|
4
|
-
* Authorization: Bearer <token> → 검증 → ctx.user
|
|
5
|
-
*
|
|
6
|
-
* @see docs/framework/14-authentication.md
|
|
7
|
-
*
|
|
8
|
-
* @param {object} [opts]
|
|
9
|
-
* @param {string} [opts.secret] - JWT 시크릿
|
|
10
|
-
* @param {string} [opts.model='User']
|
|
11
|
-
*/
|
|
12
|
-
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
13
|
-
|
|
14
|
-
export function apiAuth(opts = {}) {
|
|
15
|
-
const modelName = opts.model || 'User';
|
|
16
|
-
|
|
17
|
-
return async (ctx, next) => {
|
|
18
|
-
const authHeader = ctx.get('authorization') || '';
|
|
19
|
-
|
|
20
|
-
if (!authHeader.startsWith('Bearer ')) {
|
|
21
|
-
ctx.status(401).json({ error: { message: 'Token required', status: 401 } });
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const token = authHeader.slice(7);
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
const secret = opts.secret || ctx.app?.config?.get('app.auth.secret', 'fuzionx');
|
|
29
|
-
const payload = decodeJwtPayload(token, secret);
|
|
30
|
-
|
|
31
|
-
if (!payload || !payload.sub) {
|
|
32
|
-
ctx.status(401).json({ error: { message: 'Invalid token', status: 401 } });
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (ctx.app?.db?.[modelName]) {
|
|
37
|
-
ctx.user = await ctx.app.db[modelName].find(payload.sub);
|
|
38
|
-
} else {
|
|
39
|
-
ctx.user = { id: payload.sub, ...payload };
|
|
40
|
-
}
|
|
41
|
-
} catch {
|
|
42
|
-
ctx.status(401).json({ error: { message: 'Invalid token', status: 401 } });
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
await next();
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* JWT 디코드 + HMAC-SHA256 서명 검증
|
|
52
|
-
* @private
|
|
53
|
-
*/
|
|
54
|
-
function decodeJwtPayload(token, secret) {
|
|
55
|
-
try {
|
|
56
|
-
const parts = token.split('.');
|
|
57
|
-
if (parts.length !== 3) return null;
|
|
58
|
-
|
|
59
|
-
const [headerB64, payloadB64, signatureB64] = parts;
|
|
60
|
-
|
|
61
|
-
const signingInput = `${headerB64}.${payloadB64}`;
|
|
62
|
-
const expectedSig = createHmac('sha256', secret)
|
|
63
|
-
.update(signingInput)
|
|
64
|
-
.digest();
|
|
65
|
-
|
|
66
|
-
const actualSig = Buffer.from(signatureB64, 'base64url');
|
|
67
|
-
if (expectedSig.length !== actualSig.length) return null;
|
|
68
|
-
if (!timingSafeEqual(expectedSig, actualSig)) return null;
|
|
69
|
-
|
|
70
|
-
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
|
|
71
|
-
|
|
72
|
-
if (payload.exp !== undefined && Date.now() / 1000 > payload.exp) return null;
|
|
73
|
-
if (payload.nbf !== undefined && Date.now() / 1000 < payload.nbf) return null;
|
|
74
|
-
|
|
75
|
-
return payload;
|
|
76
|
-
} catch {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* apiAuth — JWT Bearer 토큰 인증 미들웨어
|
|
3
|
+
*
|
|
4
|
+
* Authorization: Bearer <token> → 검증 → ctx.user
|
|
5
|
+
*
|
|
6
|
+
* @see docs/framework/14-authentication.md
|
|
7
|
+
*
|
|
8
|
+
* @param {object} [opts]
|
|
9
|
+
* @param {string} [opts.secret] - JWT 시크릿
|
|
10
|
+
* @param {string} [opts.model='User']
|
|
11
|
+
*/
|
|
12
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
13
|
+
|
|
14
|
+
export function apiAuth(opts = {}) {
|
|
15
|
+
const modelName = opts.model || 'User';
|
|
16
|
+
|
|
17
|
+
return async (ctx, next) => {
|
|
18
|
+
const authHeader = ctx.get('authorization') || '';
|
|
19
|
+
|
|
20
|
+
if (!authHeader.startsWith('Bearer ')) {
|
|
21
|
+
ctx.status(401).json({ error: { message: 'Token required', status: 401 } });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const token = authHeader.slice(7);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const secret = opts.secret || ctx.app?.config?.get('app.auth.secret', 'fuzionx');
|
|
29
|
+
const payload = decodeJwtPayload(token, secret);
|
|
30
|
+
|
|
31
|
+
if (!payload || !payload.sub) {
|
|
32
|
+
ctx.status(401).json({ error: { message: 'Invalid token', status: 401 } });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (ctx.app?.db?.[modelName]) {
|
|
37
|
+
ctx.user = await ctx.app.db[modelName].find(payload.sub);
|
|
38
|
+
} else {
|
|
39
|
+
ctx.user = { id: payload.sub, ...payload };
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
ctx.status(401).json({ error: { message: 'Invalid token', status: 401 } });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await next();
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* JWT 디코드 + HMAC-SHA256 서명 검증
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
54
|
+
function decodeJwtPayload(token, secret) {
|
|
55
|
+
try {
|
|
56
|
+
const parts = token.split('.');
|
|
57
|
+
if (parts.length !== 3) return null;
|
|
58
|
+
|
|
59
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
60
|
+
|
|
61
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
62
|
+
const expectedSig = createHmac('sha256', secret)
|
|
63
|
+
.update(signingInput)
|
|
64
|
+
.digest();
|
|
65
|
+
|
|
66
|
+
const actualSig = Buffer.from(signatureB64, 'base64url');
|
|
67
|
+
if (expectedSig.length !== actualSig.length) return null;
|
|
68
|
+
if (!timingSafeEqual(expectedSig, actualSig)) return null;
|
|
69
|
+
|
|
70
|
+
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
|
|
71
|
+
|
|
72
|
+
if (payload.exp !== undefined && Date.now() / 1000 > payload.exp) return null;
|
|
73
|
+
if (payload.nbf !== undefined && Date.now() / 1000 < payload.nbf) return null;
|
|
74
|
+
|
|
75
|
+
return payload;
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
package/lib/middleware/auth.js
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* auth — 세션 인증 미들웨어
|
|
3
|
-
*
|
|
4
|
-
* ctx.session.userId → db.User.find() → ctx.user
|
|
5
|
-
*
|
|
6
|
-
* @see docs/framework/14-authentication.md
|
|
7
|
-
*
|
|
8
|
-
* @param {object} [opts]
|
|
9
|
-
* @param {string} [opts.sessionKey='userId']
|
|
10
|
-
* @param {string} [opts.model='User']
|
|
11
|
-
* @param {string} [opts.redirectTo='/login']
|
|
12
|
-
*/
|
|
13
|
-
export function auth(opts = {}) {
|
|
14
|
-
const sessionKey = opts.sessionKey || 'userId';
|
|
15
|
-
const modelName = opts.model || 'User';
|
|
16
|
-
const redirectTo = opts.redirectTo || null;
|
|
17
|
-
|
|
18
|
-
return async (ctx, next) => {
|
|
19
|
-
const userId = ctx._rawSession?.[sessionKey] || ctx.get('x-test-user-id');
|
|
20
|
-
|
|
21
|
-
if (!userId) {
|
|
22
|
-
if (redirectTo) {
|
|
23
|
-
ctx.redirect(redirectTo);
|
|
24
|
-
} else {
|
|
25
|
-
ctx.status(401).json({ error: { message: 'Unauthorized', status: 401 } });
|
|
26
|
-
}
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (ctx.app?.db?.[modelName]) {
|
|
31
|
-
try {
|
|
32
|
-
ctx.user = await ctx.app.db[modelName].find(userId);
|
|
33
|
-
} catch {
|
|
34
|
-
ctx.user = { id: userId };
|
|
35
|
-
}
|
|
36
|
-
} else {
|
|
37
|
-
ctx.user = { id: userId };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
await next();
|
|
41
|
-
};
|
|
42
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* auth — 세션 인증 미들웨어
|
|
3
|
+
*
|
|
4
|
+
* ctx.session.userId → db.User.find() → ctx.user
|
|
5
|
+
*
|
|
6
|
+
* @see docs/framework/14-authentication.md
|
|
7
|
+
*
|
|
8
|
+
* @param {object} [opts]
|
|
9
|
+
* @param {string} [opts.sessionKey='userId']
|
|
10
|
+
* @param {string} [opts.model='User']
|
|
11
|
+
* @param {string} [opts.redirectTo='/login']
|
|
12
|
+
*/
|
|
13
|
+
export function auth(opts = {}) {
|
|
14
|
+
const sessionKey = opts.sessionKey || 'userId';
|
|
15
|
+
const modelName = opts.model || 'User';
|
|
16
|
+
const redirectTo = opts.redirectTo || null;
|
|
17
|
+
|
|
18
|
+
return async (ctx, next) => {
|
|
19
|
+
const userId = ctx._rawSession?.[sessionKey] || ctx.get('x-test-user-id');
|
|
20
|
+
|
|
21
|
+
if (!userId) {
|
|
22
|
+
if (redirectTo) {
|
|
23
|
+
ctx.redirect(redirectTo);
|
|
24
|
+
} else {
|
|
25
|
+
ctx.status(401).json({ error: { message: 'Unauthorized', status: 401 } });
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (ctx.app?.db?.[modelName]) {
|
|
31
|
+
try {
|
|
32
|
+
ctx.user = await ctx.app.db[modelName].find(userId);
|
|
33
|
+
} catch {
|
|
34
|
+
ctx.user = { id: userId };
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
ctx.user = { id: userId };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await next();
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* bodyParser — JSON/Form body 파싱 미들웨어
|
|
3
|
-
*
|
|
4
|
-
* Bridge가 이미 파싱한 body를 보장.
|
|
5
|
-
* 추가 파싱이 필요한 경우 처리.
|
|
6
|
-
*
|
|
7
|
-
* @see docs/framework/12-middleware.md
|
|
8
|
-
*/
|
|
9
|
-
export function bodyParser() {
|
|
10
|
-
return async (ctx, next) => {
|
|
11
|
-
if (typeof ctx.body === 'string' && ctx.body) {
|
|
12
|
-
const ct = ctx.get('content-type') || '';
|
|
13
|
-
if (ct.includes('application/json')) {
|
|
14
|
-
try { ctx.body = JSON.parse(ctx.body); } catch {}
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
await next();
|
|
18
|
-
};
|
|
19
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* bodyParser — JSON/Form body 파싱 미들웨어
|
|
3
|
+
*
|
|
4
|
+
* Bridge가 이미 파싱한 body를 보장.
|
|
5
|
+
* 추가 파싱이 필요한 경우 처리.
|
|
6
|
+
*
|
|
7
|
+
* @see docs/framework/12-middleware.md
|
|
8
|
+
*/
|
|
9
|
+
export function bodyParser() {
|
|
10
|
+
return async (ctx, next) => {
|
|
11
|
+
if (typeof ctx.body === 'string' && ctx.body) {
|
|
12
|
+
const ct = ctx.get('content-type') || '';
|
|
13
|
+
if (ct.includes('application/json')) {
|
|
14
|
+
try { ctx.body = JSON.parse(ctx.body); } catch {}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
await next();
|
|
18
|
+
};
|
|
19
|
+
}
|
package/lib/middleware/cors.js
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cors — CORS 미들웨어
|
|
3
|
-
*
|
|
4
|
-
* @see docs/framework/12-middleware.md
|
|
5
|
-
*
|
|
6
|
-
* @param {object} [opts]
|
|
7
|
-
* @param {string|string[]} [opts.origin='*']
|
|
8
|
-
* @param {string} [opts.methods='GET,POST,PUT,PATCH,DELETE,OPTIONS']
|
|
9
|
-
* @param {string} [opts.headers='Content-Type,Authorization']
|
|
10
|
-
* @param {boolean} [opts.credentials=false]
|
|
11
|
-
* @param {number} [opts.maxAge=86400]
|
|
12
|
-
*/
|
|
13
|
-
export function cors(opts = {}) {
|
|
14
|
-
const origin = opts.origin || '*';
|
|
15
|
-
const methods = opts.methods || 'GET,POST,PUT,PATCH,DELETE,OPTIONS';
|
|
16
|
-
const headers = opts.headers || 'Content-Type,Authorization';
|
|
17
|
-
const credentials = opts.credentials || false;
|
|
18
|
-
const maxAge = opts.maxAge || 86400;
|
|
19
|
-
|
|
20
|
-
return async (ctx, next) => {
|
|
21
|
-
const reqOrigin = ctx.get('origin') || '*';
|
|
22
|
-
let allowOrigin;
|
|
23
|
-
|
|
24
|
-
if (credentials && origin === '*') {
|
|
25
|
-
allowOrigin = reqOrigin !== '*' ? reqOrigin : '';
|
|
26
|
-
} else {
|
|
27
|
-
allowOrigin = origin === '*' ? '*' : (
|
|
28
|
-
Array.isArray(origin)
|
|
29
|
-
? (origin.includes(reqOrigin) ? reqOrigin : origin[0])
|
|
30
|
-
: origin
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
ctx.header('Access-Control-Allow-Origin', allowOrigin);
|
|
35
|
-
ctx.header('Access-Control-Allow-Methods', methods);
|
|
36
|
-
ctx.header('Access-Control-Allow-Headers', headers);
|
|
37
|
-
if (credentials) ctx.header('Access-Control-Allow-Credentials', 'true');
|
|
38
|
-
|
|
39
|
-
if (ctx.method === 'OPTIONS') {
|
|
40
|
-
ctx.header('Access-Control-Max-Age', String(maxAge));
|
|
41
|
-
ctx.status(204).end();
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
await next();
|
|
46
|
-
};
|
|
47
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* cors — CORS 미들웨어
|
|
3
|
+
*
|
|
4
|
+
* @see docs/framework/12-middleware.md
|
|
5
|
+
*
|
|
6
|
+
* @param {object} [opts]
|
|
7
|
+
* @param {string|string[]} [opts.origin='*']
|
|
8
|
+
* @param {string} [opts.methods='GET,POST,PUT,PATCH,DELETE,OPTIONS']
|
|
9
|
+
* @param {string} [opts.headers='Content-Type,Authorization']
|
|
10
|
+
* @param {boolean} [opts.credentials=false]
|
|
11
|
+
* @param {number} [opts.maxAge=86400]
|
|
12
|
+
*/
|
|
13
|
+
export function cors(opts = {}) {
|
|
14
|
+
const origin = opts.origin || '*';
|
|
15
|
+
const methods = opts.methods || 'GET,POST,PUT,PATCH,DELETE,OPTIONS';
|
|
16
|
+
const headers = opts.headers || 'Content-Type,Authorization';
|
|
17
|
+
const credentials = opts.credentials || false;
|
|
18
|
+
const maxAge = opts.maxAge || 86400;
|
|
19
|
+
|
|
20
|
+
return async (ctx, next) => {
|
|
21
|
+
const reqOrigin = ctx.get('origin') || '*';
|
|
22
|
+
let allowOrigin;
|
|
23
|
+
|
|
24
|
+
if (credentials && origin === '*') {
|
|
25
|
+
allowOrigin = reqOrigin !== '*' ? reqOrigin : '';
|
|
26
|
+
} else {
|
|
27
|
+
allowOrigin = origin === '*' ? '*' : (
|
|
28
|
+
Array.isArray(origin)
|
|
29
|
+
? (origin.includes(reqOrigin) ? reqOrigin : origin[0])
|
|
30
|
+
: origin
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
ctx.header('Access-Control-Allow-Origin', allowOrigin);
|
|
35
|
+
ctx.header('Access-Control-Allow-Methods', methods);
|
|
36
|
+
ctx.header('Access-Control-Allow-Headers', headers);
|
|
37
|
+
if (credentials) ctx.header('Access-Control-Allow-Credentials', 'true');
|
|
38
|
+
|
|
39
|
+
if (ctx.method === 'OPTIONS') {
|
|
40
|
+
ctx.header('Access-Control-Max-Age', String(maxAge));
|
|
41
|
+
ctx.status(204).end();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await next();
|
|
46
|
+
};
|
|
47
|
+
}
|