@fuzionx/framework 0.1.42 → 0.1.44

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.
Files changed (80) hide show
  1. package/README.md +501 -501
  2. package/bin/fx.js +12 -12
  3. package/cli/db-sync.js +100 -100
  4. package/cli/index.js +494 -494
  5. package/cli/templates/make/app/controllers/HomeController.js +14 -14
  6. package/cli/templates/make/app/routes/api.js +7 -7
  7. package/cli/templates/make/app/routes/web.js +5 -5
  8. package/cli/templates/make/app/views/default/errors/404.html +11 -11
  9. package/cli/templates/make/app/views/default/errors/500.html +14 -14
  10. package/cli/templates/make/app/views/default/layouts/main.html +22 -22
  11. package/cli/templates/make/app/views/default/pages/home.html +11 -11
  12. package/cli/templates/make/controller.js.tpl +40 -40
  13. package/cli/templates/make/event.js.tpl +8 -8
  14. package/cli/templates/make/job.js.tpl +10 -10
  15. package/cli/templates/make/middleware.js.tpl +10 -10
  16. package/cli/templates/make/model.js.tpl +15 -15
  17. package/cli/templates/make/service.js.tpl +15 -15
  18. package/cli/templates/make/task.js.tpl +15 -15
  19. package/cli/templates/make/test.js.tpl +7 -7
  20. package/cli/templates/make/worker.js.tpl +14 -14
  21. package/cli/templates/make/ws.js.tpl +18 -18
  22. package/index.js +67 -67
  23. package/lib/core/AppError.js +46 -46
  24. package/lib/core/Application.js +1006 -1006
  25. package/lib/core/AutoLoader.js +227 -227
  26. package/lib/core/Base.js +64 -64
  27. package/lib/core/Config.js +331 -228
  28. package/lib/core/Context.js +484 -484
  29. package/lib/database/ConnectionManager.js +208 -208
  30. package/lib/database/MariaModel.js +29 -29
  31. package/lib/database/Model.js +247 -247
  32. package/lib/database/ModelRegistry.js +72 -72
  33. package/lib/database/MongoModel.js +232 -232
  34. package/lib/database/Pagination.js +37 -37
  35. package/lib/database/PostgreModel.js +29 -29
  36. package/lib/database/QueryBuilder.js +172 -172
  37. package/lib/database/SQLiteModel.js +27 -27
  38. package/lib/database/SqlModel.js +257 -257
  39. package/lib/database/SqlQueryBuilder.js +332 -332
  40. package/lib/helpers/CryptoHelper.js +48 -48
  41. package/lib/helpers/FileHelper.js +61 -61
  42. package/lib/helpers/HashHelper.js +39 -39
  43. package/lib/helpers/I18nHelper.js +174 -174
  44. package/lib/helpers/Logger.js +108 -108
  45. package/lib/helpers/MediaHelper.js +84 -84
  46. package/lib/http/Controller.js +34 -34
  47. package/lib/http/ErrorHandler.js +136 -136
  48. package/lib/http/Middleware.js +43 -43
  49. package/lib/http/Router.js +109 -109
  50. package/lib/http/Validation.js +125 -125
  51. package/lib/middleware/apiAuth.js +79 -79
  52. package/lib/middleware/auth.js +42 -42
  53. package/lib/middleware/bodyParser.js +19 -19
  54. package/lib/middleware/cors.js +47 -47
  55. package/lib/middleware/csrf.js +32 -32
  56. package/lib/middleware/index.js +13 -13
  57. package/lib/middleware/session.js +27 -27
  58. package/lib/middleware/theme.js +20 -20
  59. package/lib/realtime/RoomManager.js +85 -85
  60. package/lib/realtime/WsHandler.js +107 -107
  61. package/lib/schedule/Job.js +38 -38
  62. package/lib/schedule/Queue.js +103 -103
  63. package/lib/schedule/Scheduler.js +171 -171
  64. package/lib/schedule/Task.js +39 -39
  65. package/lib/schedule/WorkerPool.js +225 -225
  66. package/lib/services/EventBus.js +94 -94
  67. package/lib/services/Service.js +261 -261
  68. package/lib/services/Storage.js +112 -112
  69. package/lib/utilities/ArrUtil.js +112 -112
  70. package/lib/utilities/DateUtil.js +98 -98
  71. package/lib/utilities/FunctionUtil.js +119 -119
  72. package/lib/utilities/NumUtil.js +75 -75
  73. package/lib/utilities/ObjectUtil.js +170 -170
  74. package/lib/utilities/PaginationUtil.js +81 -81
  75. package/lib/utilities/StrUtil.js +105 -105
  76. package/lib/utilities/index.js +18 -18
  77. package/lib/view/OpenAPI.js +231 -231
  78. package/lib/view/View.js +83 -83
  79. package/package.json +2 -2
  80. package/testing/index.js +232 -232
@@ -1,125 +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가 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
+ /**
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
+ }
@@ -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
+ }
@@ -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
+ }