mega-framework 0.1.0
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/.env +127 -0
- package/.env.example +186 -0
- package/.prettierrc.json +8 -0
- package/CHANGELOG.md +259 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/mega-ws-hub.js +15 -0
- package/bin/mega.js +38 -0
- package/docker-compose.yml +201 -0
- package/eslint.config.js +57 -0
- package/infra/otel-collector-config.yaml +43 -0
- package/jsconfig.json +18 -0
- package/package.json +121 -0
- package/sample/crud/.env +18 -0
- package/sample/crud/.env.example +50 -0
- package/sample/crud/README.md +85 -0
- package/sample/crud/apps/main/app.config.js +114 -0
- package/sample/crud/apps/main/channels/chat-bus.js +115 -0
- package/sample/crud/apps/main/channels/chat-channel.js +145 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +144 -0
- package/sample/crud/apps/main/controllers/cron-controller.js +34 -0
- package/sample/crud/apps/main/controllers/guide-controller.js +37 -0
- package/sample/crud/apps/main/controllers/jobs-controller.js +43 -0
- package/sample/crud/apps/main/controllers/logs-controller.js +35 -0
- package/sample/crud/apps/main/controllers/metrics-controller.js +22 -0
- package/sample/crud/apps/main/controllers/note-controller.js +116 -0
- package/sample/crud/apps/main/controllers/perf-controller.js +38 -0
- package/sample/crud/apps/main/controllers/redis-controller.js +36 -0
- package/sample/crud/apps/main/controllers/tracing-controller.js +43 -0
- package/sample/crud/apps/main/controllers/upload-controller.js +98 -0
- package/sample/crud/apps/main/controllers/user-controller.js +34 -0
- package/sample/crud/apps/main/controllers/web-controller.js +137 -0
- package/sample/crud/apps/main/controllers/worker-controller.js +57 -0
- package/sample/crud/apps/main/controllers/ws-controller.js +29 -0
- package/sample/crud/apps/main/jobs/email-job.js +72 -0
- package/sample/crud/apps/main/locales/client/en.json +3 -0
- package/sample/crud/apps/main/locales/client/ko.json +3 -0
- package/sample/crud/apps/main/locales/server/en.json +316 -0
- package/sample/crud/apps/main/locales/server/ko.json +316 -0
- package/sample/crud/apps/main/middleware/web-auth.js +40 -0
- package/sample/crud/apps/main/middleware/ws-auth.js +48 -0
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +27 -0
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +30 -0
- package/sample/crud/apps/main/models/note.js +71 -0
- package/sample/crud/apps/main/models/user.js +86 -0
- package/sample/crud/apps/main/public/css/app.css +101 -0
- package/sample/crud/apps/main/public/css/guide.css +137 -0
- package/sample/crud/apps/main/public/js/app.js +54 -0
- package/sample/crud/apps/main/public/js/perf.js +129 -0
- package/sample/crud/apps/main/public/js/theme-init.js +12 -0
- package/sample/crud/apps/main/public/js/upload-demo.js +63 -0
- package/sample/crud/apps/main/public/js/worker-demo.js +92 -0
- package/sample/crud/apps/main/public/js/ws-chat.js +161 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/crud/apps/main/public/vendor/highlight/github-dark.css +109 -0
- package/sample/crud/apps/main/public/vendor/highlight/github.css +118 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/README.md +19 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.d.ts +196 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.js +1187 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm_bg.wasm +0 -0
- package/sample/crud/apps/main/routes/auth.js +15 -0
- package/sample/crud/apps/main/routes/cron.js +14 -0
- package/sample/crud/apps/main/routes/guide.js +25 -0
- package/sample/crud/apps/main/routes/jobs.js +14 -0
- package/sample/crud/apps/main/routes/logs.js +28 -0
- package/sample/crud/apps/main/routes/metrics.js +13 -0
- package/sample/crud/apps/main/routes/notes.js +19 -0
- package/sample/crud/apps/main/routes/perf.js +47 -0
- package/sample/crud/apps/main/routes/redis.js +14 -0
- package/sample/crud/apps/main/routes/tracing.js +14 -0
- package/sample/crud/apps/main/routes/upload.js +16 -0
- package/sample/crud/apps/main/routes/users.js +54 -0
- package/sample/crud/apps/main/routes/web.js +23 -0
- package/sample/crud/apps/main/routes/worker.js +15 -0
- package/sample/crud/apps/main/routes/ws.js +30 -0
- package/sample/crud/apps/main/schedules/cron-counter-schedule.js +30 -0
- package/sample/crud/apps/main/services/auth-service.js +74 -0
- package/sample/crud/apps/main/services/cron-demo-service.js +66 -0
- package/sample/crud/apps/main/services/guide-service.js +145 -0
- package/sample/crud/apps/main/services/jobs-demo-service.js +83 -0
- package/sample/crud/apps/main/services/logs-demo-service.js +59 -0
- package/sample/crud/apps/main/services/metrics-demo-service.js +144 -0
- package/sample/crud/apps/main/services/note-service.js +75 -0
- package/sample/crud/apps/main/services/perf-service.js +302 -0
- package/sample/crud/apps/main/services/redis-demo-service.js +75 -0
- package/sample/crud/apps/main/services/tracing-demo-service.js +69 -0
- package/sample/crud/apps/main/services/upload-demo-service.js +48 -0
- package/sample/crud/apps/main/services/user-service.js +65 -0
- package/sample/crud/apps/main/views/auth/login.ejs +57 -0
- package/sample/crud/apps/main/views/auth/register.ejs +71 -0
- package/sample/crud/apps/main/views/cron/index.ejs +92 -0
- package/sample/crud/apps/main/views/guide/index.ejs +24 -0
- package/sample/crud/apps/main/views/guide/page.ejs +64 -0
- package/sample/crud/apps/main/views/home.ejs +82 -0
- package/sample/crud/apps/main/views/jobs/index.ejs +113 -0
- package/sample/crud/apps/main/views/layouts/main.ejs +112 -0
- package/sample/crud/apps/main/views/logs/index.ejs +80 -0
- package/sample/crud/apps/main/views/metrics/index.ejs +123 -0
- package/sample/crud/apps/main/views/notes/edit.ejs +45 -0
- package/sample/crud/apps/main/views/notes/list.ejs +74 -0
- package/sample/crud/apps/main/views/notes/new.ejs +45 -0
- package/sample/crud/apps/main/views/perf/index.ejs +90 -0
- package/sample/crud/apps/main/views/redis/index.ejs +65 -0
- package/sample/crud/apps/main/views/tracing/index.ejs +106 -0
- package/sample/crud/apps/main/views/upload/index.ejs +79 -0
- package/sample/crud/apps/main/views/users/edit.ejs +48 -0
- package/sample/crud/apps/main/views/users/list.ejs +81 -0
- package/sample/crud/apps/main/views/users/new.ejs +48 -0
- package/sample/crud/apps/main/views/worker/index.ejs +70 -0
- package/sample/crud/apps/main/views/ws/index.ejs +62 -0
- package/sample/crud/apps/main/workers/hash-worker.js +17 -0
- package/sample/crud/apps/main/workers/hash.task.js +22 -0
- package/sample/crud/ecosystem.config.cjs +9 -0
- package/sample/crud/mega.config.js +105 -0
- package/sample/crud/package-lock.json +5665 -0
- package/sample/crud/package.json +28 -0
- package/sample/crud/test/apps/main/auth-flow.integration.test.js +177 -0
- package/sample/crud/test/apps/main/auth-service.test.js +93 -0
- package/sample/crud/test/apps/main/chat-bus.test.js +101 -0
- package/sample/crud/test/apps/main/chat-channel.test.js +144 -0
- package/sample/crud/test/apps/main/cron-demo-service.test.js +93 -0
- package/sample/crud/test/apps/main/demo-flow.integration.test.js +386 -0
- package/sample/crud/test/apps/main/email-job.test.js +76 -0
- package/sample/crud/test/apps/main/guide-service.test.js +68 -0
- package/sample/crud/test/apps/main/hash-task.test.js +30 -0
- package/sample/crud/test/apps/main/jobs-demo-service.test.js +88 -0
- package/sample/crud/test/apps/main/logs-demo-service.test.js +85 -0
- package/sample/crud/test/apps/main/metrics-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/note-service.test.js +68 -0
- package/sample/crud/test/apps/main/perf-service.test.js +121 -0
- package/sample/crud/test/apps/main/perf.integration.test.js +202 -0
- package/sample/crud/test/apps/main/redis-demo-service.test.js +98 -0
- package/sample/crud/test/apps/main/tracing-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/upload-demo-service.test.js +61 -0
- package/sample/crud/test/apps/main/user-service.test.js +65 -0
- package/sample/crud/test/apps/main/ws-chat.integration.test.js +232 -0
- package/sample/crud/vitest.config.js +8 -0
- package/sample/crud/yarn.lock +2142 -0
- package/sample/simple/.env.example +15 -0
- package/sample/simple/README.md +52 -0
- package/sample/simple/apps/main/app.config.js +35 -0
- package/sample/simple/apps/main/controllers/pages-controller.js +22 -0
- package/sample/simple/apps/main/locales/client/en.json +3 -0
- package/sample/simple/apps/main/locales/client/ko.json +3 -0
- package/sample/simple/apps/main/locales/server/en.json +23 -0
- package/sample/simple/apps/main/locales/server/ko.json +23 -0
- package/sample/simple/apps/main/public/css/app.css +101 -0
- package/sample/simple/apps/main/public/hello.txt +1 -0
- package/sample/simple/apps/main/public/js/app.js +54 -0
- package/sample/simple/apps/main/public/js/theme-init.js +12 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/simple/apps/main/routes/index.js +9 -0
- package/sample/simple/apps/main/routes/pages.js +12 -0
- package/sample/simple/apps/main/views/index.ejs +56 -0
- package/sample/simple/apps/main/views/layouts/main.ejs +74 -0
- package/sample/simple/ecosystem.config.cjs +10 -0
- package/sample/simple/mega.config.js +27 -0
- package/sample/simple/package-lock.json +1851 -0
- package/sample/simple/package.json +25 -0
- package/sample/simple/test/apps/main/index.test.js +13 -0
- package/sample/simple/vitest.config.js +8 -0
- package/src/adapters/adapter-manager.js +305 -0
- package/src/adapters/adapter-options.js +208 -0
- package/src/adapters/file-adapter.js +350 -0
- package/src/adapters/file-session-adapter.js +363 -0
- package/src/adapters/index.js +38 -0
- package/src/adapters/maria-adapter.js +425 -0
- package/src/adapters/mega-adapter.js +511 -0
- package/src/adapters/mega-bus-adapter.js +81 -0
- package/src/adapters/mega-cache-adapter.js +94 -0
- package/src/adapters/mega-db-adapter.js +72 -0
- package/src/adapters/mega-lock-adapter.js +118 -0
- package/src/adapters/mega-log-sink-adapter.js +46 -0
- package/src/adapters/mega-session-adapter.js +72 -0
- package/src/adapters/mongo-adapter.js +396 -0
- package/src/adapters/nats-adapter.js +370 -0
- package/src/adapters/postgres-adapter.js +341 -0
- package/src/adapters/redis-adapter.js +331 -0
- package/src/adapters/redis-session-adapter.js +261 -0
- package/src/adapters/redlock-adapter.js +385 -0
- package/src/adapters/registry.js +157 -0
- package/src/adapters/sqlite-adapter.js +309 -0
- package/src/auth/index.js +103 -0
- package/src/cli/commands/console-cmd.js +56 -0
- package/src/cli/commands/new.js +101 -0
- package/src/cli/commands/routes.js +107 -0
- package/src/cli/commands/scaffold.js +120 -0
- package/src/cli/commands/test-cmd.js +45 -0
- package/src/cli/generators/index.js +368 -0
- package/src/cli/index.js +472 -0
- package/src/cli/template-engine.js +72 -0
- package/src/cli/ws-hub.js +582 -0
- package/src/core/ajv-mapper.js +80 -0
- package/src/core/boot.js +323 -0
- package/src/core/cluster-metrics.js +278 -0
- package/src/core/config-loader.js +115 -0
- package/src/core/config-validator.js +322 -0
- package/src/core/ctx-builder.js +253 -0
- package/src/core/envelope.js +88 -0
- package/src/core/error-mapper.js +116 -0
- package/src/core/formbody.js +69 -0
- package/src/core/hub-link.js +552 -0
- package/src/core/i18n.js +525 -0
- package/src/core/index.js +63 -0
- package/src/core/mega-app.js +1138 -0
- package/src/core/mega-cluster.js +232 -0
- package/src/core/mega-server.js +176 -0
- package/src/core/mega-service.js +41 -0
- package/src/core/migration-runner.js +196 -0
- package/src/core/multipart.js +282 -0
- package/src/core/openapi.js +114 -0
- package/src/core/router.js +388 -0
- package/src/core/routes-loader.js +57 -0
- package/src/core/scope-registry.js +53 -0
- package/src/core/security.js +275 -0
- package/src/core/services-loader.js +98 -0
- package/src/core/session-cleanup-schedule.js +57 -0
- package/src/core/session-store.js +55 -0
- package/src/core/session.js +414 -0
- package/src/core/static-assets.js +126 -0
- package/src/core/template.js +294 -0
- package/src/core/workers-manager.js +193 -0
- package/src/core/ws-compression.js +112 -0
- package/src/core/ws-controller.js +109 -0
- package/src/core/ws-message.js +176 -0
- package/src/core/ws-upgrade.js +445 -0
- package/src/errors/config-error.js +16 -0
- package/src/errors/http-errors.js +130 -0
- package/src/errors/index.js +19 -0
- package/src/errors/mega-error.js +34 -0
- package/src/eslint-plugin/index.js +15 -0
- package/src/eslint-plugin/no-direct-model-import.js +113 -0
- package/src/index.js +131 -0
- package/src/lib/asp/config.js +83 -0
- package/src/lib/asp/crypto.js +145 -0
- package/src/lib/asp/errors.js +49 -0
- package/src/lib/asp/nonce-cache.js +94 -0
- package/src/lib/asp/plugin.js +263 -0
- package/src/lib/asp/ws-terminator.js +101 -0
- package/src/lib/env-mapper.js +222 -0
- package/src/lib/hub-protocol.js +322 -0
- package/src/lib/index.js +42 -0
- package/src/lib/logger/telegram-core.js +150 -0
- package/src/lib/logger/telegram-transport.js +126 -0
- package/src/lib/mega-brute-force.js +225 -0
- package/src/lib/mega-circuit-breaker.js +412 -0
- package/src/lib/mega-cron.js +169 -0
- package/src/lib/mega-hash.js +179 -0
- package/src/lib/mega-health.js +91 -0
- package/src/lib/mega-job-queue.js +600 -0
- package/src/lib/mega-job-worker.js +295 -0
- package/src/lib/mega-job.js +140 -0
- package/src/lib/mega-logger.js +128 -0
- package/src/lib/mega-metrics.js +661 -0
- package/src/lib/mega-plugin.js +650 -0
- package/src/lib/mega-retry.js +95 -0
- package/src/lib/mega-schedule.js +507 -0
- package/src/lib/mega-shutdown.js +176 -0
- package/src/lib/mega-tracing.js +715 -0
- package/src/lib/mega-worker.js +653 -0
- package/src/lib/worker-runner/process-entry.js +30 -0
- package/src/lib/worker-runner/task-dispatch.js +72 -0
- package/src/lib/worker-runner/thread-entry.js +26 -0
- package/src/models/index.js +7 -0
- package/src/models/mega-model.js +151 -0
- package/src/test/index.js +288 -0
- package/templates/adapter/code.tpl +40 -0
- package/templates/adapter/test.tpl +13 -0
- package/templates/app/app.config.tpl +10 -0
- package/templates/app/route.tpl +10 -0
- package/templates/app/test.tpl +13 -0
- package/templates/channel/code.tpl +38 -0
- package/templates/channel/test.tpl +19 -0
- package/templates/controller/code.tpl +16 -0
- package/templates/controller/route.tpl +9 -0
- package/templates/controller/test.tpl +14 -0
- package/templates/job/code.tpl +23 -0
- package/templates/job/test.tpl +17 -0
- package/templates/locale/code.tpl +3 -0
- package/templates/locale/test.tpl +13 -0
- package/templates/middleware/code.tpl +13 -0
- package/templates/middleware/test.tpl +11 -0
- package/templates/migration/code.tpl +20 -0
- package/templates/migration/test.tpl +14 -0
- package/templates/model/code.tpl +21 -0
- package/templates/model/test.tpl +29 -0
- package/templates/project/app.config.tpl +8 -0
- package/templates/project/app.config.views.tpl +37 -0
- package/templates/project/ecosystem.config.tpl +10 -0
- package/templates/project/env.tpl +12 -0
- package/templates/project/gitignore.tpl +8 -0
- package/templates/project/locales/client/en.json.tpl +3 -0
- package/templates/project/locales/client/ko.json.tpl +3 -0
- package/templates/project/locales/server/en.json.tpl +17 -0
- package/templates/project/locales/server/ko.json.tpl +17 -0
- package/templates/project/mega.config.tpl +11 -0
- package/templates/project/package.tpl +25 -0
- package/templates/project/public/css/app.css +101 -0
- package/templates/project/public/js/app.js +54 -0
- package/templates/project/public/js/theme-init.js +12 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/templates/project/readme.tpl +48 -0
- package/templates/project/route.test.tpl +13 -0
- package/templates/project/route.test.views.tpl +15 -0
- package/templates/project/route.tpl +10 -0
- package/templates/project/route.views.tpl +10 -0
- package/templates/project/views/index.ejs.tpl +58 -0
- package/templates/project/views/layout.ejs.tpl +73 -0
- package/templates/project/vitest.config.tpl +8 -0
- package/templates/route/code.tpl +11 -0
- package/templates/route/test.tpl +26 -0
- package/templates/schedule/code.tpl +19 -0
- package/templates/schedule/test.tpl +17 -0
- package/templates/service/code.tpl +18 -0
- package/templates/service/test.tpl +17 -0
- package/templates/worker/code.tpl +14 -0
- package/templates/worker/task.tpl +13 -0
- package/templates/worker/test.tpl +18 -0
- package/vitest.config.js +33 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
{
|
|
2
|
+
"nav_metrics": "메트릭",
|
|
3
|
+
"nav_tracing": "트레이싱",
|
|
4
|
+
"nav_logs": "로그",
|
|
5
|
+
"nav_upload": "업로드",
|
|
6
|
+
"nav_docs": "API 문서",
|
|
7
|
+
"nav_guide": "가이드",
|
|
8
|
+
"nav_perf": "성능",
|
|
9
|
+
"perf_title": "성능 벤치마크",
|
|
10
|
+
"perf_subtitle": "프레임워크 주요 표면(HTTP·암호화·DB·캐시·세션)의 처리량과 지연을 Node 내장 perf_hooks 로 직접 측정합니다.",
|
|
11
|
+
"perf_form_title": "벤치마크 실행",
|
|
12
|
+
"perf_form_desc": "시나리오와 반복 횟수를 고르고 실행하면 per-iteration 지연을 모아 백분위수를 계산합니다.",
|
|
13
|
+
"perf_scenario_label": "시나리오",
|
|
14
|
+
"perf_iterations_label": "반복",
|
|
15
|
+
"perf_concurrency_label": "동시성",
|
|
16
|
+
"perf_payload_label": "페이로드(byte)",
|
|
17
|
+
"perf_limit_hint": "동시성·페이로드를 비우면 시나리오별 기본값을 씁니다. 안전을 위해 시나리오마다 반복·동시성 상한이 있으며, 초과하면 자동으로 줄이고 결과에 표시합니다.",
|
|
18
|
+
"perf_run_btn": "실행",
|
|
19
|
+
"perf_clear_btn": "결과 지우기",
|
|
20
|
+
"perf_error": "실행 실패:",
|
|
21
|
+
"perf_results_title": "결과",
|
|
22
|
+
"perf_results_desc": "실행할 때마다 맨 위에 한 줄씩 쌓입니다. 시간(ms)은 작을수록, RPS 는 클수록 좋습니다.",
|
|
23
|
+
"perf_results_empty": "아직 실행한 결과가 없습니다.",
|
|
24
|
+
"perf_col_scenario": "시나리오",
|
|
25
|
+
"perf_col_iter": "반복",
|
|
26
|
+
"perf_col_conc": "동시성",
|
|
27
|
+
"perf_col_duration": "총시간(ms)",
|
|
28
|
+
"perf_col_rps": "RPS",
|
|
29
|
+
"perf_col_avg": "평균(ms)",
|
|
30
|
+
"perf_col_min": "최소",
|
|
31
|
+
"perf_col_max": "최대",
|
|
32
|
+
"perf_col_okfail": "성공/실패",
|
|
33
|
+
"guide_index_title": "가이드",
|
|
34
|
+
"guide_index_subtitle": "MEGA-FRAMEWORK 사용 가이드 모음입니다.",
|
|
35
|
+
"guide_index_welcome": "각 주제별 가이드를 골라 읽어보세요. 코드 예시는 서버에서 미리 하이라이트되어 표시됩니다.",
|
|
36
|
+
"guide_toc": "목차",
|
|
37
|
+
"guide_back": "목록으로",
|
|
38
|
+
"metrics_title": "메트릭",
|
|
39
|
+
"metrics_subtitle": "Prometheus /metrics 카운터를 사람이 보기 좋은 카드로 보여줍니다 (웹 프로세스 기준).",
|
|
40
|
+
"metrics_disabled": "메트릭이 비활성화되어 있습니다. mega.config.js 의 health.exposeMetrics 를 확인하세요.",
|
|
41
|
+
"metrics_http_title": "HTTP 요청",
|
|
42
|
+
"metrics_http_desc": "상태 코드 분류별 누적 요청 수입니다.",
|
|
43
|
+
"metrics_http_total": "총 요청 수",
|
|
44
|
+
"metrics_process_title": "프로세스",
|
|
45
|
+
"metrics_process_desc": "현재 웹 프로세스의 메모리·가동시간·CPU 사용량입니다.",
|
|
46
|
+
"metrics_process_heap": "힙 사용량",
|
|
47
|
+
"metrics_process_rss": "RSS",
|
|
48
|
+
"metrics_process_uptime": "가동 시간",
|
|
49
|
+
"metrics_process_cpu": "CPU 시간",
|
|
50
|
+
"metrics_jobs_title": "잡 큐",
|
|
51
|
+
"metrics_jobs_desc": "잡 처리 이벤트 누적 수입니다 (잡 데모와 연동).",
|
|
52
|
+
"metrics_jobs_enqueued": "등록",
|
|
53
|
+
"metrics_jobs_processed": "처리",
|
|
54
|
+
"metrics_jobs_retried": "재시도",
|
|
55
|
+
"metrics_jobs_dlq": "DLQ",
|
|
56
|
+
"metrics_ws_title": "WebSocket",
|
|
57
|
+
"metrics_ws_desc": "WS 메시지 누적 수입니다 (채팅 데모와 연동).",
|
|
58
|
+
"metrics_ws_total": "총 메시지 수",
|
|
59
|
+
"metrics_ws_empty": "아직 WS 메시지가 없습니다.",
|
|
60
|
+
"metrics_routes_title": "라우트별 요청 수",
|
|
61
|
+
"metrics_routes_desc": "요청이 많은 상위 라우트입니다.",
|
|
62
|
+
"metrics_routes_empty": "아직 기록된 요청이 없습니다.",
|
|
63
|
+
"metrics_routes_route": "라우트",
|
|
64
|
+
"metrics_routes_count": "요청 수",
|
|
65
|
+
"metrics_raw_hint": "원본(Prometheus scrape 포맷):",
|
|
66
|
+
"tracing_title": "분산 추적",
|
|
67
|
+
"tracing_subtitle": "현재 요청의 trace_id 와 최근 trace 를 Zipkin 으로 잇습니다.",
|
|
68
|
+
"tracing_notice_generated": "trace 를 생성했습니다. 잠시 후 Zipkin 에서 확인할 수 있습니다.",
|
|
69
|
+
"tracing_disabled": "트레이싱이 비활성화되어 있습니다. .env 의 MEGA_OTEL_ENABLED=true 후 재시작하세요.",
|
|
70
|
+
"tracing_current_title": "현재 요청 trace",
|
|
71
|
+
"tracing_current_desc": "이 페이지를 연 요청의 trace_id 입니다 (응답 헤더 x-trace-id 에도 실립니다).",
|
|
72
|
+
"tracing_trace_id": "trace_id",
|
|
73
|
+
"tracing_span_id": "span_id",
|
|
74
|
+
"tracing_open_zipkin": "Zipkin 에서 보기",
|
|
75
|
+
"tracing_no_current": "현재 trace 가 없습니다 (트레이싱 비활성).",
|
|
76
|
+
"tracing_generate": "trace 생성",
|
|
77
|
+
"tracing_generate_hint": "사용자 span 으로 DB 핑을 한 번 실행해 다층 span 트리를 만듭니다.",
|
|
78
|
+
"tracing_zipkin_title": "Zipkin",
|
|
79
|
+
"tracing_zipkin_desc": "trace 는 OTLP→collector→Zipkin 으로 전달됩니다.",
|
|
80
|
+
"tracing_service": "서비스 이름",
|
|
81
|
+
"tracing_ui": "Zipkin UI",
|
|
82
|
+
"tracing_export_hint": "전송에 약간의 지연이 있어, 생성 직후엔 Zipkin 에 잠시 뒤 나타납니다.",
|
|
83
|
+
"tracing_recent_title": "최근 trace",
|
|
84
|
+
"tracing_recent_desc": "최근 기록된 trace_id 목록입니다.",
|
|
85
|
+
"tracing_recent_empty": "아직 기록된 trace 가 없습니다.",
|
|
86
|
+
"tracing_recent_at": "시각",
|
|
87
|
+
"tracing_recent_route": "라우트",
|
|
88
|
+
"tracing_view": "보기",
|
|
89
|
+
"logs_title": "구조적 로깅",
|
|
90
|
+
"logs_subtitle": "pino 로그를 emit 하고 trace_id·민감필드 마스킹을 시연합니다.",
|
|
91
|
+
"logs_notice_emitted": "로그를 emit 했습니다. 서버 콘솔에서 구조적 출력을 확인하세요.",
|
|
92
|
+
"logs_emit_title": "로그 emit",
|
|
93
|
+
"logs_emit_desc": "선택한 레벨로 로그 1건을 실제 logger 로 내보냅니다.",
|
|
94
|
+
"logs_level": "레벨",
|
|
95
|
+
"logs_message": "메시지",
|
|
96
|
+
"logs_message_ph": "예: 데모 로그 한 줄",
|
|
97
|
+
"logs_emit_btn": "emit",
|
|
98
|
+
"logs_redact_hint": "로그 payload 의 token/password/secret 은 console 출력에서 [Redacted] 로 마스킹되고, 활성 trace_id 가 자동 첨부됩니다 (ADR-141). 서버 콘솔에서 확인하세요.",
|
|
99
|
+
"logs_recent_title": "최근 emit",
|
|
100
|
+
"logs_recent_desc": "emit 한 로그의 안전한 메타데이터입니다 (시크릿 제외).",
|
|
101
|
+
"logs_recent_empty": "아직 emit 한 로그가 없습니다.",
|
|
102
|
+
"logs_recent_at": "시각",
|
|
103
|
+
"logs_recent_trace": "trace_id",
|
|
104
|
+
"upload_title": "파일 업로드",
|
|
105
|
+
"upload_subtitle": "multipart 업로드를 프로젝트 폴더에 저장하고 메타·다운로드 링크를 보여줍니다.",
|
|
106
|
+
"upload_form_title": "업로드",
|
|
107
|
+
"upload_form_desc": "이미지·PDF·텍스트 파일을 올립니다 (MIME·크기·개수 게이트).",
|
|
108
|
+
"upload_file_label": "파일 선택",
|
|
109
|
+
"upload_constraints": "허용: 이미지/PDF/텍스트 · 최대 5MB · 한 번에 3개까지",
|
|
110
|
+
"upload_submit": "업로드",
|
|
111
|
+
"upload_error": "업로드 실패:",
|
|
112
|
+
"upload_recent_title": "최근 업로드",
|
|
113
|
+
"upload_recent_desc": "저장된 파일의 메타데이터입니다 (파일명 클릭 시 다운로드).",
|
|
114
|
+
"upload_recent_empty": "아직 업로드한 파일이 없습니다.",
|
|
115
|
+
"upload_dir_hint": "저장 위치(프로젝트 루트 기준):",
|
|
116
|
+
"upload_recent_at": "시각",
|
|
117
|
+
"upload_recent_name": "파일명",
|
|
118
|
+
"upload_recent_path": "저장 경로",
|
|
119
|
+
"upload_recent_mime": "MIME",
|
|
120
|
+
"upload_recent_size": "크기",
|
|
121
|
+
"nav_home": "홈",
|
|
122
|
+
"nav_users": "사용자",
|
|
123
|
+
"nav_demo": "데모",
|
|
124
|
+
"theme_toggle": "테마 전환",
|
|
125
|
+
"footer_built": "MEGA-FRAMEWORK 로 제작 · Bootstrap 5",
|
|
126
|
+
"home_title": "사용자 CRUD 관리",
|
|
127
|
+
"home_subtitle": "postgres 기반 사용자 리소스를 Bootstrap 5 관리 UI 로 생성·조회·수정·삭제합니다. 같은 서비스 계층을 JSON REST API 도 함께 노출합니다.",
|
|
128
|
+
"home_cta_manage": "사용자 관리",
|
|
129
|
+
"home_cta_api": "JSON API (/users)",
|
|
130
|
+
"home_api_note": "모든 요청은 정본 계층 흐름을 따릅니다:",
|
|
131
|
+
"users_title": "사용자 목록",
|
|
132
|
+
"users_new": "사용자 추가",
|
|
133
|
+
"users_empty": "아직 사용자가 없습니다.",
|
|
134
|
+
"col_id": "ID",
|
|
135
|
+
"col_name": "이름",
|
|
136
|
+
"col_email": "이메일",
|
|
137
|
+
"col_created": "생성일",
|
|
138
|
+
"col_actions": "관리",
|
|
139
|
+
"action_edit": "수정",
|
|
140
|
+
"action_delete": "삭제",
|
|
141
|
+
"delete_confirm_title": "삭제 확인",
|
|
142
|
+
"delete_confirm_body": "정말 삭제할까요?",
|
|
143
|
+
"delete_confirm_cancel": "취소",
|
|
144
|
+
"delete_confirm_ok": "삭제",
|
|
145
|
+
"new_title": "사용자 추가",
|
|
146
|
+
"edit_title": "사용자 수정",
|
|
147
|
+
"field_name": "이름",
|
|
148
|
+
"field_name_ph": "예: 홍길동",
|
|
149
|
+
"field_name_required": "이름을 입력하세요.",
|
|
150
|
+
"field_email": "이메일",
|
|
151
|
+
"field_email_ph": "예: hong@example.com",
|
|
152
|
+
"field_email_required": "올바른 이메일을 입력하세요.",
|
|
153
|
+
"btn_create": "생성",
|
|
154
|
+
"btn_save": "저장",
|
|
155
|
+
"btn_cancel": "취소",
|
|
156
|
+
"notice_created": "사용자를 생성했습니다.",
|
|
157
|
+
"notice_updated": "사용자를 수정했습니다.",
|
|
158
|
+
"notice_deleted": "사용자를 삭제했습니다.",
|
|
159
|
+
"field_password": "비밀번호",
|
|
160
|
+
"field_password_hint": "최소 8자 이상 입력하세요.",
|
|
161
|
+
"login_title": "로그인",
|
|
162
|
+
"login_failed": "이메일 또는 비밀번호가 올바르지 않습니다.",
|
|
163
|
+
"login_locked": "로그인 시도가 너무 많습니다. 잠시 후 다시 시도하세요.",
|
|
164
|
+
"login_no_account": "계정이 없으신가요?",
|
|
165
|
+
"register_title": "회원가입",
|
|
166
|
+
"register_have_account": "이미 계정이 있으신가요?",
|
|
167
|
+
"btn_login": "로그인",
|
|
168
|
+
"btn_logout": "로그아웃",
|
|
169
|
+
"btn_register": "가입",
|
|
170
|
+
"auth_notice_logged_out": "로그아웃되었습니다.",
|
|
171
|
+
"auth_notice_registered": "가입이 완료되었습니다.",
|
|
172
|
+
"nav_notes": "노트 (Mongo)",
|
|
173
|
+
"nav_redis": "캐시 (Redis)",
|
|
174
|
+
"nav_ws": "채팅 (WS+ASP)",
|
|
175
|
+
"notes_title": "노트 (MongoDB)",
|
|
176
|
+
"notes_subtitle": "MongoDB 도큐먼트 어댑터로 notes 컬렉션을 생성·조회·수정·삭제합니다.",
|
|
177
|
+
"notes_new": "노트 추가",
|
|
178
|
+
"notes_empty": "아직 노트가 없습니다.",
|
|
179
|
+
"notes_new_title": "노트 추가",
|
|
180
|
+
"notes_edit_title": "노트 수정",
|
|
181
|
+
"notes_field_title": "제목",
|
|
182
|
+
"notes_field_title_ph": "예: 회의 메모",
|
|
183
|
+
"notes_field_title_required": "제목을 입력하세요.",
|
|
184
|
+
"notes_field_body": "본문",
|
|
185
|
+
"notes_field_body_ph": "내용을 입력하세요 (선택).",
|
|
186
|
+
"notes_notice_created": "노트를 생성했습니다.",
|
|
187
|
+
"notes_notice_updated": "노트를 수정했습니다.",
|
|
188
|
+
"notes_notice_deleted": "노트를 삭제했습니다.",
|
|
189
|
+
"redis_title": "Redis 데모",
|
|
190
|
+
"redis_subtitle": "Redis 어댑터로 방문 카운터(원자적 INCR/EXPIRE)와 쿼리 결과 캐시(GET/SET/TTL/DEL)를 시연합니다.",
|
|
191
|
+
"redis_visits_title": "방문 카운터",
|
|
192
|
+
"redis_visits_desc": "이 페이지를 열 때마다 카운터를 원자적으로 1씩 올립니다. 당일 카운터는 2일 후 자동 만료됩니다.",
|
|
193
|
+
"redis_visits_total": "누적 방문",
|
|
194
|
+
"redis_visits_today": "오늘 방문",
|
|
195
|
+
"redis_cache_title": "쿼리 결과 캐시",
|
|
196
|
+
"redis_cache_desc": "사용자 수를 Redis 에 30초간 캐싱합니다. 캐시에 있으면 hit(SQL 미실행), 없으면 miss(SQL 재계산)입니다.",
|
|
197
|
+
"redis_cache_hit": "캐시 적중",
|
|
198
|
+
"redis_cache_miss": "캐시 미스",
|
|
199
|
+
"redis_cache_ttl": "남은 TTL",
|
|
200
|
+
"redis_cache_reload": "다시 조회",
|
|
201
|
+
"redis_cache_clear": "캐시 비우기",
|
|
202
|
+
"redis_notice_cache_cleared": "캐시를 비웠습니다.",
|
|
203
|
+
"home_demo_notes": "Mongo 노트 데모",
|
|
204
|
+
"home_demo_redis": "Redis 캐시 데모",
|
|
205
|
+
"home_demo_ws": "WebSocket 채팅 데모",
|
|
206
|
+
"ws_title": "실시간 채팅 (WebSocket + ASP)",
|
|
207
|
+
"ws_subtitle": "WASM MegaSocket 으로 /ws/chat 에 접속합니다. 메시지는 ASP 로 암호화(E: 프레임)되어 브라우저에서 직접 암복호화되고, 서버는 채널 전체에 broadcast 합니다.",
|
|
208
|
+
"ws_asp_badge": "ASP 암호화",
|
|
209
|
+
"ws_asp_badge_title": "모든 메시지가 AES-256-GCM(E: 프레임)으로 암호화되어 송수신됩니다.",
|
|
210
|
+
"ws_status_connecting": "연결 중…",
|
|
211
|
+
"ws_status_open": "연결됨",
|
|
212
|
+
"ws_status_closed": "연결 끊김",
|
|
213
|
+
"ws_status_error": "오류",
|
|
214
|
+
"ws_online": "접속자 {n}명",
|
|
215
|
+
"ws_presence_join": "{user} 님이 입장했습니다.",
|
|
216
|
+
"ws_presence_leave": "{user} 님이 퇴장했습니다.",
|
|
217
|
+
"ws_empty": "아직 메시지가 없습니다. 첫 메시지를 보내보세요.",
|
|
218
|
+
"ws_input_placeholder": "메시지를 입력하세요…",
|
|
219
|
+
"ws_send": "보내기",
|
|
220
|
+
"ws_encrypted_note": "메시지는 ASP(AES-256-GCM) E: 프레임으로 암호화되어 전송됩니다.",
|
|
221
|
+
"nav_cron": "스케줄러 (Cron)",
|
|
222
|
+
"nav_jobs": "잡 큐 (NATS)",
|
|
223
|
+
"nav_worker": "워커 (Threads)",
|
|
224
|
+
"home_demo_cron": "스케줄러 데모",
|
|
225
|
+
"home_demo_jobs": "잡 큐 데모",
|
|
226
|
+
"home_demo_worker": "CPU 워커 데모",
|
|
227
|
+
"cron_title": "스케줄러 데모 (MegaSchedule)",
|
|
228
|
+
"cron_subtitle": "mega scheduler 프로세스가 30초마다 Redis 카운터를 원자적으로 올립니다. 다중 인스턴스 중복 실행은 분산 락(redlock leader election)으로 막습니다.",
|
|
229
|
+
"cron_counter_title": "실행 카운터",
|
|
230
|
+
"cron_counter_desc": "스케줄이 돌 때마다(또는 아래 수동 실행) Redis 카운터를 1씩 올립니다.",
|
|
231
|
+
"cron_counter_total": "누적 실행 횟수",
|
|
232
|
+
"cron_run_now": "지금 실행",
|
|
233
|
+
"cron_run_hint": "cron 시각을 기다리지 않고 같은 작업을 즉시 1회 실행합니다(수동 트리거).",
|
|
234
|
+
"cron_next_title": "다음 실행 시각",
|
|
235
|
+
"cron_next_desc": "croner 가 계산한 다음 다섯 번의 실행 예정 시각입니다(타이머 미가동 순수 계산).",
|
|
236
|
+
"cron_next_soonest": "가장 가까움",
|
|
237
|
+
"cron_history_title": "최근 실행 이력",
|
|
238
|
+
"cron_history_desc": "Redis LIST 에 최근 10건을 남깁니다(LPUSH + LTRIM). 자동 실행과 수동 실행이 함께 쌓입니다.",
|
|
239
|
+
"cron_history_empty": "아직 실행 이력이 없습니다. scheduler 프로세스를 켜거나 위에서 수동 실행해 보세요.",
|
|
240
|
+
"cron_history_at": "실행 시각",
|
|
241
|
+
"cron_history_source": "출처",
|
|
242
|
+
"cron_source_schedule": "스케줄",
|
|
243
|
+
"cron_source_manual": "수동",
|
|
244
|
+
"cron_notice_triggered": "스케줄 작업을 즉시 1회 실행했습니다.",
|
|
245
|
+
"jobs_title": "잡 큐 데모 (MegaJob)",
|
|
246
|
+
"jobs_subtitle": "EmailJob 을 NATS JetStream 에 enqueue 하면 mega worker 프로세스가 소비합니다. 일시 실패는 재시도되고, 영구 실패는 DLQ 로 격리됩니다.",
|
|
247
|
+
"jobs_enqueue_title": "이메일 발송 잡 넣기",
|
|
248
|
+
"jobs_enqueue_desc": "실제 발송은 하지 않고 처리만 시뮬레이션합니다. 모드로 성공/재시도/영구실패 흐름을 시연합니다.",
|
|
249
|
+
"jobs_enqueue_btn": "큐에 넣기",
|
|
250
|
+
"jobs_reload": "새로고침",
|
|
251
|
+
"jobs_field_to": "받는 사람",
|
|
252
|
+
"jobs_field_mode": "모드",
|
|
253
|
+
"jobs_mode_ok": "성공",
|
|
254
|
+
"jobs_mode_flaky": "재시도",
|
|
255
|
+
"jobs_mode_fail": "영구 실패",
|
|
256
|
+
"jobs_mode_ok_hint": "첫 시도에 바로 성공",
|
|
257
|
+
"jobs_mode_flaky_hint": "1번째 시도 실패 → 재시도 → 2번째 성공",
|
|
258
|
+
"jobs_mode_fail_hint": "매 시도 실패 → 재시도 소진 후 DLQ 격리",
|
|
259
|
+
"jobs_dlq_title": "DLQ (격리된 잡)",
|
|
260
|
+
"jobs_dlq_desc": "재시도를 모두 소진한 영구 실패 잡이 모이는 곳입니다(NATS 스트림). 원인 분석·재처리용.",
|
|
261
|
+
"jobs_dlq_empty": "아직 DLQ 로 간 잡이 없습니다.",
|
|
262
|
+
"jobs_dlq_failed_at": "실패 시각",
|
|
263
|
+
"jobs_dlq_deliveries": "전달 횟수",
|
|
264
|
+
"jobs_dlq_error": "오류",
|
|
265
|
+
"jobs_dlq_payload": "페이로드",
|
|
266
|
+
"jobs_events_title": "처리 이벤트",
|
|
267
|
+
"jobs_events_desc": "워커가 각 시도마다 남긴 타임라인입니다(최신순). 재시도가 일어나면 같은 잡이 여러 번 보입니다.",
|
|
268
|
+
"jobs_events_empty": "아직 처리된 잡이 없습니다. 위에서 잡을 넣고 mega worker 프로세스를 켜 보세요.",
|
|
269
|
+
"jobs_events_at": "시각",
|
|
270
|
+
"jobs_events_id": "잡 ID",
|
|
271
|
+
"jobs_events_attempt": "시도",
|
|
272
|
+
"jobs_events_status": "상태",
|
|
273
|
+
"jobs_status_sent": "발송됨",
|
|
274
|
+
"jobs_status_retry": "재시도",
|
|
275
|
+
"jobs_status_failed": "실패",
|
|
276
|
+
"jobs_notice_enqueued": "잡을 큐에 넣었습니다. 워커가 처리하면 아래 이벤트에 나타납니다.",
|
|
277
|
+
"worker_title": "CPU 워커 데모 (MegaWorker)",
|
|
278
|
+
"worker_subtitle": "SHA-256 N회 반복 같은 CPU-bound 작업을 worker_threads 풀에서 돌립니다. 계산 도중에도 서버가 다른 요청에 즉시 응답하는지(메인 스레드 non-block) 하트비트로 확인합니다.",
|
|
279
|
+
"worker_run_title": "해시 계산 실행",
|
|
280
|
+
"worker_run_desc": "반복 횟수만큼 SHA-256 을 체인 해시합니다. 메인 스레드가 아니라 워커 스레드에서 돕니다.",
|
|
281
|
+
"worker_run_btn": "워커에서 실행",
|
|
282
|
+
"worker_rounds_label": "반복 횟수",
|
|
283
|
+
"worker_pool_mode": "모드",
|
|
284
|
+
"worker_pool_size": "풀 크기",
|
|
285
|
+
"worker_error": "실행 실패:",
|
|
286
|
+
"worker_result_rounds": "반복 횟수",
|
|
287
|
+
"worker_result_ms": "소요 시간",
|
|
288
|
+
"worker_result_digest": "최종 해시",
|
|
289
|
+
"worker_heartbeat_title": "메인 스레드 하트비트",
|
|
290
|
+
"worker_heartbeat_desc": "1초마다 서버에 ping 을 보냅니다. 워커가 계산하는 동안에도 이 지연이 작게 유지되면 메인 스레드가 안 막힌 것입니다.",
|
|
291
|
+
"worker_heartbeat_count": "ping 횟수",
|
|
292
|
+
"worker_heartbeat_latency": "최근 왕복 지연",
|
|
293
|
+
"worker_heartbeat_last": "마지막 ping",
|
|
294
|
+
"worker_heartbeat_proof": "계산을 메인 스레드에서 했다면 이 ping 들이 계산이 끝날 때까지 통째로 멈춥니다. 워커 스레드라 멈추지 않습니다.",
|
|
295
|
+
"server": {
|
|
296
|
+
"internal": "내부 서버 오류"
|
|
297
|
+
},
|
|
298
|
+
"validation": {
|
|
299
|
+
"failed": "입력값이 올바르지 않습니다."
|
|
300
|
+
},
|
|
301
|
+
"auth": {
|
|
302
|
+
"required": "인증이 필요합니다."
|
|
303
|
+
},
|
|
304
|
+
"csrf": {
|
|
305
|
+
"invalid_token": "Invalid csrf token",
|
|
306
|
+
"missing_secret": "Missing csrf secret"
|
|
307
|
+
},
|
|
308
|
+
"upload": {
|
|
309
|
+
"unsupported_media_type": "허용되지 않는 파일 형식입니다.",
|
|
310
|
+
"too_large": "파일 크기가 허용 한도를 초과했습니다.",
|
|
311
|
+
"not_found": "파일을 찾을 수 없습니다."
|
|
312
|
+
},
|
|
313
|
+
"guide": {
|
|
314
|
+
"not_found": "가이드를 찾을 수 없습니다."
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 웹(MPA) 인증 가드 + 뷰용 현재 사용자 helper (ADR-155).
|
|
4
|
+
*
|
|
5
|
+
* 프레임워크 `requireAuth`(mega-framework/auth)는 비로그인 시 401 을 throw 한다 — JSON 응답을 받는
|
|
6
|
+
* REST API(`/users`)에 맞는 동작이다. 반면 브라우저로 보는 관리 UI(`/admin/**`)는 401 본문 대신
|
|
7
|
+
* 로그인 페이지로 보내는 게 자연스럽다. 그래서 웹 라우트 전용 **리다이렉트** 가드를 따로 둔다.
|
|
8
|
+
*
|
|
9
|
+
* @module middleware/web-auth
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** 로그인 페이지 경로 — 가드와 뷰가 같은 출처를 보도록 상수화. */
|
|
13
|
+
export const LOGIN_PATH = '/auth/login'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 세션에서 현재 로그인 사용자를 뷰 locals 형태로 뽑는다. 비로그인이면 null.
|
|
17
|
+
* 로그인 시 세션에 `userId`+`userName` 을 심어두므로(컨트롤러), navbar 는 DB 재조회 없이 이름을 보여준다.
|
|
18
|
+
* @param {import('fastify').FastifyRequest} req
|
|
19
|
+
* @returns {{ id: any, name: string } | null}
|
|
20
|
+
*/
|
|
21
|
+
export function currentUser(req) {
|
|
22
|
+
const session = /** @type {any} */ (req).session
|
|
23
|
+
if (session?.userId == null) return null
|
|
24
|
+
return { id: session.userId, name: typeof session.userName === 'string' ? session.userName : '' }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `/admin/**` 보호용 `before` 가드. 비로그인 요청을 로그인 페이지로 리다이렉트한다(401 throw 대신).
|
|
29
|
+
* Fastify preHandler 에서 reply 를 보내면(redirect) 핸들러 체인이 단락된다.
|
|
30
|
+
* @param {import('fastify').FastifyRequest} req
|
|
31
|
+
* @param {import('fastify').FastifyReply} reply
|
|
32
|
+
* @returns {Promise<void>}
|
|
33
|
+
*/
|
|
34
|
+
export async function webRequireAuth(req, reply) {
|
|
35
|
+
const session = /** @type {any} */ (req).session
|
|
36
|
+
if (session?.userId == null) {
|
|
37
|
+
req.log?.debug?.({ route: req.url }, 'web.auth redirect — not logged in')
|
|
38
|
+
return /** @type {any} */ (reply).redirect(LOGIN_PATH)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* WS upgrade 인증 가드 (ADR-158) — 로그인 세션만 /ws/chat 연결 허용.
|
|
4
|
+
*
|
|
5
|
+
* HTTP `'upgrade'` 핸드셰이크는 Fastify 요청 파이프라인을 타지 않아 `req.session` 이 없다(raw
|
|
6
|
+
* IncomingMessage). 그래서 프레임워크 `readSession`(ADR-159)으로 쿠키 → 서명 검증 → 세션 스토어
|
|
7
|
+
* 로드를 직접 수행해 신원을 확인한다. 비로그인/위조/만료는 모두 `false` 반환 → upgrade 401 거부
|
|
8
|
+
* (ws-upgrade.js handleUpgrade, fail-closed). 반환한 신원 객체는 채널 onConnect 의 `ctx.auth` 가 된다.
|
|
9
|
+
*
|
|
10
|
+
* 세션 스토어·시크릿 출처:
|
|
11
|
+
* - store: `router.app.sessionStore`(app.config.js 의 session.store 가 연결한 redis 인스턴스).
|
|
12
|
+
* - secret: `SESSION_SECRET`(mega.config.js 의 server.sessionSecret 과 동일 출처) — 쿠키 HMAC 검증용.
|
|
13
|
+
*
|
|
14
|
+
* @module middleware/ws-auth
|
|
15
|
+
*/
|
|
16
|
+
import { readSession } from 'mega-framework'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `/ws/chat` upgrade 인증 `before` 미들웨어를 만든다(앱 바인딩 팩토리).
|
|
20
|
+
*
|
|
21
|
+
* 라우트 파일에서 `router.app` 으로 앱을 얻어 세션 스토어에 닿게 한다 — `before(req)` 는 raw req 만
|
|
22
|
+
* 받으므로 클로저로 스토어를 주입한다.
|
|
23
|
+
*
|
|
24
|
+
* @param {import('mega-framework').MegaApp} app - 바인딩 앱(router.app). sessionStore 출처.
|
|
25
|
+
* @returns {(req: import('node:http').IncomingMessage) => Promise<false | { userId: string, sessionId: string, userName: string }>}
|
|
26
|
+
* 비로그인이면 false(401), 로그인이면 ctx.auth 가 될 신원 객체.
|
|
27
|
+
*/
|
|
28
|
+
export function makeWsRequireAuth(app) {
|
|
29
|
+
const secret = process.env.SESSION_SECRET
|
|
30
|
+
if (typeof secret !== 'string' || secret.length === 0) {
|
|
31
|
+
// 시크릿 없는 세션 검증은 불가 — 부팅 시 fail-fast(silent 진행 금지).
|
|
32
|
+
throw new Error('makeWsRequireAuth: SESSION_SECRET is required (set it in .env).')
|
|
33
|
+
}
|
|
34
|
+
return async function wsRequireAuth(req) {
|
|
35
|
+
const sess = await readSession(req, { store: app.sessionStore, secret })
|
|
36
|
+
// 로그인 신원은 컨트롤러가 세션에 심은 userId/userName(web-auth.js currentUser 와 동일 키).
|
|
37
|
+
const userId = sess?.data.userId
|
|
38
|
+
if (userId == null) {
|
|
39
|
+
app.fastify.log?.debug?.({ route: req.url }, 'ws.auth deny — not logged in (401)')
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
userId: String(userId),
|
|
44
|
+
sessionId: sess.sid,
|
|
45
|
+
userName: typeof sess.data.userName === 'string' ? sess.data.userName : '',
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 마이그레이션 create-users (20260606000001). up = users 테이블 생성, down = 제거(ADR-149).
|
|
4
|
+
* `mega migrate`(up)·`mega migrate:down`·`mega migrate:status` 로 실행. 적용 이력은 대상 DB 의
|
|
5
|
+
* `mega_migrations` 테이블이 추적하며, 각 마이그레이션은 트랜잭션으로 감싸 적용된다.
|
|
6
|
+
*
|
|
7
|
+
* @param {{ query: (sql: string, params?: any[]) => Promise<any> }} db - 대상 DB 어댑터(트랜잭션 내 query).
|
|
8
|
+
* @returns {Promise<void>}
|
|
9
|
+
*/
|
|
10
|
+
export async function up(db) {
|
|
11
|
+
await db.query(`
|
|
12
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
13
|
+
id SERIAL PRIMARY KEY,
|
|
14
|
+
name TEXT NOT NULL,
|
|
15
|
+
email TEXT NOT NULL UNIQUE,
|
|
16
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
17
|
+
)
|
|
18
|
+
`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {{ query: (sql: string, params?: any[]) => Promise<any> }} db
|
|
23
|
+
* @returns {Promise<void>}
|
|
24
|
+
*/
|
|
25
|
+
export async function down(db) {
|
|
26
|
+
await db.query('DROP TABLE IF EXISTS users')
|
|
27
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 마이그레이션 add-auth-to-users (20260606000002). 로그인을 위해 users 에 인증 컬럼을 더한다(ADR-155).
|
|
4
|
+
* `mega migrate`(up)·`mega migrate:down` 로 실행하며, 각 마이그레이션은 트랜잭션으로 감싸 적용된다.
|
|
5
|
+
*
|
|
6
|
+
* 컬럼은 모두 nullable — 기존 row(name+email 만 있는 사용자)를 깨지 않는다. password_hash 가 비어 있는
|
|
7
|
+
* 계정은 로그인 대상이 아니며, /register 로 만든 계정만 해시를 가진다.
|
|
8
|
+
*
|
|
9
|
+
* - password_hash: scrypt 해시 문자열(`$scrypt$…`, src/lib/mega-hash.js). 평문 비밀번호는 저장하지 않는다.
|
|
10
|
+
* - last_login_at: 마지막 로그인 시각(성공 시 갱신). 운영 관측·"최근 로그인" 표시에 쓴다.
|
|
11
|
+
*
|
|
12
|
+
* brute-force 실패 카운트·잠금은 DB 컬럼이 아니라 redis 에 둔다(원자적 INCR, src/lib/mega-brute-force.js,
|
|
13
|
+
* ADR-049/130) — 여기 failed_login_count 같은 컬럼을 두지 않는 이유다.
|
|
14
|
+
*
|
|
15
|
+
* @param {{ query: (sql: string, params?: any[]) => Promise<any> }} db - 대상 DB 어댑터(트랜잭션 내 query).
|
|
16
|
+
* @returns {Promise<void>}
|
|
17
|
+
*/
|
|
18
|
+
export async function up(db) {
|
|
19
|
+
await db.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS password_hash TEXT')
|
|
20
|
+
await db.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMPTZ')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {{ query: (sql: string, params?: any[]) => Promise<any> }} db
|
|
25
|
+
* @returns {Promise<void>}
|
|
26
|
+
*/
|
|
27
|
+
export async function down(db) {
|
|
28
|
+
await db.query('ALTER TABLE users DROP COLUMN IF EXISTS last_login_at')
|
|
29
|
+
await db.query('ALTER TABLE users DROP COLUMN IF EXISTS password_hash')
|
|
30
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import { MegaModel } from 'mega-framework'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Note 모델 — `notes` 컬렉션(globalKey 'mongo' 의 MongoDB, ADR-108). SQL 모델(User)과 달리 `this.db` 가
|
|
7
|
+
* MongoClient 의 `Db` 핸들이라, ORM 없이 도큐먼트 API(`collection().find/insertOne/...`)로 도메인 메서드를
|
|
8
|
+
* 직접 작성한다(ADR-009). 모델은 서비스만 import 한다(라우트·컨트롤러 직접 import 는 `mega/no-direct-model-import` 차단, ADR-022).
|
|
9
|
+
*
|
|
10
|
+
* 식별자는 mongo `_id`(ObjectId) 대신 자체 `id`(UUID v4) 필드를 쓴다 — 라우트 파라미터/폼에서 다루기 쉽고,
|
|
11
|
+
* `mongodb` 드라이버의 `ObjectId` 를 샘플에서 import 하지 않아도 돼 driver 결합을 피한다. 읽기에서는
|
|
12
|
+
* `_id` 를 projection 으로 제외해 도메인 형태만 노출한다.
|
|
13
|
+
*
|
|
14
|
+
* @typedef {{ id: string, title: string, body: string, created_at: string }} NoteDoc
|
|
15
|
+
*/
|
|
16
|
+
export class Note extends MegaModel {
|
|
17
|
+
static adapter = 'mongo'
|
|
18
|
+
static table = 'notes'
|
|
19
|
+
|
|
20
|
+
/** 읽기 공통 projection — 내부 `_id` 를 빼고 도메인 필드만 돌려준다. */
|
|
21
|
+
static #projection = { projection: { _id: 0 } }
|
|
22
|
+
|
|
23
|
+
/** @returns {import('mongodb').Collection} `notes` 컬렉션 핸들. */
|
|
24
|
+
static get collection() {
|
|
25
|
+
return this.db.collection(this.table)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 최신순 전체 목록. @returns {Promise<NoteDoc[]>} */
|
|
29
|
+
static async list() {
|
|
30
|
+
return /** @type {Promise<NoteDoc[]>} */ (
|
|
31
|
+
this.collection.find({}, Note.#projection).sort({ created_at: -1 }).toArray()
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 단건 조회(없으면 null). @param {string} id @returns {Promise<NoteDoc | null>} */
|
|
36
|
+
static async findById(id) {
|
|
37
|
+
return /** @type {Promise<NoteDoc | null>} */ (this.collection.findOne({ id }, Note.#projection))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 생성 — UUID·생성시각을 부여해 삽입한다. @param {{ title: string, body: string }} input @returns {Promise<NoteDoc>} */
|
|
41
|
+
static async create({ title, body }) {
|
|
42
|
+
/** @type {NoteDoc} */
|
|
43
|
+
const doc = { id: randomUUID(), title, body, created_at: new Date().toISOString() }
|
|
44
|
+
await this.collection.insertOne({ ...doc })
|
|
45
|
+
return doc
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 수정 — 주어진 필드만 $set 한다(undefined 는 제외해 기존 값 보존). 대상이 없으면 null.
|
|
50
|
+
* @param {string} id @param {{ title?: string, body?: string }} patch @returns {Promise<NoteDoc | null>}
|
|
51
|
+
*/
|
|
52
|
+
static async update(id, { title, body }) {
|
|
53
|
+
/** @type {Record<string, string>} */
|
|
54
|
+
const set = {}
|
|
55
|
+
if (title !== undefined) set.title = title
|
|
56
|
+
if (body !== undefined) set.body = body
|
|
57
|
+
if (Object.keys(set).length > 0) await this.collection.updateOne({ id }, { $set: set })
|
|
58
|
+
return this.findById(id)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 삭제 — 삭제 건수가 1 이상이면 true. @param {string} id @returns {Promise<boolean>} */
|
|
62
|
+
static async remove(id) {
|
|
63
|
+
const { deletedCount } = await this.collection.deleteOne({ id })
|
|
64
|
+
return deletedCount > 0
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 전체 도큐먼트 수(캐시 데모용 카운트 쿼리). @returns {Promise<number>} */
|
|
68
|
+
static async count() {
|
|
69
|
+
return this.collection.countDocuments({})
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { MegaModel } from 'mega-framework'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* User 모델 — `users` 테이블(globalKey 'primary' 의 postgres). ORM 없이 계측 SQL(`this.query`, ADR-138)로
|
|
6
|
+
* 도메인 메서드를 직접 작성한다(ADR-009). 모델은 서비스만 import 한다(라우트·컨트롤러 직접 import 는
|
|
7
|
+
* `mega/no-direct-model-import` 가 차단, ADR-022).
|
|
8
|
+
*
|
|
9
|
+
* @typedef {{ id: number, name: string, email: string, created_at: string }} UserRow
|
|
10
|
+
* @typedef {{ id: number, name: string, email: string, password_hash: string | null }} UserAuthRow
|
|
11
|
+
*/
|
|
12
|
+
export class User extends MegaModel {
|
|
13
|
+
static adapter = 'primary'
|
|
14
|
+
static table = 'users'
|
|
15
|
+
|
|
16
|
+
/** @returns {Promise<UserRow[]>} */
|
|
17
|
+
static async list() {
|
|
18
|
+
const { rows } = await this.query('SELECT id, name, email, created_at FROM users ORDER BY id ASC')
|
|
19
|
+
return rows
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 전체 사용자 수 — /demo/redis 캐시 데모가 캐싱하는 SQL 카운트 쿼리. @returns {Promise<number>} */
|
|
23
|
+
static async count() {
|
|
24
|
+
const { rows } = await this.query('SELECT count(*)::int AS n FROM users')
|
|
25
|
+
return rows[0].n
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @param {number} id @returns {Promise<UserRow | null>} */
|
|
29
|
+
static async findById(id) {
|
|
30
|
+
const { rows } = await this.query('SELECT id, name, email, created_at FROM users WHERE id = $1', [id])
|
|
31
|
+
return rows[0] ?? null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @param {{ name: string, email: string }} input @returns {Promise<UserRow>} */
|
|
35
|
+
static async create({ name, email }) {
|
|
36
|
+
const { rows } = await this.query(
|
|
37
|
+
'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email, created_at',
|
|
38
|
+
[name, email],
|
|
39
|
+
)
|
|
40
|
+
return rows[0]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @param {number} id @param {{ name?: string, email?: string }} patch @returns {Promise<UserRow | null>} */
|
|
44
|
+
static async update(id, { name, email }) {
|
|
45
|
+
const { rows } = await this.query(
|
|
46
|
+
'UPDATE users SET name = COALESCE($2, name), email = COALESCE($3, email) WHERE id = $1 RETURNING id, name, email, created_at',
|
|
47
|
+
[id, name ?? null, email ?? null],
|
|
48
|
+
)
|
|
49
|
+
return rows[0] ?? null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @param {number} id @returns {Promise<boolean>} 삭제 여부. */
|
|
53
|
+
static async remove(id) {
|
|
54
|
+
const { rowCount } = await this.query('DELETE FROM users WHERE id = $1', [id])
|
|
55
|
+
return rowCount > 0
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 비밀번호 해시를 포함해 새 사용자를 만든다(/register). 반환에는 해시를 포함하지 않는다(노출 최소화).
|
|
60
|
+
* @param {{ name: string, email: string, passwordHash: string }} input
|
|
61
|
+
* @returns {Promise<UserRow>}
|
|
62
|
+
*/
|
|
63
|
+
static async register({ name, email, passwordHash }) {
|
|
64
|
+
const { rows } = await this.query(
|
|
65
|
+
'INSERT INTO users (name, email, password_hash) VALUES ($1, $2, $3) RETURNING id, name, email, created_at',
|
|
66
|
+
[name, email, passwordHash],
|
|
67
|
+
)
|
|
68
|
+
return rows[0]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 로그인 검증용 — 이메일로 사용자를 찾아 password_hash 까지 돌려준다. 해시가 필요한 인증 경로에서만 쓴다
|
|
73
|
+
* (일반 조회 list/findById 는 해시를 SELECT 하지 않아 노출되지 않는다).
|
|
74
|
+
* @param {string} email
|
|
75
|
+
* @returns {Promise<UserAuthRow | null>}
|
|
76
|
+
*/
|
|
77
|
+
static async findByEmailWithHash(email) {
|
|
78
|
+
const { rows } = await this.query('SELECT id, name, email, password_hash FROM users WHERE email = $1', [email])
|
|
79
|
+
return rows[0] ?? null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 로그인 성공 시각 갱신. @param {number} id @returns {Promise<void>} */
|
|
83
|
+
static async touchLastLogin(id) {
|
|
84
|
+
await this.query('UPDATE users SET last_login_at = now() WHERE id = $1', [id])
|
|
85
|
+
}
|
|
86
|
+
}
|