create-fuzionx 0.1.31 → 0.1.33

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 (124) hide show
  1. package/index.js +90 -79
  2. package/package.json +1 -1
  3. package/templates/common/.env.example.tpl +16 -0
  4. package/templates/{.gitignore.tpl → common/.gitignore.tpl} +2 -0
  5. package/templates/{fuzionx.yaml.tpl → common/fuzionx.yaml.tpl} +18 -8
  6. package/templates/{package.json.tpl → common/package.json.tpl} +2 -2
  7. package/templates/spa/controllers/AuthController.js +114 -0
  8. package/templates/spa/controllers/HomeController.js +68 -0
  9. package/templates/spa/controllers/PostController.js +191 -0
  10. package/templates/spa/controllers/UserController.js +43 -0
  11. package/templates/spa/meta.json +24 -0
  12. package/templates/spa/routes/api.js +31 -0
  13. package/templates/spa/routes/web.js +19 -0
  14. package/templates/spa/services/AuthService.js +48 -0
  15. package/templates/spa/services/PostService.js +372 -0
  16. package/templates/spa/services/UserService.js +48 -0
  17. package/templates/spa/views/default/errors/404.html +11 -0
  18. package/templates/spa/views/default/errors/500.html +11 -0
  19. package/templates/spa/views/default/layouts/main.html +34 -0
  20. package/templates/spa/views/default/pages/home.html +22 -0
  21. package/templates/spa/views/default/spa/index.html +13 -0
  22. package/templates/spa/views/default/spa/package.json +20 -0
  23. package/templates/spa/views/default/spa/src/App.vue +41 -0
  24. package/templates/spa/views/default/spa/src/assets/landing.css +220 -0
  25. package/templates/spa/views/default/spa/src/assets/style.css +1156 -0
  26. package/templates/spa/views/default/spa/src/components/AlertDialog.vue +179 -0
  27. package/templates/spa/views/default/spa/src/components/CodeBlock.vue +33 -0
  28. package/templates/spa/views/default/spa/src/components/EditorToolbar.vue +54 -0
  29. package/templates/spa/views/default/spa/src/components/FileUpload.vue +161 -0
  30. package/templates/spa/views/default/spa/src/components/FlashMessage.vue +39 -0
  31. package/templates/spa/views/default/spa/src/components/LanguageSwitcher.vue +108 -0
  32. package/templates/spa/views/default/spa/src/components/Lightbox.vue +62 -0
  33. package/templates/spa/views/default/spa/src/components/Navbar.vue +68 -0
  34. package/templates/spa/views/default/spa/src/components/Pagination.vue +166 -0
  35. package/templates/spa/views/default/spa/src/components/ToastContainer.vue +135 -0
  36. package/templates/spa/views/default/spa/src/composables/useApi.js +129 -0
  37. package/templates/spa/views/default/spa/src/composables/useClipboard.js +44 -0
  38. package/templates/spa/views/default/spa/src/composables/useDate.js +73 -0
  39. package/templates/spa/views/default/spa/src/composables/useDebounce.js +59 -0
  40. package/templates/spa/views/default/spa/src/composables/useFlash.js +46 -0
  41. package/templates/spa/views/default/spa/src/composables/useHeartbeat.js +45 -0
  42. package/templates/spa/views/default/spa/src/composables/useLocalStorage.js +43 -0
  43. package/templates/spa/views/default/spa/src/composables/useLocale.js +79 -0
  44. package/templates/spa/views/default/spa/src/composables/useWebSocket.js +93 -0
  45. package/templates/spa/views/default/spa/src/main.js +108 -0
  46. package/templates/spa/views/default/spa/src/plugins/alert.js +96 -0
  47. package/templates/spa/views/default/spa/src/plugins/toast.js +79 -0
  48. package/templates/spa/views/default/spa/src/router/index.js +29 -0
  49. package/templates/spa/views/default/spa/src/stores/auth.js +58 -0
  50. package/templates/spa/views/default/spa/src/views/BoardDetail.vue +169 -0
  51. package/templates/spa/views/default/spa/src/views/BoardForm.vue +192 -0
  52. package/templates/spa/views/default/spa/src/views/BoardList.vue +129 -0
  53. package/templates/spa/views/default/spa/src/views/ChatView.vue +327 -0
  54. package/templates/spa/views/default/spa/src/views/FeaturesView.vue +242 -0
  55. package/templates/spa/views/default/spa/src/views/HomeView.vue +215 -0
  56. package/templates/spa/views/default/spa/src/views/Login.vue +82 -0
  57. package/templates/spa/views/default/spa/src/views/Profile.vue +85 -0
  58. package/templates/spa/views/default/spa/src/views/Register.vue +84 -0
  59. package/templates/spa/views/default/spa/vite.config.js +28 -0
  60. package/templates/spa/views/default/spa/yarn.lock +633 -0
  61. package/templates/spa/ws/ChatHandler.js +7 -0
  62. package/templates/ssr/controllers/AuthController.js +119 -0
  63. package/templates/ssr/controllers/ChatController.js +15 -0
  64. package/templates/ssr/controllers/FeaturesController.js +15 -0
  65. package/templates/ssr/controllers/HomeController.js +21 -0
  66. package/templates/ssr/controllers/PostController.js +214 -0
  67. package/templates/ssr/controllers/UserController.js +48 -0
  68. package/templates/ssr/meta.json +11 -0
  69. package/templates/ssr/public/css/fx-ui.css +43 -0
  70. package/templates/ssr/public/css/landing.css +220 -0
  71. package/templates/ssr/public/css/style.css +1011 -0
  72. package/templates/ssr/public/js/fx-ui.js +124 -0
  73. package/templates/ssr/routes/web.js +46 -0
  74. package/templates/ssr/services/AuthService.js +48 -0
  75. package/templates/ssr/services/PostService.js +372 -0
  76. package/templates/ssr/services/UserService.js +48 -0
  77. package/templates/ssr/views/default/errors/404.html +11 -0
  78. package/templates/ssr/views/default/errors/500.html +48 -0
  79. package/templates/ssr/views/default/layouts/main.html +93 -0
  80. package/templates/ssr/views/default/pages/board/form.html +240 -0
  81. package/templates/ssr/views/default/pages/board/index.html +73 -0
  82. package/templates/ssr/views/default/pages/board/show.html +148 -0
  83. package/templates/ssr/views/default/pages/chat.html +288 -0
  84. package/templates/ssr/views/default/pages/features.html +373 -0
  85. package/templates/ssr/views/default/pages/home.html +258 -0
  86. package/templates/ssr/views/default/pages/login.html +27 -0
  87. package/templates/ssr/views/default/pages/profile.html +36 -0
  88. package/templates/ssr/views/default/pages/register.html +35 -0
  89. package/templates/ssr/views/default/partials/pagination.html +75 -0
  90. package/templates/ssr/ws/ChatHandler.js +138 -0
  91. package/templates/.env.example.tpl +0 -14
  92. package/templates/fuzionx/controllers/HomeController.js +0 -13
  93. package/templates/fuzionx/routes/api.js.tpl +0 -7
  94. package/templates/fuzionx/routes/web.js.tpl +0 -5
  95. package/templates/fuzionx/views/default/layouts/main.html +0 -22
  96. package/templates/fuzionx/views/default/pages/home.html +0 -188
  97. package/templates/tester/views/default/errors/404.html +0 -15
  98. package/templates/tester/views/default/errors/500.html +0 -14
  99. /package/templates/{app.js.tpl → common/app.js.tpl} +0 -0
  100. /package/templates/{database → common/database}/models/User.js +0 -0
  101. /package/templates/{locales → common/locales}/en.json +0 -0
  102. /package/templates/{locales → common/locales}/ko.json +0 -0
  103. /package/templates/{shared → common/shared}/events/userEvents.js +0 -0
  104. /package/templates/{shared → common/shared}/jobs/CleanupJob.js +0 -0
  105. /package/templates/{shared → common/shared}/jobs/EmailTask.js +0 -0
  106. /package/templates/{shared → common/shared}/jobs/VideoPreviewTask.js +0 -0
  107. /package/templates/{shared → common/shared}/workers/heavy.js +0 -0
  108. /package/templates/{tester → common/tester}/controllers/FileController.js +0 -0
  109. /package/templates/{tester → common/tester}/controllers/HomeController.js +0 -0
  110. /package/templates/{tester → common/tester}/controllers/UserController.js +0 -0
  111. /package/templates/{tester → common/tester}/middleware/RequestLogger.js +0 -0
  112. /package/templates/{tester → common/tester}/routes/api.js +0 -0
  113. /package/templates/{tester → common/tester}/routes/web.js +0 -0
  114. /package/templates/{tester → common/tester}/services/UserService.js +0 -0
  115. /package/templates/{fuzionx → common/tester}/views/default/errors/404.html +0 -0
  116. /package/templates/{fuzionx → common/tester}/views/default/errors/500.html +0 -0
  117. /package/templates/{tester → common/tester}/views/default/layouts/main.html +0 -0
  118. /package/templates/{tester → common/tester}/views/default/pages/home.html +0 -0
  119. /package/templates/{tester → common/tester}/views/default/pages/i18n.html +0 -0
  120. /package/templates/{tester → common/tester}/views/default/pages/upload.html +0 -0
  121. /package/templates/{tester → common/tester}/views/default/pages/websocket.html +0 -0
  122. /package/templates/{tester → common/tester}/views/default/partials/footer.html +0 -0
  123. /package/templates/{tester → common/tester}/views/default/partials/header.html +0 -0
  124. /package/templates/{tester → common/tester}/ws/ChatHandler.js +0 -0
