mega-framework 0.1.6 → 0.1.8
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 +9 -0
- package/bin/mega-ws-hub.js +2 -2
- package/package.json +33 -9
- package/sample/crud/.env +10 -1
- package/sample/crud/.env.example +10 -1
- package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
- package/sample/crud/.mega/journal/snapshot.json +261 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
- package/sample/crud/apps/main/controllers/web-controller.js +7 -5
- package/sample/crud/apps/main/locales/server/en.json +12 -1
- package/sample/crud/apps/main/locales/server/ko.json +12 -1
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +91 -13
- package/sample/crud/apps/main/migrations/20260606000002-create-boards.js +165 -0
- package/sample/crud/apps/main/migrations/20260606000003-create-logs.js +107 -0
- package/sample/crud/apps/main/models/log-partition-model.js +105 -0
- package/sample/crud/apps/main/models/note-model.js +79 -0
- package/sample/crud/apps/main/models/user-level-model.js +24 -0
- package/sample/crud/apps/main/models/user-model.js +146 -0
- package/sample/crud/apps/main/models/user-type-model.js +21 -0
- package/sample/crud/apps/main/models/wallet-model.js +24 -0
- package/sample/crud/apps/main/routes/users.js +55 -10
- package/sample/crud/apps/main/schedules/log-partition-schedule.js +33 -0
- package/sample/crud/apps/main/services/auth-service.js +39 -24
- package/sample/crud/apps/main/services/log-partition-service.js +101 -0
- package/sample/crud/apps/main/services/note-service.js +6 -6
- package/sample/crud/apps/main/services/redis-demo-service.js +3 -3
- package/sample/crud/apps/main/services/user-service.js +62 -21
- package/sample/crud/apps/main/views/auth/login.ejs +6 -6
- package/sample/crud/apps/main/views/auth/register.ejs +46 -5
- package/sample/crud/apps/main/views/users/edit.ejs +42 -5
- package/sample/crud/apps/main/views/users/list.ejs +6 -2
- package/sample/crud/apps/main/views/users/new.ejs +56 -4
- package/sample/crud/docs/log_partition_design.mm.md +23 -0
- package/sample/crud/mega.config.js +10 -2
- package/sample/crud/package.json +3 -3
- package/sample/crud/scripts/start-ws-hub.sh +20 -6
- package/sample/simple/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/sample/simple/package.json +2 -2
- package/src/adapters/adapter-manager.js +2 -1
- package/src/adapters/adapter-options.js +44 -3
- package/src/adapters/file-adapter.js +9 -5
- package/src/adapters/file-session-adapter.js +4 -3
- package/src/adapters/maria-adapter.js +33 -7
- package/src/adapters/mega-cache-adapter.js +83 -6
- package/src/adapters/mega-db-adapter.js +10 -1
- package/src/adapters/mongo-adapter.js +40 -8
- package/src/adapters/postgres-adapter.js +33 -6
- package/src/adapters/redis-adapter.js +7 -3
- package/src/adapters/sqlite-adapter.js +26 -3
- package/src/cli/commands/console-cmd.js +3 -1
- package/src/cli/commands/new.js +13 -3
- package/src/cli/commands/scaffold.js +173 -33
- package/src/cli/generators/index.js +140 -3
- package/src/cli/index.js +437 -155
- package/src/cli/watch.js +188 -0
- package/src/core/ajv-mapper.js +30 -3
- package/src/core/boot.js +464 -245
- package/src/core/cluster-metrics.js +13 -4
- package/src/core/ctx-builder.js +65 -3
- package/src/core/envelope.js +119 -12
- package/src/core/hub-link.js +89 -18
- package/src/core/i18n.js +11 -1
- package/src/core/index.js +7 -3
- package/src/core/mega-app.js +253 -505
- package/src/core/mega-cluster.js +4 -1
- package/src/core/mega-server.js +40 -9
- package/src/core/migration/dialect-registry.js +107 -0
- package/src/core/migration/dialects/README.md +62 -0
- package/src/core/migration/dialects/maria.js +496 -0
- package/src/core/migration/dialects/mongo.js +824 -0
- package/src/core/migration/dialects/postgres.js +563 -0
- package/src/core/migration/dialects/sqlite.js +476 -0
- package/src/core/migration/differ.js +456 -0
- package/src/core/migration/generate.js +508 -0
- package/src/core/migration/journal.js +167 -0
- package/src/core/migration/model-scan.js +84 -0
- package/src/core/migration/mongo-migration-db.js +97 -0
- package/src/core/migration/schema-builder.js +400 -0
- package/src/core/migration/schema-validator.js +315 -0
- package/src/core/migration-lock.js +205 -0
- package/src/core/migration-runner.js +166 -38
- package/src/core/multipart.js +28 -5
- package/src/core/pipeline.js +131 -0
- package/src/core/router.js +70 -65
- package/src/core/scope-registry.js +1 -0
- package/src/core/security.js +70 -12
- package/src/core/session-store.js +14 -1
- package/src/core/workers-manager.js +12 -1
- package/src/core/ws-cluster.js +10 -3
- package/src/core/ws-message.js +48 -4
- package/src/core/ws-presence.js +636 -0
- package/src/core/ws-roster.js +50 -8
- package/src/core/ws-upgrade.js +223 -12
- package/src/index.js +1 -1
- package/src/lib/hub-protocol.js +29 -0
- package/src/lib/mega-circuit-breaker.js +5 -3
- package/src/lib/mega-health.js +35 -4
- package/src/lib/mega-job-queue.js +151 -34
- package/src/lib/mega-job.js +37 -1
- package/src/lib/mega-metrics.js +31 -13
- package/src/lib/mega-plugin.js +34 -3
- package/src/lib/mega-schedule.js +40 -22
- package/src/lib/mega-shutdown.js +114 -39
- package/src/lib/mega-tracing.js +66 -19
- package/src/lib/mega-worker.js +33 -6
- package/src/lib/otel-resource.js +36 -0
- package/src/{cli → lib}/ws-hub.js +139 -15
- package/src/models/crud-sql-builder.js +133 -0
- package/src/models/mega-model.js +82 -2
- package/src/models/model-crud.js +483 -0
- package/src/models/mongo-crud.js +285 -0
- package/templates/adr/code.tpl +23 -0
- package/templates/model/code-mongo.tpl +35 -0
- package/templates/model/code.tpl +15 -1
- package/templates/model/test-mongo.tpl +38 -0
- package/templates/model/test.tpl +4 -0
- package/types/adapters/adapter-manager.d.ts +95 -0
- package/types/adapters/adapter-options.d.ts +93 -0
- package/types/adapters/file-adapter.d.ts +105 -0
- package/types/adapters/file-session-adapter.d.ts +103 -0
- package/types/adapters/index.d.ts +20 -0
- package/types/adapters/maria-adapter.d.ts +117 -0
- package/types/adapters/mega-adapter.d.ts +215 -0
- package/types/adapters/mega-bus-adapter.d.ts +45 -0
- package/types/adapters/mega-cache-adapter.d.ts +73 -0
- package/types/adapters/mega-db-adapter.d.ts +50 -0
- package/types/adapters/mega-lock-adapter.d.ts +62 -0
- package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
- package/types/adapters/mega-session-adapter.d.ts +32 -0
- package/types/adapters/mongo-adapter.d.ts +150 -0
- package/types/adapters/nats-adapter.d.ts +108 -0
- package/types/adapters/postgres-adapter.d.ts +141 -0
- package/types/adapters/redis-adapter.d.ts +78 -0
- package/types/adapters/redis-session-adapter.d.ts +82 -0
- package/types/adapters/redlock-adapter.d.ts +149 -0
- package/types/adapters/registry.d.ts +46 -0
- package/types/adapters/sqlite-adapter.d.ts +112 -0
- package/types/auth/index.d.ts +24 -0
- package/types/cli/commands/console-cmd.d.ts +37 -0
- package/types/cli/commands/new.d.ts +16 -0
- package/types/cli/commands/routes.d.ts +36 -0
- package/types/cli/commands/scaffold.d.ts +78 -0
- package/types/cli/commands/test-cmd.d.ts +14 -0
- package/types/cli/generators/index.d.ts +122 -0
- package/types/cli/index.d.ts +234 -0
- package/types/cli/template-engine.d.ts +40 -0
- package/types/cli/watch.d.ts +59 -0
- package/types/core/ajv-mapper.d.ts +27 -0
- package/types/core/boot.d.ts +233 -0
- package/types/core/cluster-metrics.d.ts +52 -0
- package/types/core/config-loader.d.ts +13 -0
- package/types/core/config-validator.d.ts +30 -0
- package/types/core/ctx-builder.d.ts +103 -0
- package/types/core/envelope.d.ts +79 -0
- package/types/core/error-mapper.d.ts +17 -0
- package/types/core/formbody.d.ts +41 -0
- package/types/core/hub-link.d.ts +266 -0
- package/types/core/i18n.d.ts +178 -0
- package/types/core/index.d.ts +28 -0
- package/types/core/mega-app.d.ts +529 -0
- package/types/core/mega-cluster.d.ts +104 -0
- package/types/core/mega-server.d.ts +91 -0
- package/types/core/mega-service.d.ts +31 -0
- package/types/core/migration/dialect-registry.d.ts +22 -0
- package/types/core/migration/dialects/maria.d.ts +99 -0
- package/types/core/migration/dialects/mongo.d.ts +89 -0
- package/types/core/migration/dialects/postgres.d.ts +117 -0
- package/types/core/migration/dialects/sqlite.d.ts +111 -0
- package/types/core/migration/differ.d.ts +47 -0
- package/types/core/migration/generate.d.ts +56 -0
- package/types/core/migration/journal.d.ts +52 -0
- package/types/core/migration/model-scan.d.ts +19 -0
- package/types/core/migration/mongo-migration-db.d.ts +7 -0
- package/types/core/migration/schema-builder.d.ts +197 -0
- package/types/core/migration/schema-validator.d.ts +20 -0
- package/types/core/migration-lock.d.ts +33 -0
- package/types/core/migration-runner.d.ts +101 -0
- package/types/core/multipart.d.ts +86 -0
- package/types/core/openapi.d.ts +62 -0
- package/types/core/pipeline.d.ts +93 -0
- package/types/core/router.d.ts +159 -0
- package/types/core/routes-loader.d.ts +21 -0
- package/types/core/scope-registry.d.ts +14 -0
- package/types/core/security.d.ts +77 -0
- package/types/core/services-loader.d.ts +27 -0
- package/types/core/session-cleanup-schedule.d.ts +19 -0
- package/types/core/session-store.d.ts +25 -0
- package/types/core/session.d.ts +77 -0
- package/types/core/static-assets.d.ts +73 -0
- package/types/core/template.d.ts +106 -0
- package/types/core/workers-manager.d.ts +79 -0
- package/types/core/ws-cluster.d.ts +208 -0
- package/types/core/ws-compression.d.ts +112 -0
- package/types/core/ws-controller.d.ts +65 -0
- package/types/core/ws-message.d.ts +106 -0
- package/types/core/ws-presence.d.ts +273 -0
- package/types/core/ws-roster.d.ts +108 -0
- package/types/core/ws-upgrade.d.ts +260 -0
- package/types/errors/config-error.d.ts +10 -0
- package/types/errors/http-errors.d.ts +120 -0
- package/types/errors/index.d.ts +3 -0
- package/types/errors/mega-error.d.ts +32 -0
- package/types/index.d.ts +39 -0
- package/types/lib/asp/config.d.ts +49 -0
- package/types/lib/asp/crypto.d.ts +43 -0
- package/types/lib/asp/errors.d.ts +30 -0
- package/types/lib/asp/nonce-cache.d.ts +52 -0
- package/types/lib/asp/plugin.d.ts +30 -0
- package/types/lib/asp/ws-terminator.d.ts +45 -0
- package/types/lib/env-mapper.d.ts +14 -0
- package/types/lib/hub-protocol.d.ts +106 -0
- package/types/lib/index.d.ts +22 -0
- package/types/lib/logger/telegram-core.d.ts +104 -0
- package/types/lib/logger/telegram-transport.d.ts +45 -0
- package/types/lib/mega-brute-force.d.ts +66 -0
- package/types/lib/mega-circuit-breaker.d.ts +243 -0
- package/types/lib/mega-cron.d.ts +66 -0
- package/types/lib/mega-hash.d.ts +32 -0
- package/types/lib/mega-health.d.ts +48 -0
- package/types/lib/mega-job-queue.d.ts +188 -0
- package/types/lib/mega-job-worker.d.ts +130 -0
- package/types/lib/mega-job.d.ts +145 -0
- package/types/lib/mega-logger.d.ts +45 -0
- package/types/lib/mega-metrics.d.ts +285 -0
- package/types/lib/mega-plugin.d.ts +245 -0
- package/types/lib/mega-retry.d.ts +85 -0
- package/types/lib/mega-schedule.d.ts +260 -0
- package/types/lib/mega-shutdown.d.ts +135 -0
- package/types/lib/mega-tracing.d.ts +224 -0
- package/types/lib/mega-worker.d.ts +129 -0
- package/types/lib/otel-resource.d.ts +16 -0
- package/types/lib/worker-runner/process-entry.d.ts +1 -0
- package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
- package/types/lib/worker-runner/thread-entry.d.ts +1 -0
- package/types/lib/ws-hub.d.ts +259 -0
- package/types/models/crud-sql-builder.d.ts +48 -0
- package/types/models/index.d.ts +1 -0
- package/types/models/mega-model.d.ts +138 -0
- package/types/models/model-crud.d.ts +82 -0
- package/types/models/mongo-crud.d.ts +59 -0
- package/types/test/index.d.ts +84 -0
- package/.env +0 -127
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
- package/sample/crud/apps/main/models/note.js +0 -71
- package/sample/crud/apps/main/models/user.js +0 -86
- package/sample/crud/package-lock.json +0 -5665
- package/sample/crud/yarn.lock +0 -2142
- package/sample/simple/package-lock.json +0 -1851
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"generatedAt": "2026-06-12T09:25:43.485Z",
|
|
4
|
+
"adapters": {
|
|
5
|
+
"primary": {
|
|
6
|
+
"driver": "postgres",
|
|
7
|
+
"models": [
|
|
8
|
+
{
|
|
9
|
+
"name": "UserLevelModel",
|
|
10
|
+
"table": "user_levels",
|
|
11
|
+
"record": {
|
|
12
|
+
"table": "user_levels",
|
|
13
|
+
"adapter": "primary",
|
|
14
|
+
"columns": {
|
|
15
|
+
"id": {
|
|
16
|
+
"type": "serial",
|
|
17
|
+
"primary": true
|
|
18
|
+
},
|
|
19
|
+
"code": {
|
|
20
|
+
"type": "text",
|
|
21
|
+
"notNull": true,
|
|
22
|
+
"unique": true,
|
|
23
|
+
"comment": "회원 등급 코드"
|
|
24
|
+
},
|
|
25
|
+
"type_code": {
|
|
26
|
+
"type": "text",
|
|
27
|
+
"notNull": true,
|
|
28
|
+
"references": {
|
|
29
|
+
"model": "UserTypeModel",
|
|
30
|
+
"column": "code",
|
|
31
|
+
"onDelete": "cascade",
|
|
32
|
+
"table": "user_types"
|
|
33
|
+
},
|
|
34
|
+
"comment": "회원 유형 코드"
|
|
35
|
+
},
|
|
36
|
+
"sort_order": {
|
|
37
|
+
"type": "integer",
|
|
38
|
+
"notNull": true,
|
|
39
|
+
"default": 0,
|
|
40
|
+
"comment": "정렬 순서"
|
|
41
|
+
},
|
|
42
|
+
"name": {
|
|
43
|
+
"type": "text",
|
|
44
|
+
"notNull": true,
|
|
45
|
+
"comment": "회원 등급명"
|
|
46
|
+
},
|
|
47
|
+
"created_at": {
|
|
48
|
+
"type": "timestamptz",
|
|
49
|
+
"notNull": true,
|
|
50
|
+
"default": {
|
|
51
|
+
"raw": "now()"
|
|
52
|
+
},
|
|
53
|
+
"comment": "생성 시간"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"indexes": [
|
|
57
|
+
{
|
|
58
|
+
"columns": [
|
|
59
|
+
"type_code"
|
|
60
|
+
],
|
|
61
|
+
"name": "idx_user_levels_type_code"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "UserModel",
|
|
68
|
+
"table": "users",
|
|
69
|
+
"record": {
|
|
70
|
+
"table": "users",
|
|
71
|
+
"adapter": "primary",
|
|
72
|
+
"columns": {
|
|
73
|
+
"id": {
|
|
74
|
+
"type": "bigSerial",
|
|
75
|
+
"primary": true
|
|
76
|
+
},
|
|
77
|
+
"uuid": {
|
|
78
|
+
"type": "uuid",
|
|
79
|
+
"notNull": true,
|
|
80
|
+
"default": {
|
|
81
|
+
"raw": "gen_random_uuid()"
|
|
82
|
+
},
|
|
83
|
+
"comment": "고유값"
|
|
84
|
+
},
|
|
85
|
+
"username": {
|
|
86
|
+
"type": "text",
|
|
87
|
+
"notNull": true,
|
|
88
|
+
"unique": true,
|
|
89
|
+
"comment": "로그인 아이디"
|
|
90
|
+
},
|
|
91
|
+
"password_hash": {
|
|
92
|
+
"type": "text",
|
|
93
|
+
"notNull": true,
|
|
94
|
+
"comment": "비밀번호 해시"
|
|
95
|
+
},
|
|
96
|
+
"type_code": {
|
|
97
|
+
"type": "text",
|
|
98
|
+
"notNull": true,
|
|
99
|
+
"default": "USER",
|
|
100
|
+
"comment": "회원 유형"
|
|
101
|
+
},
|
|
102
|
+
"level_code": {
|
|
103
|
+
"type": "text",
|
|
104
|
+
"notNull": true,
|
|
105
|
+
"default": "lv1",
|
|
106
|
+
"comment": "회원 등급"
|
|
107
|
+
},
|
|
108
|
+
"name": {
|
|
109
|
+
"type": "text",
|
|
110
|
+
"notNull": true,
|
|
111
|
+
"comment": "실명"
|
|
112
|
+
},
|
|
113
|
+
"nickname": {
|
|
114
|
+
"type": "text",
|
|
115
|
+
"notNull": true,
|
|
116
|
+
"unique": true,
|
|
117
|
+
"comment": "별명"
|
|
118
|
+
},
|
|
119
|
+
"email": {
|
|
120
|
+
"type": "text",
|
|
121
|
+
"comment": "이메일"
|
|
122
|
+
},
|
|
123
|
+
"phone_number": {
|
|
124
|
+
"type": "text",
|
|
125
|
+
"comment": "전화번호"
|
|
126
|
+
},
|
|
127
|
+
"last_login_at": {
|
|
128
|
+
"type": "timestamptz",
|
|
129
|
+
"comment": "마지막 로그인 시간"
|
|
130
|
+
},
|
|
131
|
+
"created_at": {
|
|
132
|
+
"type": "timestamptz",
|
|
133
|
+
"notNull": true,
|
|
134
|
+
"default": {
|
|
135
|
+
"raw": "now()"
|
|
136
|
+
},
|
|
137
|
+
"comment": "생성 시간"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"indexes": [
|
|
141
|
+
{
|
|
142
|
+
"columns": [
|
|
143
|
+
"uuid"
|
|
144
|
+
],
|
|
145
|
+
"name": "idx_users_uuid",
|
|
146
|
+
"unique": true
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"columns": [
|
|
150
|
+
"email"
|
|
151
|
+
],
|
|
152
|
+
"name": "idx_users_email"
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"columns": [
|
|
156
|
+
"type_code"
|
|
157
|
+
],
|
|
158
|
+
"name": "idx_users_type_code"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"columns": [
|
|
162
|
+
"level_code"
|
|
163
|
+
],
|
|
164
|
+
"name": "idx_users_level_code"
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"name": "UserTypeModel",
|
|
171
|
+
"table": "user_types",
|
|
172
|
+
"record": {
|
|
173
|
+
"table": "user_types",
|
|
174
|
+
"adapter": "primary",
|
|
175
|
+
"columns": {
|
|
176
|
+
"id": {
|
|
177
|
+
"type": "serial",
|
|
178
|
+
"primary": true
|
|
179
|
+
},
|
|
180
|
+
"code": {
|
|
181
|
+
"type": "text",
|
|
182
|
+
"notNull": true,
|
|
183
|
+
"unique": true,
|
|
184
|
+
"comment": "회원 유형 코드"
|
|
185
|
+
},
|
|
186
|
+
"sort_order": {
|
|
187
|
+
"type": "integer",
|
|
188
|
+
"notNull": true,
|
|
189
|
+
"default": 0,
|
|
190
|
+
"comment": "정렬 순서"
|
|
191
|
+
},
|
|
192
|
+
"name": {
|
|
193
|
+
"type": "text",
|
|
194
|
+
"notNull": true,
|
|
195
|
+
"comment": "회원 유형명"
|
|
196
|
+
},
|
|
197
|
+
"created_at": {
|
|
198
|
+
"type": "timestamptz",
|
|
199
|
+
"notNull": true,
|
|
200
|
+
"default": {
|
|
201
|
+
"raw": "now()"
|
|
202
|
+
},
|
|
203
|
+
"comment": "생성 시간"
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
"indexes": []
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"name": "WalletModel",
|
|
211
|
+
"table": "wallets",
|
|
212
|
+
"record": {
|
|
213
|
+
"table": "wallets",
|
|
214
|
+
"adapter": "primary",
|
|
215
|
+
"columns": {
|
|
216
|
+
"id": {
|
|
217
|
+
"type": "serial",
|
|
218
|
+
"primary": true
|
|
219
|
+
},
|
|
220
|
+
"user_id": {
|
|
221
|
+
"type": "bigInteger",
|
|
222
|
+
"notNull": true,
|
|
223
|
+
"references": {
|
|
224
|
+
"model": "UserModel",
|
|
225
|
+
"column": "id",
|
|
226
|
+
"onDelete": "cascade",
|
|
227
|
+
"table": "users"
|
|
228
|
+
},
|
|
229
|
+
"comment": "회원 ID"
|
|
230
|
+
},
|
|
231
|
+
"balance": {
|
|
232
|
+
"type": "decimal",
|
|
233
|
+
"precision": 20,
|
|
234
|
+
"scale": 4,
|
|
235
|
+
"notNull": true,
|
|
236
|
+
"default": 0,
|
|
237
|
+
"comment": "잔액"
|
|
238
|
+
},
|
|
239
|
+
"created_at": {
|
|
240
|
+
"type": "timestamptz",
|
|
241
|
+
"notNull": true,
|
|
242
|
+
"default": {
|
|
243
|
+
"raw": "now()"
|
|
244
|
+
},
|
|
245
|
+
"comment": "생성 시간"
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
"indexes": [
|
|
249
|
+
{
|
|
250
|
+
"columns": [
|
|
251
|
+
"user_id"
|
|
252
|
+
],
|
|
253
|
+
"name": "idx_wallets_user_id"
|
|
254
|
+
}
|
|
255
|
+
]
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
]
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -45,22 +45,22 @@ export class AuthController {
|
|
|
45
45
|
|
|
46
46
|
/** POST /auth/login — 인증 → 세션 생성 → 관리 UI. 잠김/실패 시 폼 재렌더. @param {any} req @param {any} reply @param {any} ctx */
|
|
47
47
|
static async login(req, reply, ctx) {
|
|
48
|
-
const
|
|
48
|
+
const username = typeof req.body?.username === 'string' ? req.body.username.trim() : ''
|
|
49
49
|
const password = typeof req.body?.password === 'string' ? req.body.password : ''
|
|
50
|
-
// subject = IP:
|
|
51
|
-
const subject = `${req.ip}:${
|
|
50
|
+
// subject = IP:username — username 단독 잠금은 피해자 계정을 공격자가 잠그는 DoS 가 가능(I-1).
|
|
51
|
+
const subject = `${req.ip}:${username}`
|
|
52
52
|
const bf = ctx.bruteForce
|
|
53
53
|
|
|
54
54
|
const status = await bf.check(subject)
|
|
55
55
|
if (status.isLocked) {
|
|
56
|
-
return AuthController.#renderLogin(reply, ctx, {
|
|
56
|
+
return AuthController.#renderLogin(reply, ctx, { username }, ctx.t('login_locked', '로그인 시도가 너무 많습니다. 잠시 후 다시 시도하세요.'), 423)
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const user = await ctx.services.auth.authenticate({
|
|
59
|
+
const user = await ctx.services.auth.authenticate({ username, password })
|
|
60
60
|
if (!user) {
|
|
61
61
|
const after = await bf.fail(subject)
|
|
62
|
-
const msg = after.isLocked ? ctx.t('login_locked', '로그인 시도가 너무 많습니다. 잠시 후 다시 시도하세요.') : ctx.t('login_failed', '
|
|
63
|
-
return AuthController.#renderLogin(reply, ctx, {
|
|
62
|
+
const msg = after.isLocked ? ctx.t('login_locked', '로그인 시도가 너무 많습니다. 잠시 후 다시 시도하세요.') : ctx.t('login_failed', '아이디 또는 비밀번호가 올바르지 않습니다.')
|
|
63
|
+
return AuthController.#renderLogin(reply, ctx, { username }, msg, after.isLocked ? 423 : 401)
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
await bf.reset(subject)
|
|
@@ -99,7 +99,13 @@ export class AuthController {
|
|
|
99
99
|
reply.code(err instanceof MegaConflictError ? 409 : 400)
|
|
100
100
|
return ctx.render('auth/register', {
|
|
101
101
|
title: ctx.t('register_title', '회원가입'),
|
|
102
|
-
values: {
|
|
102
|
+
values: {
|
|
103
|
+
username: req.body?.username ?? '',
|
|
104
|
+
name: req.body?.name ?? '',
|
|
105
|
+
nickname: req.body?.nickname ?? '',
|
|
106
|
+
email: req.body?.email ?? '',
|
|
107
|
+
phone_number: req.body?.phone_number ?? '',
|
|
108
|
+
},
|
|
103
109
|
invalid: AuthController.#invalidFields(err),
|
|
104
110
|
error: err.message,
|
|
105
111
|
currentUser: null,
|
|
@@ -112,7 +118,7 @@ export class AuthController {
|
|
|
112
118
|
|
|
113
119
|
/**
|
|
114
120
|
* 로그인 폼 재렌더 헬퍼(상태코드 + 에러 메시지 + 입력 보존 + 새 CSRF 토큰).
|
|
115
|
-
* @param {any} reply @param {any} ctx @param {{
|
|
121
|
+
* @param {any} reply @param {any} ctx @param {{ username: string }} values @param {string} error @param {number} code
|
|
116
122
|
*/
|
|
117
123
|
static #renderLogin(reply, ctx, values, error, code) {
|
|
118
124
|
reply.code(code)
|
|
@@ -128,17 +134,19 @@ export class AuthController {
|
|
|
128
134
|
|
|
129
135
|
/**
|
|
130
136
|
* 회원가입 검증/충돌 에러 → 폼 is-invalid 표시용 필드 맵.
|
|
131
|
-
* @param {unknown} err @returns {{ name: boolean,
|
|
137
|
+
* @param {unknown} err @returns {{ username: boolean, name: boolean, nickname: boolean, password: boolean }}
|
|
132
138
|
*/
|
|
133
139
|
static #invalidFields(err) {
|
|
134
140
|
if (err instanceof MegaValidationError) {
|
|
135
141
|
const d = /** @type {any} */ (err).details ?? {}
|
|
136
|
-
// auth-service 는 details.{name,
|
|
137
|
-
return { name: !d.name,
|
|
142
|
+
// auth-service 는 details.{username,name,nickname,password} = "값이 유효한가"(true=정상) → 없으면 invalid.
|
|
143
|
+
return { username: !d.username, name: !d.name, nickname: !d.nickname, password: !d.password }
|
|
138
144
|
}
|
|
139
145
|
if (err instanceof MegaConflictError) {
|
|
140
|
-
|
|
146
|
+
// username/nickname 중복 — code 로 구분(user.nickname_taken vs user.username_taken).
|
|
147
|
+
const isNick = /** @type {any} */ (err).code === 'user.nickname_taken'
|
|
148
|
+
return { username: !isNick, name: false, nickname: isNick, password: false }
|
|
141
149
|
}
|
|
142
|
-
return { name: false,
|
|
150
|
+
return { username: false, name: false, nickname: false, password: false }
|
|
143
151
|
}
|
|
144
152
|
}
|
|
@@ -19,18 +19,20 @@ const NOTICE_KEYS = new Set(['created', 'updated', 'deleted'])
|
|
|
19
19
|
/**
|
|
20
20
|
* 검증/충돌 에러에서 어떤 필드가 잘못됐는지 계산한다(폼 is-invalid 표시용).
|
|
21
21
|
* @param {unknown} err
|
|
22
|
-
* @returns {{ name: boolean,
|
|
22
|
+
* @returns {{ username: boolean, name: boolean, nickname: boolean, password: boolean }}
|
|
23
23
|
*/
|
|
24
24
|
function invalidFields(err) {
|
|
25
25
|
if (err instanceof MegaValidationError) {
|
|
26
26
|
const d = /** @type {any} */ (err).details ?? {}
|
|
27
|
-
// user-service 는 details.{name,
|
|
28
|
-
return { name: !d.name,
|
|
27
|
+
// user-service 는 details.{username,name,nickname,password} = "값이 유효한가"(true=정상) → 없으면 invalid.
|
|
28
|
+
return { username: !d.username, name: !d.name, nickname: !d.nickname, password: !d.password }
|
|
29
29
|
}
|
|
30
30
|
if (err instanceof MegaConflictError) {
|
|
31
|
-
|
|
31
|
+
// username/nickname 중복 — code 로 구분.
|
|
32
|
+
const isNick = /** @type {any} */ (err).code === 'user.nickname_taken'
|
|
33
|
+
return { username: !isNick, name: false, nickname: isNick, password: false }
|
|
32
34
|
}
|
|
33
|
-
return { name: false,
|
|
35
|
+
return { username: false, name: false, nickname: false, password: false }
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export class WebController {
|
|
@@ -313,5 +313,16 @@
|
|
|
313
313
|
"csrf": {
|
|
314
314
|
"invalid_token": "Invalid csrf token",
|
|
315
315
|
"missing_secret": "Missing csrf secret"
|
|
316
|
-
}
|
|
316
|
+
},
|
|
317
|
+
"field_username": "아이디",
|
|
318
|
+
"field_username_ph": "예: hong123",
|
|
319
|
+
"field_username_required": "아이디를 입력하세요.",
|
|
320
|
+
"field_nickname": "별명",
|
|
321
|
+
"field_nickname_ph": "예: 길동이",
|
|
322
|
+
"field_nickname_required": "별명을 입력하세요.",
|
|
323
|
+
"optional": "선택",
|
|
324
|
+
"field_phone": "전화번호",
|
|
325
|
+
"field_phone_ph": "예: 010-1234-5678",
|
|
326
|
+
"col_username": "아이디",
|
|
327
|
+
"col_nickname": "별명"
|
|
317
328
|
}
|
|
@@ -313,5 +313,16 @@
|
|
|
313
313
|
},
|
|
314
314
|
"guide": {
|
|
315
315
|
"not_found": "가이드를 찾을 수 없습니다."
|
|
316
|
-
}
|
|
316
|
+
},
|
|
317
|
+
"field_username": "아이디",
|
|
318
|
+
"field_username_ph": "예: hong123",
|
|
319
|
+
"field_username_required": "아이디를 입력하세요.",
|
|
320
|
+
"field_nickname": "별명",
|
|
321
|
+
"field_nickname_ph": "예: 길동이",
|
|
322
|
+
"field_nickname_required": "별명을 입력하세요.",
|
|
323
|
+
"optional": "선택",
|
|
324
|
+
"field_phone": "전화번호",
|
|
325
|
+
"field_phone_ph": "예: 010-1234-5678",
|
|
326
|
+
"col_username": "아이디",
|
|
327
|
+
"col_nickname": "별명"
|
|
317
328
|
}
|
|
@@ -1,27 +1,105 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* 마이그레이션 create-users (
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* 마이그레이션 create-users (20260612092543) — `mega migrate:generate` 자동 생성 (ADR-204).
|
|
4
|
+
* 기준 스냅샷: .mega/journal/history/20260612092543-create-users.json
|
|
5
|
+
* 적용 전 SQL 검토 필수 — 파괴적 변경(-- 경고)·캐스트(-- TODO) 주석이 있으면 직접 확정하세요.
|
|
6
|
+
* 적용은 `mega migrate`(락·checksum 은 러너 관리, ADR-149/190), 롤백은 `mega migrate:down`.
|
|
6
7
|
*
|
|
7
8
|
* @param {{ query: (sql: string, params?: any[]) => Promise<any> }} db - 대상 DB 어댑터(트랜잭션 내 query).
|
|
8
9
|
* @returns {Promise<void>}
|
|
9
10
|
*/
|
|
10
11
|
export async function up(db) {
|
|
11
|
-
await db.query(`
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
await db.query(`CREATE TABLE user_levels (
|
|
13
|
+
id SERIAL PRIMARY KEY,
|
|
14
|
+
code TEXT NOT NULL,
|
|
15
|
+
type_code TEXT NOT NULL,
|
|
16
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
17
|
+
name TEXT NOT NULL,
|
|
18
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
19
|
+
)`)
|
|
20
|
+
await db.query(`COMMENT ON TABLE user_levels IS '회원 등급 테이블'`)
|
|
21
|
+
await db.query(`ALTER TABLE user_levels ADD CONSTRAINT uniq_user_levels_code UNIQUE (code)`)
|
|
22
|
+
await db.query(`COMMENT ON COLUMN user_levels.code IS '회원 등급 코드'`)
|
|
23
|
+
await db.query(`COMMENT ON COLUMN user_levels.type_code IS '회원 유형 코드'`)
|
|
24
|
+
await db.query(`COMMENT ON COLUMN user_levels.sort_order IS '정렬 순서'`)
|
|
25
|
+
await db.query(`COMMENT ON COLUMN user_levels.name IS '회원 등급명'`)
|
|
26
|
+
await db.query(`COMMENT ON COLUMN user_levels.created_at IS '생성 시간'`)
|
|
27
|
+
await db.query(`CREATE TABLE users (
|
|
28
|
+
id BIGSERIAL PRIMARY KEY,
|
|
29
|
+
uuid UUID NOT NULL DEFAULT gen_random_uuid(),
|
|
30
|
+
username TEXT NOT NULL,
|
|
31
|
+
password_hash TEXT NOT NULL,
|
|
32
|
+
type_code TEXT NOT NULL DEFAULT 'USER',
|
|
33
|
+
level_code TEXT NOT NULL DEFAULT 'lv1',
|
|
34
|
+
name TEXT NOT NULL,
|
|
35
|
+
nickname TEXT NOT NULL,
|
|
36
|
+
email TEXT,
|
|
37
|
+
phone_number TEXT,
|
|
38
|
+
last_login_at TIMESTAMPTZ,
|
|
39
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
40
|
+
)`)
|
|
41
|
+
await db.query(`COMMENT ON TABLE users IS '회원 테이블'`)
|
|
42
|
+
await db.query(`COMMENT ON COLUMN users.uuid IS '고유값'`)
|
|
43
|
+
await db.query(`ALTER TABLE users ADD CONSTRAINT uniq_users_username UNIQUE (username)`)
|
|
44
|
+
await db.query(`COMMENT ON COLUMN users.username IS '로그인 아이디'`)
|
|
45
|
+
await db.query(`COMMENT ON COLUMN users.password_hash IS '비밀번호 해시'`)
|
|
46
|
+
await db.query(`COMMENT ON COLUMN users.type_code IS '회원 유형'`)
|
|
47
|
+
await db.query(`COMMENT ON COLUMN users.level_code IS '회원 등급'`)
|
|
48
|
+
await db.query(`COMMENT ON COLUMN users.name IS '실명'`)
|
|
49
|
+
await db.query(`ALTER TABLE users ADD CONSTRAINT uniq_users_nickname UNIQUE (nickname)`)
|
|
50
|
+
await db.query(`COMMENT ON COLUMN users.nickname IS '별명'`)
|
|
51
|
+
await db.query(`COMMENT ON COLUMN users.email IS '이메일'`)
|
|
52
|
+
await db.query(`COMMENT ON COLUMN users.phone_number IS '전화번호'`)
|
|
53
|
+
await db.query(`COMMENT ON COLUMN users.last_login_at IS '마지막 로그인 시간'`)
|
|
54
|
+
await db.query(`COMMENT ON COLUMN users.created_at IS '생성 시간'`)
|
|
55
|
+
await db.query(`CREATE TABLE user_types (
|
|
56
|
+
id SERIAL PRIMARY KEY,
|
|
57
|
+
code TEXT NOT NULL,
|
|
58
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
59
|
+
name TEXT NOT NULL,
|
|
60
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
61
|
+
)`)
|
|
62
|
+
await db.query(`COMMENT ON TABLE user_types IS '회원 유형 테이블'`)
|
|
63
|
+
await db.query(`ALTER TABLE user_types ADD CONSTRAINT uniq_user_types_code UNIQUE (code)`)
|
|
64
|
+
await db.query(`COMMENT ON COLUMN user_types.code IS '회원 유형 코드'`)
|
|
65
|
+
await db.query(`COMMENT ON COLUMN user_types.sort_order IS '정렬 순서'`)
|
|
66
|
+
await db.query(`COMMENT ON COLUMN user_types.name IS '회원 유형명'`)
|
|
67
|
+
await db.query(`COMMENT ON COLUMN user_types.created_at IS '생성 시간'`)
|
|
68
|
+
await db.query(`CREATE TABLE wallets (
|
|
69
|
+
id SERIAL PRIMARY KEY,
|
|
70
|
+
user_id BIGINT NOT NULL,
|
|
71
|
+
balance NUMERIC(20, 4) NOT NULL DEFAULT 0,
|
|
72
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
73
|
+
)`)
|
|
74
|
+
await db.query(`COMMENT ON TABLE wallets IS '회원 지갑 테이블'`)
|
|
75
|
+
await db.query(`COMMENT ON COLUMN wallets.user_id IS '회원 ID'`)
|
|
76
|
+
await db.query(`COMMENT ON COLUMN wallets.balance IS '잔액'`)
|
|
77
|
+
await db.query(`COMMENT ON COLUMN wallets.created_at IS '생성 시간'`)
|
|
78
|
+
await db.query(`ALTER TABLE user_levels ADD CONSTRAINT fk_user_levels_type_code_user_types FOREIGN KEY (type_code) REFERENCES user_types (code) ON DELETE CASCADE`)
|
|
79
|
+
await db.query(`ALTER TABLE wallets ADD CONSTRAINT fk_wallets_user_id_users FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE`)
|
|
80
|
+
await db.query(`CREATE INDEX idx_user_levels_type_code ON user_levels (type_code)`)
|
|
81
|
+
await db.query(`CREATE UNIQUE INDEX idx_users_uuid ON users (uuid)`)
|
|
82
|
+
await db.query(`CREATE INDEX idx_users_email ON users (email)`)
|
|
83
|
+
await db.query(`CREATE INDEX idx_users_type_code ON users (type_code)`)
|
|
84
|
+
await db.query(`CREATE INDEX idx_users_level_code ON users (level_code)`)
|
|
85
|
+
await db.query(`CREATE INDEX idx_wallets_user_id ON wallets (user_id)`)
|
|
19
86
|
}
|
|
20
87
|
|
|
21
88
|
/**
|
|
22
|
-
* @param {{ query: (sql: string, params?: any[]) => Promise<any> }} db
|
|
89
|
+
* @param {{ query: (sql: string, params?: any[]) => Promise<any> }} db - 대상 DB 어댑터(트랜잭션 내 query).
|
|
23
90
|
* @returns {Promise<void>}
|
|
24
91
|
*/
|
|
25
92
|
export async function down(db) {
|
|
26
|
-
await db.query(
|
|
93
|
+
await db.query(`DROP INDEX IF EXISTS idx_wallets_user_id`)
|
|
94
|
+
await db.query(`DROP INDEX IF EXISTS idx_users_level_code`)
|
|
95
|
+
await db.query(`DROP INDEX IF EXISTS idx_users_type_code`)
|
|
96
|
+
await db.query(`DROP INDEX IF EXISTS idx_users_email`)
|
|
97
|
+
await db.query(`DROP INDEX IF EXISTS idx_users_uuid`)
|
|
98
|
+
await db.query(`DROP INDEX IF EXISTS idx_user_levels_type_code`)
|
|
99
|
+
await db.query(`ALTER TABLE wallets DROP CONSTRAINT fk_wallets_user_id_users`)
|
|
100
|
+
await db.query(`ALTER TABLE user_levels DROP CONSTRAINT fk_user_levels_type_code_user_types`)
|
|
101
|
+
await db.query(`DROP TABLE IF EXISTS wallets`)
|
|
102
|
+
await db.query(`DROP TABLE IF EXISTS user_types`)
|
|
103
|
+
await db.query(`DROP TABLE IF EXISTS users`)
|
|
104
|
+
await db.query(`DROP TABLE IF EXISTS user_levels`)
|
|
27
105
|
}
|