package/index.js CHANGED
@@ -25,51 +25,37 @@ async function loadTemplate(relPath, vars) {
25
25
  return render(raw, vars);
26
26
  }
27
27
 
28
- /** 디렉토리 재귀 복사 */
29
- async function copyDirRecursive(src, dst) {
28
+ /** 디렉토리 재귀 복사 — .tpl 파일은 변수 치환 */
29
+ async function copyDirRecursive(src, dst, vars = {}) {
30
30
  await fs.mkdir(dst, { recursive: true });
31
31
  const entries = await fs.readdir(src, { withFileTypes: true });
32
32
  for (const entry of entries) {
33
33
  const srcPath = path.join(src, entry.name);
34
34
  const dstPath = path.join(dst, entry.name);
35
35
  if (entry.isDirectory()) {
36
- await copyDirRecursive(srcPath, dstPath);
36
+ await copyDirRecursive(srcPath, dstPath, vars);
37
+ } else if (entry.name === 'meta.json') {
38
+ // meta.json은 스캐폴딩에 포함하지 않음
39
+ continue;
40
+ } else if (entry.name.endsWith('.tpl')) {
41
+ // .tpl 파일은 변수 치환 후 확장자 제거
42
+ const content = await fs.readFile(srcPath, 'utf-8');
43
+ const rendered = render(content, vars);
44
+ const destName = entry.name.replace(/\.tpl$/, '');
45
+ await fs.writeFile(path.join(dst, destName), rendered);
37
46
  } else {
38
47
  await fs.copyFile(srcPath, dstPath);
39
48
  }
40
49
  }
41
50
  }
42
51
 
43
- // ── 스캐폴딩 파일 목록 (fuzionx + tester 2-app 구조) ──
52
+ // ── 디렉토리 (.gitkeep 포함) ──
44
53
 
45
- const APP_FILES = [
46
- { tpl: 'package.json.tpl', dest: 'package.json' },
47
- { tpl: 'fuzionx.yaml.tpl', dest: 'fuzionx.yaml' },
48
- { tpl: 'app.js.tpl', dest: 'app.js' },
49
- { tpl: '.env.example.tpl', dest: '.env.example' },
50
- { tpl: '.env.example.tpl', dest: '.env' },
51
- { tpl: '.gitignore.tpl', dest: '.gitignore' },
52
- ];
53
-
54
- /** fuzionx 앱 — .tpl 파일 (치환 필요) */
55
- const FUZIONX_TPL_FILES = [
56
- { tpl: 'fuzionx/routes/web.js.tpl', dest: 'app/fuzionx/routes/web.js' },
57
- { tpl: 'fuzionx/routes/api.js.tpl', dest: 'app/fuzionx/routes/api.js' },
58
- ];
59
-
60
- /** 빈 디렉토리 (.gitkeep 포함) */
61
- const APP_DIRS = [
62
- // fuzionx 앱
63
- 'app/fuzionx/services',
64
- 'app/fuzionx/middleware',
65
- 'app/fuzionx/ws',
66
- // 인프라
54
+ const EMPTY_DIRS = [
67
55
  'database/migrations',
68
56
  'database/seeds',
69
57
  'storage/logs',
70
58
  'storage/uploads',
71
- 'public/css',
72
- 'public/js',
73
59
  'tests',
74
60
  ];
75
61
 
@@ -77,14 +63,25 @@ const APP_DIRS = [
77
63
 
78
64
  async function createApp(name, targetDir, type = 'spa') {
79
65
  const dir = targetDir || path.resolve(name);
80
- const appName = type; // 앱 이름은 타입에 따라 고정
66
+ const appName = type; // 앱 디렉토리명은 타입과 동일
81
67
  const vars = {
82
68
  name,
69
+ appName: type,
83
70
  dbName: name.replace(/-/g, '_'),
84
71
  };
85
72
 
86
- // 디렉토리 생성 (.gitkeep 포함)
87
- for (const d of APP_DIRS) {
73
+ const commonDir = path.join(TPL_DIR, 'common');
74
+ const typeDir = path.join(TPL_DIR, type);
75
+
76
+ // meta.json 로드
77
+ let meta = {};
78
+ try {
79
+ const metaRaw = await fs.readFile(path.join(typeDir, 'meta.json'), 'utf-8');
80
+ meta = JSON.parse(metaRaw);
81
+ } catch { /* meta.json 없어도 진행 */ }
82
+
83
+ // 1. 빈 디렉토리 생성
84
+ for (const d of EMPTY_DIRS) {
88
85
  const fullDir = path.join(dir, d);
89
86
  await fs.mkdir(fullDir, { recursive: true });
90
87
  const files = await fs.readdir(fullDir).catch(() => []);
@@ -93,57 +90,69 @@ async function createApp(name, targetDir, type = 'spa') {
93
90
  }
94
91
  }
95
92
 
96
- // 루트 템플릿 파일 생성 (.tpl 치환)
97
- for (const { tpl, dest } of APP_FILES) {
98
- const content = await loadTemplate(tpl, vars);
99
- const fullPath = path.join(dir, dest);
100
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
101
- await fs.writeFile(fullPath, content);
93
+ // 전용 디렉토리
94
+ for (const d of ['services', 'middleware', 'ws']) {
95
+ const fullDir = path.join(dir, `app/${appName}`, d);
96
+ await fs.mkdir(fullDir, { recursive: true });
97
+ const files = await fs.readdir(fullDir).catch(() => []);
98
+ if (files.length === 0) {
99
+ await fs.writeFile(path.join(fullDir, '.gitkeep'), '');
100
+ }
101
+ }
102
+
103
+ // 2. common/ 루트 .tpl 파일 → 프로젝트 루트
104
+ const commonEntries = await fs.readdir(commonDir, { withFileTypes: true });
105
+ for (const entry of commonEntries) {
106
+ if (entry.isFile() && entry.name.endsWith('.tpl')) {
107
+ const content = await fs.readFile(path.join(commonDir, entry.name), 'utf-8');
108
+ const rendered = render(content, vars);
109
+ const destName = entry.name.replace(/\.tpl$/, '');
110
+ await fs.writeFile(path.join(dir, destName), rendered);
111
+
112
+ // .env.example → .env 도 함께 생성
113
+ if (destName === '.env.example') {
114
+ await fs.writeFile(path.join(dir, '.env'), rendered);
115
+ }
116
+ }
117
+ }
118
+
119
+ // 3. common/ 하위 디렉토리 복사 (database, locales, shared)
120
+ for (const entry of commonEntries) {
121
+ if (!entry.isDirectory()) continue;
122
+ if (entry.name === 'tester') continue; // tester는 별도 처리
123
+ await copyDirRecursive(
124
+ path.join(commonDir, entry.name),
125
+ path.join(dir, entry.name),
126
+ vars,
127
+ );
102
128
  }
103
129
 
104
- // fuzionx .tpl 파일 (치환 필요)
105
- for (const { tpl, dest } of FUZIONX_TPL_FILES) {
106
- const content = await loadTemplate(tpl, vars);
107
- const actualDest = dest.replace('app/fuzionx/', `app/${appName}/`);
108
- const fullPath = path.join(dir, actualDest);
109
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
110
- await fs.writeFile(fullPath, content);
130
+ // 4. tester 복사 (common/tester/ app/tester/)
131
+ const testerSrc = path.join(commonDir, 'tester');
132
+ try {
133
+ await fs.access(testerSrc);
134
+ await copyDirRecursive(testerSrc, path.join(dir, 'app/tester'), vars);
135
+ } catch { /* tester 없으면 스킵 */ }
136
+
137
+ // 5. 타입별 템플릿 복사 → app/{type}/
138
+ // controllers, services, middleware, routes, ws, views
139
+ const typeEntries = await fs.readdir(typeDir, { withFileTypes: true });
140
+ for (const entry of typeEntries) {
141
+ if (!entry.isDirectory()) continue;
142
+ if (entry.name === 'public') continue; // public은 프로젝트 루트로 복사
143
+ await copyDirRecursive(
144
+ path.join(typeDir, entry.name),
145
+ path.join(dir, `app/${appName}`, entry.name),
146
+ vars,
147
+ );
111
148
  }
112
149
 
113
- // fuzionx 직접 복사 (controllers, views)
114
- const fuzionxSrc = path.join(TPL_DIR, 'fuzionx');
115
- await copyDirRecursive(
116
- path.join(fuzionxSrc, 'controllers'),
117
- path.join(dir, `app/${appName}/controllers`),
118
- );
119
- await copyDirRecursive(
120
- path.join(fuzionxSrc, 'views'),
121
- path.join(dir, `app/${appName}/views`),
122
- );
123
-
124
- // tester 앱 — 전체 복사
125
- await copyDirRecursive(
126
- path.join(TPL_DIR, 'tester'),
127
- path.join(dir, 'app/tester'),
128
- );
129
-
130
- // shared — 전체 복사
131
- await copyDirRecursive(
132
- path.join(TPL_DIR, 'shared'),
133
- path.join(dir, 'shared'),
134
- );
135
-
136
- // database/models — 복사
137
- await copyDirRecursive(
138
- path.join(TPL_DIR, 'database'),
139
- path.join(dir, 'database'),
140
- );
141
-
142
- // locales — 복사
143
- await copyDirRecursive(
144
- path.join(TPL_DIR, 'locales'),
145
- path.join(dir, 'locales'),
146
- );
150
+ // 6. public/ 디렉토리 복사 (SSR의 경우 CSS/JS assets)
151
+ const publicSrc = path.join(typeDir, 'public');
152
+ try {
153
+ await fs.access(publicSrc);
154
+ await copyDirRecursive(publicSrc, path.join(dir, 'public'), vars);
155
+ } catch { /* public 없으면 스킵 */ }
147
156
 
148
157
  return dir;
149
158
  }
@@ -175,6 +184,7 @@ if (!validTypes.includes(type)) {
175
184
 
176
185
  try {
177
186
  const dir = await createApp(name, null, type);
187
+ const nextCmd = type === 'spa' ? 'npx fx dev:spa' : 'npx fx dev';
178
188
  console.log(`
179
189
  ✅ Created ${name} at ${dir} (type: ${type})
180
190
 
@@ -182,7 +192,8 @@ try {
182
192
 
183
193
  cd ${name}
184
194
  npm install
185
- npx fx dev${type === 'spa' ? ':spa' : ''}
195
+ ${type === 'spa' ? `cd app/spa/views/default/spa && npm install && cd ../../../../..
196
+ ` : ''}${nextCmd}
186
197
  `);
187
198
  } catch (err) {
188
199
  console.error(`❌ Failed to create app: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fuzionx",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "description": "Create a new FuzionX application — npx create-fuzionx my-app",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,16 @@
1
+ # .env — FuzionX App 환경 변수
2
+
3
+ # ASP 암호화 (Bridge + WASM 공유)
4
+ ASP_SECRET=change-me-in-production
5
+
6
+ # Auth (JWT/세션)
7
+ JWT_SECRET=change-me-in-production
8
+
9
+ # Client Secret (설정값 암호화 → SPA 주입)
10
+ CLIENT_SECRET=change-me-in-production
11
+
12
+ # Database
13
+ DB_NAME={{dbName}}
14
+
15
+ # Redis (세션/큐 — 미사용 시 빈 값)
16
+ REDIS_URL=
@@ -2,4 +2,6 @@
2
2
  node_modules/
3
3
  storage/logs/
4
4
  storage/uploads/
5
+ storage/database.sqlite
5
6
  fuzionx.pid
7
+ public/dist/
@@ -38,25 +38,25 @@ bridge:
38
38
 
39
39
  # ── 세션 ──
40
40
  session:
41
- enabled: false
42
- store: memory # memory | redis
41
+ enabled: true
42
+ store: file # file | memory | redis
43
43
  ttl: 3600 # 세션 TTL (초)
44
44
  cookie_name: fuzionx.sid
45
45
  # redis_url: "redis://localhost:6379"
46
46
 
47
47
  # ── 국제화 (i18n) ──
48
48
  i18n:
49
- enabled: false
49
+ enabled: true
50
50
  default_locale: ko
51
51
  locale_dir: ./locales
52
- auto_complete: false
52
+ auto_complete: true
53
53
  locales:
54
54
  - ko
55
55
  - en
56
56
 
57
57
  # ── WebSocket ──
58
58
  websocket:
59
- enabled: false
59
+ enabled: true
60
60
  path: /ws
61
61
  check_interval: 60 # 헬스체크 간격 (초)
62
62
  timeout: 60 # 타임아웃 (초)
@@ -79,6 +79,9 @@ bridge:
79
79
  - image/gif
80
80
  - video/mp4
81
81
  - video/webm
82
+ - video/quicktime
83
+ - video/matroska
84
+ - video/x-matroska
82
85
  - application/pdf
83
86
  # watermark: ./assets/watermark.png
84
87
  # watermark_opacity: 50
@@ -87,6 +90,10 @@ bridge:
87
90
  static:
88
91
  - url: /public
89
92
  path: ./public
93
+ - url: /wasm
94
+ path: ./node_modules/@fuzionx/client
95
+ - url: /storage
96
+ path: ./storage
90
97
 
91
98
  # ── 로깅 ──
92
99
  logging:
@@ -125,7 +132,7 @@ bridge:
125
132
 
126
133
  # ── ASP 암호화 ──
127
134
  asp:
128
- enabled: false
135
+ enabled: true
129
136
  master_secret: "${ASP_SECRET:change-me-in-production}"
130
137
  header_signal: Ruxy-Enc-Mode
131
138
 
@@ -160,6 +167,9 @@ app:
160
167
  # refreshTtl: '7d'
161
168
  # bcrypt_rounds: 12
162
169
 
170
+ # Client Secret (SPA 설정값 암호화)
171
+ client_secret: '${CLIENT_SECRET:change-me-in-production}'
172
+
163
173
  # 국제화 (프레임워크 편의 설정)
164
174
  i18n:
165
175
  default_locale: 'ko'
@@ -196,7 +206,7 @@ app:
196
206
  # prefix: "myapp:queue"
197
207
 
198
208
  # ── Multi-App (도메인 → 앱 라우팅) ────────────────
199
- # tester를 실행하려면 fuzionx를 주석 처리하고 tester 주석을 해제하세요.
209
+ # tester를 실행하려면 {{appName}}를 주석 처리하고 tester 주석을 해제하세요.
200
210
  apps:
201
- "127.0.0.1:49080": fuzionx
211
+ "127.0.0.1:49080": {{appName}}
202
212
  # "127.0.0.1:49080": tester
@@ -9,8 +9,8 @@
9
9
  "test": "vitest run"
10
10
  },
11
11
  "dependencies": {
12
- "@fuzionx/framework": "^0.1.31",
13
- "@fuzionx/client": "^0.1.30",
12
+ "@fuzionx/framework": "^0.1.33",
13
+ "@fuzionx/client": "^0.1.33",
14
14
  "joi": "^18.1.1"
15
15
  },
16
16
  "devDependencies": {
@@ -0,0 +1,114 @@
1
+ import { Controller } from '@fuzionx/framework';
2
+
3
+ /**
4
+ * SPA AuthController — JSON API 전용 인증
5
+ *
6
+ * 모든 페이지는 Vue에서 렌더링. 서버는 API만 제공.
7
+ * 세션 기반 인증. JSON 요청/응답.
8
+ */
9
+ export default class AuthController extends Controller {
10
+ /** @type {import('@fuzionx/framework').RouteHandler} 로그인 API */
11
+ static login;
12
+ /** @type {import('@fuzionx/framework').RouteHandler} 회원가입 API */
13
+ static register;
14
+ /** @type {import('@fuzionx/framework').RouteHandler} 로그아웃 API */
15
+ static logout;
16
+ /** @type {import('@fuzionx/framework').RouteHandler} 인증 상태 확인 API */
17
+ static check;
18
+ /** @type {import('@fuzionx/framework').RouteHandler} 하트비트 (세션 연장) */
19
+ static heartbeat;
20
+
21
+ /**
22
+ * POST /api/auth/login — 로그인
23
+ *
24
+ * email/password 검증 후 세션에 userId 저장.
25
+ * 성공 → { user }, 실패 → 401 { error }.
26
+ *
27
+ * @param {import('@fuzionx/framework').Context} ctx - 요청 컨텍스트
28
+ * @returns {void}
29
+ */
30
+ async login(ctx) {
31
+ const { email, password } = ctx.body;
32
+ const user = await this.service('AuthService').login({ email, password });
33
+
34
+ if (!user) {
35
+ return ctx.status(401).json({ error: ctx.t('auth.login_failed') });
36
+ }
37
+
38
+ ctx.session.set('userId', user.id);
39
+ ctx.json({ user: { id: user.id, name: user.name, email: user.email, role: user.role } });
40
+ }
41
+
42
+ /**
43
+ * POST /api/auth/register — 회원가입
44
+ *
45
+ * name/email/password/password_confirm 검증.
46
+ * 성공 → 201 { user }, 실패 → 400 { error }.
47
+ *
48
+ * @param {import('@fuzionx/framework').Context} ctx - 요청 컨텍스트
49
+ * @returns {void}
50
+ */
51
+ async register(ctx) {
52
+ const { name, email, password, password_confirm } = ctx.body;
53
+
54
+ if (password !== password_confirm) {
55
+ return ctx.status(400).json({ error: ctx.t('auth.password_mismatch') });
56
+ }
57
+
58
+ try {
59
+ const user = await this.service('AuthService').register({ name, email, password });
60
+ ctx.session.set('userId', user.id);
61
+ ctx.status(201).json({ user: { id: user.id, name: user.name, email: user.email } });
62
+ } catch (e) {
63
+ ctx.status(e.status || 400).json({ error: ctx.t(e.message) || e.message });
64
+ }
65
+ }
66
+
67
+ /**
68
+ * POST /api/auth/logout — 로그아웃
69
+ *
70
+ * 세션 파기 후 { success: true } 반환.
71
+ *
72
+ * @param {import('@fuzionx/framework').Context} ctx - 요청 컨텍스트
73
+ * @returns {void}
74
+ */
75
+ async logout(ctx) {
76
+ ctx.session.destroy();
77
+ ctx.json({ success: true });
78
+ }
79
+
80
+ /**
81
+ * GET /api/auth/check — 인증 상태 확인
82
+ *
83
+ * 세션에 userId 존재 → DB 조회 → { authenticated, user }.
84
+ * 미인증 시 { authenticated: false, user: null }.
85
+ *
86
+ * @param {import('@fuzionx/framework').Context} ctx - 요청 컨텍스트
87
+ * @returns {void}
88
+ */
89
+ async check(ctx) {
90
+ const userId = ctx.session.get('userId');
91
+ if (!userId) return ctx.json({ authenticated: false, user: null });
92
+
93
+ try {
94
+ const user = await this.db.User.find(userId);
95
+ if (!user) return ctx.json({ authenticated: false, user: null });
96
+ ctx.json({ authenticated: true, user: user.toJSON() });
97
+ } catch {
98
+ ctx.json({ authenticated: false, user: null });
99
+ }
100
+ }
101
+
102
+ /**
103
+ * GET /api/heartbeat — 세션 연장 (하트비트)
104
+ *
105
+ * 인증된 사용자의 세션 유지. { alive, user }.
106
+ *
107
+ * @param {import('@fuzionx/framework').Context} ctx - 요청 컨텍스트
108
+ * @returns {void}
109
+ */
110
+ async heartbeat(ctx) {
111
+ if (!ctx.user) return ctx.status(401).json({ alive: false });
112
+ ctx.json({ alive: true, user: { id: ctx.user.id } });
113
+ }
114
+ }
@@ -0,0 +1,68 @@
1
+ import { Controller } from '@fuzionx/framework';
2
+
3
+ /**
4
+ * SPA HomeController — Vue 셸 렌더링
5
+ *
6
+ * 모든 프론트엔드 라우트에 대해 Vue 앱 셸을 서빙.
7
+ * 인증 여부와 관계없이 셸을 렌더링하며,
8
+ * 유저 데이터를 ASP 암호화하여 Vue에 전달.
9
+ * Vue가 user=null이면 로그인 화면, 아니면 대시보드를 표시.
10
+ *
11
+ * bridge.cryptoEncryptCustom(key, plaintext) → WASM decrypt_custom(key, ciphertext)
12
+ */
13
+ export default class HomeController extends Controller {
14
+ /** @type {import('@fuzionx/framework').RouteHandler} SPA 셸 렌더링 */
15
+ static index;
16
+
17
+ /**
18
+ * GET / | /* — Vue SPA 셸 렌더링
19
+ *
20
+ * 세션에서 유저 조회 후 ASP 암호화된 payload와 함께 렌더링.
21
+ * payload: { user, locale, isDev, asp, app }
22
+ *
23
+ * @param {import('@fuzionx/framework').Context} ctx - 요청 컨텍스트
24
+ * @returns {void}
25
+ */
26
+ async index(ctx) {
27
+ const clientSecret = this.config.get('app.client_secret');
28
+ const bridge = this.app._bridge;
29
+
30
+ // ASP config에서 masterSecret 가져오기
31
+ const aspConfig = JSON.parse(bridge.getAspConfig());
32
+ const masterSecret = aspConfig.masterSecret || '';
33
+
34
+ // 세션에서 유저 조회 (인증 안 되어 있으면 null)
35
+ let user = null;
36
+ const userId = ctx.session?.get('userId');
37
+ if (userId && this.db?.User) {
38
+ try {
39
+ const u = await this.db.User.find(userId);
40
+ if (u) user = { id: u.id, name: u.name, email: u.email, role: u.role };
41
+ } catch {}
42
+ }
43
+
44
+ const isDev = this.config.get('app.environment') === 'development';
45
+
46
+ // SPA에 전달할 payload
47
+ const payload = {
48
+ user,
49
+ locale: ctx.locale,
50
+ locales: this.app?.i18n?.locales?.() || [],
51
+ translations: ctx.t.all(),
52
+ isDev,
53
+ asp: { headerSignal: aspConfig.headerSignal || 'Ruxy-Enc-Mode' },
54
+ app: { name: this.config.get('app.name'), theme: this.config.get('app.themes.default') },
55
+ };
56
+
57
+ // 암호화 (bridge crypto_encrypt_custom — WASM decrypt_custom과 호환)
58
+ const encryptedPayload = bridge.cryptoEncryptCustom(clientSecret, JSON.stringify(payload));
59
+ const encryptedSecret = bridge.cryptoEncryptCustom(clientSecret, masterSecret);
60
+
61
+ ctx.render('home', {
62
+ __fx__: encryptedPayload,
63
+ __fx_secret__: encryptedSecret,
64
+ client_secret: clientSecret,
65
+ isDev,
66
+ });
67
+ }
68
+ }