@gaonjs/cli 0.32.0 → 0.34.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/dist/commands/build.d.ts +11 -0
- package/dist/commands/build.js +49 -0
- package/dist/commands/check.d.ts +10 -1
- package/dist/commands/check.js +51 -6
- package/dist/commands/dev.js +2 -0
- package/dist/commands/gen.js +4 -2
- package/dist/db/projectData.d.ts +14 -0
- package/dist/db/projectData.js +51 -0
- package/dist/db.js +38 -2
- package/dist/dev/build.d.ts +34 -0
- package/dist/dev/build.js +144 -0
- package/dist/dev/frontend-build.d.ts +3 -3
- package/dist/dev/frontend-build.js +42 -37
- package/dist/dev.d.ts +11 -1
- package/dist/dev.js +29 -1
- package/dist/doctor/async-offload.d.ts +3 -1
- package/dist/doctor/async-offload.js +5 -2
- package/dist/doctor/auth-wiring.d.ts +4 -2
- package/dist/doctor/auth-wiring.js +13 -6
- package/dist/doctor/csrf-wiring.d.ts +3 -1
- package/dist/doctor/csrf-wiring.js +9 -5
- package/dist/doctor/seal-security.js +12 -6
- package/dist/doctor/source-scan.d.ts +11 -0
- package/dist/doctor/source-scan.js +70 -0
- package/dist/generate.d.ts +12 -3
- package/dist/generate.js +113 -42
- package/dist/index.d.ts +3 -0
- package/dist/index.js +25 -5
- package/dist/messages-gen.d.ts +6 -0
- package/dist/messages-gen.js +24 -0
- package/dist/scaffold/app.js +2 -1
- package/dist/templates/auth/Dashboard.vue.tpl +2 -2
- package/dist/templates/auth/Login.vue.tpl +2 -5
- package/dist/templates/auth/Signup.vue.tpl +2 -2
- package/dist/templates/auth/app.config.ts.tpl +3 -3
- package/dist/templates/auth/dashboard.controller.ts.tpl +1 -1
- package/dist/templates/auth/dashboard.secure.controller.ts.tpl +14 -0
- package/dist/templates/auth/registration.controller.ts.tpl +3 -3
- package/dist/templates/auth/session.controller.ts.tpl +5 -5
- package/dist/templates/project/.env.example.tpl +6 -0
- package/dist/templates/project/AGENTS.md.tpl +3 -1
- package/dist/templates/project/CLAUDE.md.tpl +1 -1
- package/dist/templates/project/agents/async.md.tpl +46 -5
- package/dist/templates/project/agents/data.md.tpl +47 -2
- package/dist/templates/project/agents/frontend.md.tpl +3 -1
- package/dist/templates/project/agents/i18n.md.tpl +107 -0
- package/dist/templates/project/agents/mail.md.tpl +92 -0
- package/dist/templates/project/agents/realtime.md.tpl +8 -0
- package/dist/templates/project/agents/security.md.tpl +45 -2
- package/dist/templates/project/agents/testing.md.tpl +11 -0
- package/dist/templates/project/agents/web.md.tpl +32 -0
- package/dist/templates/project/gaon.config.ts.tpl +14 -0
- package/dist/templates/project/package.json.tpl +1 -1
- package/dist/templates/project/vite.config.ts.tpl +6 -5
- package/package.json +7 -6
- package/dist/templates/auth/app.ts.tpl +0 -29
- package/dist/templates/auth/server.ts.tpl +0 -14
package/dist/generate.js
CHANGED
|
@@ -16,45 +16,90 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
16
16
|
import { dirname, join, resolve } from 'node:path';
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
18
|
import { authUiKitFiles, writeUiKitFiles } from './uikit.js';
|
|
19
|
+
/** 결정 155: 이 스캐폴드가 공개 회원가입을 포함하는가(web 기본 O · 비-web 은 --public 시만). */
|
|
20
|
+
function includesPublicRegistration(opts) {
|
|
21
|
+
const app = opts.app ?? 'web';
|
|
22
|
+
return app === 'web' || opts.public === true;
|
|
23
|
+
}
|
|
19
24
|
// ── 템플릿 로드·치환 ───────────────────────────────────────────
|
|
20
25
|
// 템플릿은 이 모듈과 같은 위치의 templates/auth/ 에 있다(빌드가 dist 로 복사).
|
|
21
26
|
const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), 'templates', 'auth');
|
|
22
|
-
/**
|
|
23
|
-
*
|
|
24
|
-
|
|
27
|
+
/**
|
|
28
|
+
* 앱의 URL 프리픽스 — web 은 루트라 '' (URL 앞에 아무것도 안 붙음), 그 외는 '/<app>'
|
|
29
|
+
* (결정 141 · 12차 W4). 폴더명=프리픽스 관례(§3.3)와 정합. 라우트 **등록** 경로는
|
|
30
|
+
* 프레임웍이 앱 스코프에서 자동으로 프리픽스를 붙이므로 이 값은 라우트 등록이 아니라
|
|
31
|
+
* **절대 URL**(form.post·router.delete·Link href·redirect·loginRedirect)에만 쓴다.
|
|
32
|
+
*/
|
|
33
|
+
function urlPrefixFor(app) {
|
|
34
|
+
return app === 'web' ? '' : `/${app}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 앱의 세션 secret 환경변수명 — web 은 'SESSION_SECRET'(기존), 그 외는
|
|
38
|
+
* '<APP>_SESSION_SECRET'(결정 141 · 앱별 세션 완전 분리 · session.ts 는 앱마다
|
|
39
|
+
* secret 이 달라야 분리가 완성된다고 명시). 앱 이름의 비영숫자는 '_' 로 정규화.
|
|
40
|
+
*/
|
|
41
|
+
function sessionSecretEnvFor(app) {
|
|
42
|
+
if (app === 'web')
|
|
43
|
+
return 'SESSION_SECRET';
|
|
44
|
+
return `${app.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_SESSION_SECRET`;
|
|
45
|
+
}
|
|
46
|
+
/** 앱별 개발용 세션 secret 폴백(32자 이상 · 앱마다 다르게). */
|
|
47
|
+
function devSessionSecretFor(app) {
|
|
48
|
+
return `dev-only-session-secret-${app}-change-me-now!!`;
|
|
49
|
+
}
|
|
50
|
+
/** 결정 155: Login 페이지의 회원가입 링크 — 공개 가입 앱에만 넣는다(시큐어 앱은 뺀다).
|
|
51
|
+
* 템플릿의 `{{SIGNUP_LINK}}` 자리(8칸 들여쓰기)에 in-place 치환되므로 첫 줄은 들여쓰기 없이. */
|
|
52
|
+
function signupLinkMarkup(app) {
|
|
53
|
+
return (`<p class="mt-4 text-center text-sm text-muted-foreground">\n` +
|
|
54
|
+
` 계정이 없으신가요?\n` +
|
|
55
|
+
` <Link href="${urlPrefixFor(app)}/registration/new" class="font-medium text-primary underline-offset-4 hover:underline">회원가입</Link>\n` +
|
|
56
|
+
` </p>`);
|
|
57
|
+
}
|
|
58
|
+
/** 템플릿 파일을 읽어 토큰을 치환한다. {{APP_NAME}}·{{URL_PREFIX}}·{{SESSION_SECRET_ENV}}·
|
|
59
|
+
* {{SIGNUP_LINK}} 는 Vue 의 {{ }} 보간과 겹치지 않는 고정 리터럴이라 단순 replaceAll 로
|
|
60
|
+
* 안전하다(결정 141·155). SIGNUP_LINK 는 공개 가입 앱에만 링크를, 시큐어 앱엔 빈 값. */
|
|
61
|
+
function renderTemplate(name, app, includePublic) {
|
|
25
62
|
const raw = readFileSync(join(TEMPLATE_DIR, name), 'utf8');
|
|
26
|
-
return raw
|
|
63
|
+
return raw
|
|
64
|
+
.replaceAll('{{APP_NAME}}', app)
|
|
65
|
+
.replaceAll('{{URL_PREFIX}}', urlPrefixFor(app))
|
|
66
|
+
.replaceAll('{{SESSION_SECRET_ENV}}', sessionSecretEnvFor(app))
|
|
67
|
+
.replaceAll('{{SIGNUP_LINK}}', includePublic ? signupLinkMarkup(app) : '');
|
|
27
68
|
}
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
{ tpl: 'user.schema.ts.tpl', out: () => 'domain/schema/users.ts' },
|
|
31
|
-
{ tpl: 'user.model.ts.tpl', out: () => 'domain/models/User.ts' },
|
|
32
|
-
{ tpl: 'auth.wiring.ts.tpl', out: (a) => `apps/${a}/auth.ts` },
|
|
33
|
-
{ tpl: 'session.controller.ts.tpl', out: (a) => `apps/${a}/controllers/session.ts` },
|
|
34
|
-
{ tpl: 'registration.controller.ts.tpl', out: (a) => `apps/${a}/controllers/registration.ts` },
|
|
35
|
-
{ tpl: 'dashboard.controller.ts.tpl', out: (a) => `apps/${a}/controllers/dashboard.ts` },
|
|
36
|
-
// 결정 32·46: Vue 페이지 경로 세그먼트는 PascalCase(Route 이름) — 'Auth/'.
|
|
37
|
-
// examples/blog 정본과 page-filename doctor 규칙에 정합(소문자 'auth/' 는
|
|
38
|
-
// doctor 가 error 로 잡던 스캐폴드 표류였다 · W10 실측).
|
|
39
|
-
{ tpl: 'Login.vue.tpl', out: (a) => `apps/${a}/pages/Auth/Login.vue` },
|
|
40
|
-
{ tpl: 'Signup.vue.tpl', out: (a) => `apps/${a}/pages/Auth/Signup.vue` },
|
|
41
|
-
{ tpl: 'Dashboard.vue.tpl', out: (a) => `apps/${a}/pages/Dashboard.vue` },
|
|
42
|
-
// 결정 59: 세션·인증 배선은 app.config.ts — 표준 부팅(gaon dev/serve = wireGaon)이
|
|
43
|
-
// 소비한다. 과거의 수동 부팅 스캐폴드(app.ts·server.ts)는 두 번째 부팅 경로를
|
|
44
|
-
// 만들어 One Way 를 깨고, 표준 경로에는 auth 배선이 빠져 currentUser 가 영구
|
|
45
|
-
// null 이 되는 파손을 남겼다 — 제거했다.
|
|
46
|
-
{ tpl: 'app.config.ts.tpl', out: (a) => `apps/${a}/app.config.ts` },
|
|
47
|
-
];
|
|
48
|
-
/** 인증 스캐폴드가 생성하는 파일 목록(라우트 제외). 템플릿을 읽어 렌더링한다. */
|
|
69
|
+
/** 인증 스캐폴드가 생성하는 파일 목록(라우트 제외). 템플릿을 읽어 렌더링한다.
|
|
70
|
+
* 결정 155: 공개 가입 여부(includePublic)로 registration·Signup·dashboard 변형을 가른다. */
|
|
49
71
|
export function authScaffoldFiles(opts = {}) {
|
|
50
72
|
const app = opts.app ?? 'web';
|
|
51
|
-
|
|
73
|
+
const includePublic = includesPublicRegistration(opts);
|
|
74
|
+
const specs = [
|
|
75
|
+
{ tpl: 'user.schema.ts.tpl', out: 'domain/schema/users.ts' },
|
|
76
|
+
{ tpl: 'user.model.ts.tpl', out: 'domain/models/User.ts' },
|
|
77
|
+
{ tpl: 'auth.wiring.ts.tpl', out: `apps/${app}/auth.ts` },
|
|
78
|
+
{ tpl: 'session.controller.ts.tpl', out: `apps/${app}/controllers/session.ts` },
|
|
79
|
+
// 결정 155: 시큐어(비-web · 비-public) 앱은 역할 게이트 대시보드, 공개 앱은 requireAuth 대시보드.
|
|
80
|
+
{
|
|
81
|
+
tpl: includePublic ? 'dashboard.controller.ts.tpl' : 'dashboard.secure.controller.ts.tpl',
|
|
82
|
+
out: `apps/${app}/controllers/dashboard.ts`,
|
|
83
|
+
},
|
|
84
|
+
// 결정 32·46: Vue 페이지 경로 세그먼트는 PascalCase(Route 이름) — 'Auth/'.
|
|
85
|
+
{ tpl: 'Login.vue.tpl', out: `apps/${app}/pages/Auth/Login.vue` },
|
|
86
|
+
{ tpl: 'Dashboard.vue.tpl', out: `apps/${app}/pages/Dashboard.vue` },
|
|
87
|
+
// 결정 59: 세션·인증 배선은 app.config.ts — 표준 부팅(gaon dev/serve = wireGaon)이 소비한다.
|
|
88
|
+
{ tpl: 'app.config.ts.tpl', out: `apps/${app}/app.config.ts` },
|
|
89
|
+
];
|
|
90
|
+
// 결정 155: 공개 가입 앱만 회원가입 컨트롤러·페이지를 깐다. 시큐어 앱(admin 등)은 빼서
|
|
91
|
+
// "관리 앱에 공개 가입 + 로그인 고객 200" 위험 기본을 구조적으로 차단한다.
|
|
92
|
+
if (includePublic) {
|
|
93
|
+
specs.splice(4, 0, { tpl: 'registration.controller.ts.tpl', out: `apps/${app}/controllers/registration.ts` });
|
|
94
|
+
specs.push({ tpl: 'Signup.vue.tpl', out: `apps/${app}/pages/Auth/Signup.vue` });
|
|
95
|
+
}
|
|
96
|
+
return specs.map(({ tpl, out }) => ({ path: out, contents: renderTemplate(tpl, app, includePublic) }));
|
|
52
97
|
}
|
|
53
98
|
/**
|
|
54
99
|
* 기존 routes.ts 에 세션·회원가입 리소스를 끼워 넣는다. 이미 있으면 null.
|
|
55
100
|
* `routes((r) => {` 콜백 여는 지점 뒤에 두 줄을 삽입한다.
|
|
56
101
|
*/
|
|
57
|
-
export function patchRoutes(existing) {
|
|
102
|
+
export function patchRoutes(existing, includePublic = true) {
|
|
58
103
|
if (existing.includes("resource('session')"))
|
|
59
104
|
return null;
|
|
60
105
|
const m = existing.match(/routes\(\s*\(\s*\w+\s*\)\s*=>\s*\{/);
|
|
@@ -64,9 +109,10 @@ export function patchRoutes(existing) {
|
|
|
64
109
|
// dashboard 라우트도 함께 배선한다 — 없으면 스캐폴드 Dashboard.vue 의
|
|
65
110
|
// pageProps<'…:dashboard#show'> 가 라우트 맵에서 해상되지 않아 vue-tsc 가
|
|
66
111
|
// 깨진다(결정 60 클린룸 실측 — gaon new 프로젝트에 g auth 를 얹는 경로).
|
|
112
|
+
// 결정 155: 회원가입 리소스는 공개 가입 앱에만(시큐어 앱은 빼서 공개 가입 라우트 자체를 안 깐다).
|
|
67
113
|
const inject = "\n r.get('/dashboard', 'dashboard#show') // 보호 페이지 (gaon g auth)" +
|
|
68
114
|
"\n r.resource('session') // 로그인/로그아웃 (gaon g auth)" +
|
|
69
|
-
"\n r.resource('registration') // 회원가입 (gaon g auth)";
|
|
115
|
+
(includePublic ? "\n r.resource('registration') // 회원가입 (gaon g auth)" : '');
|
|
70
116
|
return existing.slice(0, insertAt) + inject + existing.slice(insertAt);
|
|
71
117
|
}
|
|
72
118
|
/**
|
|
@@ -76,18 +122,30 @@ export function patchRoutes(existing) {
|
|
|
76
122
|
* · defineAppConfig 객체 리터럴을 못 찾으면 null (안전 · 상위에서 경고 폴백).
|
|
77
123
|
* 성공 시 loadUser import 도 함께 보장한다.
|
|
78
124
|
*/
|
|
79
|
-
export function patchAppConfigAddAuth(existing) {
|
|
125
|
+
export function patchAppConfigAddAuth(existing, app = 'web') {
|
|
80
126
|
if (/\bauth\s*:/.test(existing))
|
|
81
127
|
return null;
|
|
82
128
|
const m = existing.match(/defineAppConfig\(\s*\{/);
|
|
83
129
|
if (!m || m.index === undefined)
|
|
84
130
|
return null;
|
|
85
131
|
const insertAt = m.index + m[0].length;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
132
|
+
const prefix = urlPrefixFor(app);
|
|
133
|
+
// 결정 141(12차 W4): 대상 앱에 세션이 없으면(예: gaon g app 이 만든 빈 config)
|
|
134
|
+
// 세션도 함께 배선한다 — 세션이 없으면 로그인이 저장될 곳이 없어 왕복이 깨지고,
|
|
135
|
+
// doctor 가 세션/CSRF 누락을 error 로 잡는다(결정 140). secret 은 앱별로 분리한다.
|
|
136
|
+
const hasSession = /\bsession\s*:/.test(existing);
|
|
137
|
+
const lines = [];
|
|
138
|
+
if (!hasSession) {
|
|
139
|
+
const secretEnv = sessionSecretEnvFor(app);
|
|
140
|
+
lines.push(` // 세션 secret 은 32자 이상 — .env 의 ${secretEnv} 로 주입한다(앱별 세션 완전 분리).`, ` session: { secret: process.env.${secretEnv} ?? '${devSessionSecretFor(app)}' },`);
|
|
141
|
+
}
|
|
142
|
+
lines.push(' // 결정 59: 세션 userId → 사용자 로드 배선(g auth). 이게 있어야 로그인 후', ' // this.currentUser / this.requireAuth() 가 실제 사용자를 받는다.', ` auth: { loadUser, loginRedirect: '${prefix}/session/new' },`);
|
|
143
|
+
// 결정 141: 닫는 괄호 오염 방지 — 빈 `defineAppConfig({})` 처럼 삽입 지점 바로 뒤가
|
|
144
|
+
// 닫는 `}` 면 삽입 블록 끝에 개행을 더해 `})` 를 제 줄로 내린다(`},})` 방지).
|
|
145
|
+
const rest = existing.slice(insertAt);
|
|
146
|
+
const closesImmediately = /^\s*\}/.test(rest);
|
|
147
|
+
const block = '\n' + lines.join('\n') + (closesImmediately ? '\n' : '');
|
|
148
|
+
let out = existing.slice(0, insertAt) + block + rest;
|
|
91
149
|
// loadUser import 보장 — gaonjs/config import 바로 뒤에 넣는다.
|
|
92
150
|
if (!/from\s+['"]\.\/auth\.js['"]/.test(out)) {
|
|
93
151
|
const im = out.match(/import\s+\{[^}]*\}\s+from\s+['"]gaonjs\/config['"][^\n]*\n/);
|
|
@@ -113,6 +171,7 @@ export function writeAuthScaffold(cwd, opts = {}) {
|
|
|
113
171
|
// 필요한 최소 세트를 먼저 보장한다 — 이미 있으면(gaon g ui-kit 를 먼저 돌린
|
|
114
172
|
// 경우) skip, 없으면 생성. 이 보장이 없으면 스캐폴드 직후 페이지가 컴포넌트를
|
|
115
173
|
// 해상하지 못해 vue-tsc 가 깨진다(gaon new → g auth 단독 경로 · 결정 60 게이트).
|
|
174
|
+
const includePublic = includesPublicRegistration(opts);
|
|
116
175
|
const ui = writeUiKitFiles(root, authUiKitFiles(app));
|
|
117
176
|
created.push(...ui.created);
|
|
118
177
|
skipped.push(...ui.skipped);
|
|
@@ -124,7 +183,7 @@ export function writeAuthScaffold(cwd, opts = {}) {
|
|
|
124
183
|
// 결정 59: auth 배선이 없으면 로그인 후에도 currentUser 가 영구 null 이다.
|
|
125
184
|
if (file.path === `apps/${app}/app.config.ts`) {
|
|
126
185
|
const existing = readFileSync(abs, 'utf8');
|
|
127
|
-
const patchedConfig = patchAppConfigAddAuth(existing);
|
|
186
|
+
const patchedConfig = patchAppConfigAddAuth(existing, app);
|
|
128
187
|
if (patchedConfig) {
|
|
129
188
|
writeFileSync(abs, patchedConfig, 'utf8');
|
|
130
189
|
patched.push(file.path);
|
|
@@ -134,7 +193,8 @@ export function writeAuthScaffold(cwd, opts = {}) {
|
|
|
134
193
|
skipped.push(file.path);
|
|
135
194
|
warnings.push(`apps/${app}/app.config.ts 가 이미 있어 인증 배선을 자동 추가하지 못했습니다.\n` +
|
|
136
195
|
`→ apps/${app}/app.config.ts 의 defineAppConfig({...}) 에 다음을 추가하세요:\n` +
|
|
137
|
-
`
|
|
196
|
+
` session: { secret: process.env.${sessionSecretEnvFor(app)} ?? '32자 이상 비밀' },\n` +
|
|
197
|
+
` auth: { loadUser, loginRedirect: '${urlPrefixFor(app)}/session/new' } // import { loadUser } from './auth.js'\n` +
|
|
138
198
|
`→ 이 배선이 없으면 로그인해도 this.currentUser/requireAuth() 가 사용자를 받지 못합니다.`);
|
|
139
199
|
}
|
|
140
200
|
else {
|
|
@@ -154,7 +214,7 @@ export function writeAuthScaffold(cwd, opts = {}) {
|
|
|
154
214
|
const routesPath = join(root, 'apps', app, 'routes.ts');
|
|
155
215
|
const routesRel = `apps/${app}/routes.ts`;
|
|
156
216
|
if (existsSync(routesPath)) {
|
|
157
|
-
const patchedContent = patchRoutes(readFileSync(routesPath, 'utf8'));
|
|
217
|
+
const patchedContent = patchRoutes(readFileSync(routesPath, 'utf8'), includePublic);
|
|
158
218
|
if (patchedContent) {
|
|
159
219
|
writeFileSync(routesPath, patchedContent, 'utf8');
|
|
160
220
|
patched.push(routesRel);
|
|
@@ -165,18 +225,27 @@ export function writeAuthScaffold(cwd, opts = {}) {
|
|
|
165
225
|
}
|
|
166
226
|
else {
|
|
167
227
|
mkdirSync(dirname(routesPath), { recursive: true });
|
|
168
|
-
writeFileSync(routesPath, renderTemplate('routes.ts.tpl', app), 'utf8');
|
|
228
|
+
writeFileSync(routesPath, renderTemplate('routes.ts.tpl', app, includePublic), 'utf8');
|
|
169
229
|
created.push(routesRel);
|
|
170
230
|
}
|
|
231
|
+
// 결정 155: 시큐어 앱(공개 가입 미생성)은 역할 게이트 대시보드를 깔았다 — role 컬럼을
|
|
232
|
+
// 두라고 안내한다(§7.5.3 · authorize 예시가 실제 역할 규칙이 되도록).
|
|
233
|
+
if (!includePublic) {
|
|
234
|
+
warnings.push(`apps/${app}/ 는 시큐어 스캐폴드입니다(공개 회원가입 미생성 · 결정 155).\n` +
|
|
235
|
+
`→ 관리자는 직접 만들거나 승격하세요(공개 가입 라우트 없음). 공개 가입이 필요하면 --public 로 다시 생성.\n` +
|
|
236
|
+
`→ 역할 인가를 완성하려면 domain/schema/users.ts 에 role 컬럼을 추가하세요:\n` +
|
|
237
|
+
` role: t.string().default('user'),\n` +
|
|
238
|
+
` 그러면 apps/${app}/controllers/dashboard.ts 의 authorize(역할) 게이트가 실제 역할로 동작합니다.`);
|
|
239
|
+
}
|
|
171
240
|
return { created: created.sort(), skipped: skipped.sort(), patched: patched.sort(), warnings };
|
|
172
241
|
}
|
|
173
242
|
/** `gaon g auth` 진입점. 스캐폴드를 쓰고 결과를 사람/JSON 으로 출력한다. */
|
|
174
243
|
export function runGenerateAuthCommand(opts = {}) {
|
|
175
244
|
const cwd = opts.cwd ?? process.cwd();
|
|
176
245
|
const app = opts.app ?? 'web';
|
|
177
|
-
const result = writeAuthScaffold(cwd, { app });
|
|
246
|
+
const result = writeAuthScaffold(cwd, { app, public: opts.public });
|
|
178
247
|
if (opts.json) {
|
|
179
|
-
process.stdout.write(JSON.stringify({ command: 'g auth', app, ...result }, null, 2) + '\n');
|
|
248
|
+
process.stdout.write(JSON.stringify({ command: 'g auth', app, public: opts.public ?? app === 'web', ...result }, null, 2) + '\n');
|
|
180
249
|
return 0;
|
|
181
250
|
}
|
|
182
251
|
const lines = [''];
|
|
@@ -191,11 +260,13 @@ export function runGenerateAuthCommand(opts = {}) {
|
|
|
191
260
|
for (const w of result.warnings)
|
|
192
261
|
lines.push('', ` ⚠ ${w.split('\n').join('\n ')}`);
|
|
193
262
|
lines.push('');
|
|
263
|
+
const prefix = urlPrefixFor(app);
|
|
264
|
+
const secretEnv = sessionSecretEnvFor(app);
|
|
194
265
|
lines.push(' 다음 단계:');
|
|
195
|
-
lines.push(
|
|
266
|
+
lines.push(` 1) .env 에 REDIS_URL·${secretEnv}(32자 이상)·COOKIE_SECRET 을 설정한다.`);
|
|
196
267
|
lines.push(' 2) gaon db diff && gaon db migrate 로 users 테이블을 만든다.');
|
|
197
268
|
lines.push(' 3) gaon dev 로 실행한다 (.gaon 타입 브리지 생성 + 세션·인증 자동 배선).');
|
|
198
|
-
lines.push(
|
|
269
|
+
lines.push(` → ${prefix}/registration/new 회원가입 · ${prefix}/session/new 로그인 · ${prefix}/dashboard 보호 페이지 (this.requireAuth()).`);
|
|
199
270
|
lines.push('');
|
|
200
271
|
process.stdout.write(lines.join('\n') + '\n');
|
|
201
272
|
return 0;
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ export { runDevCommand, type DevCommandOptions } from "./commands/dev.js";
|
|
|
4
4
|
export { createDevConsole, findComposeFile, isDockerAvailable, inspectCompose, composeUp, composeDown, ensureInfra, startTscWatchers, killChild, startRestartWatcher, isRestartChange, resolveWatchRoots, type DevConsole, type DevConsoleOptions, type DevSource, type DevLevel, type ComposeStatus, type ComposeUpOptions, type EnsureInfraResult, type DockerLocateOptions, type TscWatcherOptions, type TscWatcherHandle, type RestartWatcherOptions, type RestartWatcherHandle, } from "./dev/index.js";
|
|
5
5
|
export { runCheckCommand, type CheckCommandOptions, type CheckStep, type CheckStepStatus, type CheckStepResult, } from "./commands/check.js";
|
|
6
6
|
export { runGenCommand, regenerateProjectGaon, type GenCommandOptions, type GenResult, } from "./commands/gen.js";
|
|
7
|
+
export { runBuildCommand, type BuildCommandOptions, type BuildCommandResult, } from "./commands/build.js";
|
|
8
|
+
export { buildApp, buildAllApps, listFrontendApps, verifyAppDist, appBase, type BuildAppOptions, type DistVerifyResult, } from "./dev/build.js";
|
|
9
|
+
export { generateMessagesDts } from "./messages-gen.js";
|
|
7
10
|
export { runNewCommand, type NewCommandOptions, type NewCommandResult } from "./commands/new.js";
|
|
8
11
|
export { runConsoleCommand, type ConsoleCommandOptions } from "./commands/console.js";
|
|
9
12
|
export { runTestCommand, type TestCommandOptions, type TestScope } from "./commands/test.js";
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { MILESTONES, VERSION, HOMEPAGE, loadDotEnv } from "@gaonjs/core";
|
|
|
14
14
|
import { runDevCommand } from "./commands/dev.js";
|
|
15
15
|
import { runCheckCommand } from "./commands/check.js";
|
|
16
16
|
import { runGenCommand } from "./commands/gen.js";
|
|
17
|
+
import { runBuildCommand } from "./commands/build.js";
|
|
17
18
|
import { runNewCommand } from "./commands/new.js";
|
|
18
19
|
import { runConsoleCommand } from "./commands/console.js";
|
|
19
20
|
import { runTestCommand } from "./commands/test.js";
|
|
@@ -32,6 +33,9 @@ export { runDevCommand } from "./commands/dev.js";
|
|
|
32
33
|
export { createDevConsole, findComposeFile, isDockerAvailable, inspectCompose, composeUp, composeDown, ensureInfra, startTscWatchers, killChild, startRestartWatcher, isRestartChange, resolveWatchRoots, } from "./dev/index.js";
|
|
33
34
|
export { runCheckCommand, } from "./commands/check.js";
|
|
34
35
|
export { runGenCommand, regenerateProjectGaon, } from "./commands/gen.js";
|
|
36
|
+
export { runBuildCommand, } from "./commands/build.js";
|
|
37
|
+
export { buildApp, buildAllApps, listFrontendApps, verifyAppDist, appBase, } from "./dev/build.js";
|
|
38
|
+
export { generateMessagesDts } from "./messages-gen.js";
|
|
35
39
|
export { runNewCommand } from "./commands/new.js";
|
|
36
40
|
export { runConsoleCommand } from "./commands/console.js";
|
|
37
41
|
export { runTestCommand } from "./commands/test.js";
|
|
@@ -100,8 +104,9 @@ function renderHelp(version = VERSION) {
|
|
|
100
104
|
" gaon serve 웹 서버 부팅 (gaon.config.ts 자동 배선 · Fastify listen)",
|
|
101
105
|
" gaon serve --port <n> --host <h> 리슨 포트·호스트 (config 값을 덮음)",
|
|
102
106
|
" gaon serve --workers <n|auto> node:cluster 워커 다중화 (env WEB_CONCURRENCY · 기본 1)",
|
|
103
|
-
" gaon check typecheck · vue-tsc · build 통합 검사 (--only <step> · --
|
|
107
|
+
" gaon check typecheck · vue-tsc · build · doctor 통합 검사 (--only <step> · --no-doctor)",
|
|
104
108
|
" gaon gen .gaon 타입 브리지 + api() 런타임 매니페스트만 재생성 (서버·검사 없이 · build 전제 · --json)",
|
|
109
|
+
" gaon build 멀티 앱 프론트 프로덕션 빌드 (gaon gen + apps/* 순회 · 앱별 dist/<앱>·base=/<앱>/ · --json)",
|
|
105
110
|
" gaon console 프로젝트 컨텍스트 REPL (--no-config)",
|
|
106
111
|
" gaon test 테스트 러너 (테스트 DB <db>_test 자동 생성·마이그레이션 후 vitest · --scope unit|integration|all · -- vitest 인자)",
|
|
107
112
|
" gaon doctor 정적 검사 (25 검사 · 응답 혼용·N+1·의존·커넥션·마이그·컴포저블 순수·자동 import·파일명/컬럼 관례·인증 배선·UI 킷 배선·라우트 등록·정적 충돌·_method·CSRF 배선·내부 앵커·pageProps 구조분해·비동기 오프로드·페이지 레이아웃 브레이크포인트·Link>Button 중첩·seal 클라 배선·보안 역전·§4.5 관계)",
|
|
@@ -220,8 +225,8 @@ export function runCli(argv, opts = {}) {
|
|
|
220
225
|
return;
|
|
221
226
|
}
|
|
222
227
|
// `gaon check` — typecheck · vue-tsc · build (·doctor) 통합 검사(M9-G).
|
|
223
|
-
// package.json 스크립트 관례 재사용. --only <step> 로 단일
|
|
224
|
-
//
|
|
228
|
+
// package.json 스크립트 관례 재사용. --only <step> 로 단일 단계. 결정 157: doctor 는
|
|
229
|
+
// 기본 포함(규칙 5 등이 check 만 도는 CI 에서 새지 않게) · --no-doctor 로만 뺀다.
|
|
225
230
|
if (argv[0] === "check") {
|
|
226
231
|
const knownSteps = ["typecheck", "vue-tsc", "build", "doctor"];
|
|
227
232
|
const onlyIdx = argv.indexOf("--only");
|
|
@@ -232,7 +237,7 @@ export function runCli(argv, opts = {}) {
|
|
|
232
237
|
void runCheckCommand({
|
|
233
238
|
json: argv.includes("--json"),
|
|
234
239
|
only,
|
|
235
|
-
|
|
240
|
+
noDoctor: argv.includes("--no-doctor"),
|
|
236
241
|
})
|
|
237
242
|
.then((code) => {
|
|
238
243
|
process.exitCode = code;
|
|
@@ -258,6 +263,20 @@ export function runCli(argv, opts = {}) {
|
|
|
258
263
|
});
|
|
259
264
|
return;
|
|
260
265
|
}
|
|
266
|
+
// `gaon build` — 멀티 앱 프론트 프로덕션 빌드(결정 146). gaon gen 뒤 apps/* 를
|
|
267
|
+
// 순회해 앱마다 root/outDir/base 로 vite build. 스캐폴드 build 스크립트가 이걸 부른다.
|
|
268
|
+
if (argv[0] === "build") {
|
|
269
|
+
void runBuildCommand({ json: argv.includes("--json") })
|
|
270
|
+
.then((code) => {
|
|
271
|
+
process.exitCode = code;
|
|
272
|
+
})
|
|
273
|
+
.catch((err) => {
|
|
274
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
275
|
+
process.stderr.write(` ✗ gaon build 실패: ${msg}\n`);
|
|
276
|
+
process.exitCode = 1;
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
261
280
|
// `gaon doctor` — 정적 검사(M9-E · 17 검사). --check=<이름>[,<이름>...] 로
|
|
262
281
|
// 선택 실행, --json 은 자동화 파싱용.
|
|
263
282
|
// exit code (M9-E-Fix): fatal → 2(사용자 오류) / errors > 0 → 1 / 그 외 → 0.
|
|
@@ -377,7 +396,8 @@ export function runCli(argv, opts = {}) {
|
|
|
377
396
|
if (argv[1] === "auth") {
|
|
378
397
|
const appIdx = argv.indexOf("--app");
|
|
379
398
|
const app = appIdx >= 0 ? argv[appIdx + 1] : undefined;
|
|
380
|
-
|
|
399
|
+
// 결정 155: --public 은 비-web 앱도 공개 회원가입을 opt-in(공개 비-web 앱 탈출구).
|
|
400
|
+
const code = runGenerateAuthCommand({ app, json: argv.includes("--json"), public: argv.includes("--public") });
|
|
381
401
|
process.exitCode = code;
|
|
382
402
|
return;
|
|
383
403
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// @gaonjs/cli · .gaon/messages.d.ts 생성기 (결정 158 · 13차 W2)
|
|
2
|
+
//
|
|
3
|
+
// tables.d.ts·routes.d.ts 와 같은 .gaon 파이프라인의 세 번째 축(메시지). locales/
|
|
4
|
+
// 카탈로그의 키를 유니온 타입으로 물성화해, t('key') 의 존재하지 않는 키를 컴파일
|
|
5
|
+
// 타임에 잡는다(현재는 GaonMessages 가 비어 있어 키가 string 으로 열림). 생성 파일은
|
|
6
|
+
// 타입만 담는다(규칙 3). @gaonjs/i18n 의 공개 API(loadLocales·renderMessagesDts)만 쓴다.
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { dirname } from 'node:path';
|
|
9
|
+
import { loadLocales, renderMessagesDts } from '@gaonjs/i18n';
|
|
10
|
+
/**
|
|
11
|
+
* locales/ 카탈로그에서 .gaon/messages.d.ts 를 생성한다. 카탈로그가 없거나 비어
|
|
12
|
+
* 있으면 생성하지 않는다(GaonMessages 를 비운 채로 둬 t() 키가 string 폴백 — i18n 을
|
|
13
|
+
* 안 쓰는 프로젝트가 never 로 깨지지 않게). 생성 여부를 돌려준다.
|
|
14
|
+
*/
|
|
15
|
+
export function generateMessagesDts(localesDir, out) {
|
|
16
|
+
if (!existsSync(localesDir))
|
|
17
|
+
return false;
|
|
18
|
+
const resources = loadLocales(localesDir);
|
|
19
|
+
if (Object.keys(resources).length === 0)
|
|
20
|
+
return false;
|
|
21
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
22
|
+
writeFileSync(out, renderMessagesDts(resources), 'utf8');
|
|
23
|
+
return true;
|
|
24
|
+
}
|
package/dist/scaffold/app.js
CHANGED
|
@@ -72,9 +72,10 @@ export function appScaffoldFiles(name) {
|
|
|
72
72
|
contents: [
|
|
73
73
|
`// ${name} 앱 설정 — apps/${name}/app.config.ts.`,
|
|
74
74
|
`// 잘 지은 앱의 app.config.ts 는 거의 비어 있다(v0.16 §1.1 ②).`,
|
|
75
|
+
`// 세션·인증 배선은 \`gaon g auth --app ${name}\` 이 여기에 채운다 — 직접 손대지 않는다.`,
|
|
76
|
+
`// 그 전에 상태 변경(POST 등) 라우트를 두면 doctor 가 세션/CSRF 누락을 잡는다(결정 140).`,
|
|
75
77
|
`// 관례에서 벗어날 때만 값을 채운다:`,
|
|
76
78
|
`// hosts: ['${name}.example.com'] // 도메인 바인딩(개발은 ${name}.localhost 자동)`,
|
|
77
|
-
`// session: { cookie: '${name}_session' } // 세션 옵션 덮어쓰기`,
|
|
78
79
|
`import { defineAppConfig } from 'gaonjs/config'`,
|
|
79
80
|
``,
|
|
80
81
|
`export default defineAppConfig({})`,
|
|
@@ -13,10 +13,10 @@ import Button from '@shared/components/ui/Button.vue'
|
|
|
13
13
|
// 이 페이지는 requireAuth 로 보호되므로 런타임엔 항상 로그인 상태(타입은 nullable).
|
|
14
14
|
const shared = useShared()
|
|
15
15
|
|
|
16
|
-
// 로그아웃 = DELETE /session (r.resource('session') 의 destroy).
|
|
16
|
+
// 로그아웃 = DELETE {{URL_PREFIX}}/session (r.resource('session') 의 destroy).
|
|
17
17
|
// HTML <form> 은 DELETE 를 못 보내므로 Inertia 라우터로 실제 메서드를 보낸다(결정 64).
|
|
18
18
|
function logout(): void {
|
|
19
|
-
router.delete('/session', { headers: { 'x-csrf-token': shared.csrf } })
|
|
19
|
+
router.delete('{{URL_PREFIX}}/session', { headers: { 'x-csrf-token': shared.csrf } })
|
|
20
20
|
}
|
|
21
21
|
</script>
|
|
22
22
|
|
|
@@ -36,7 +36,7 @@ const form = useForm({ email: '', password: '', _csrf: shared.csrf })
|
|
|
36
36
|
<Alert v-if="props.error" variant="destructive" class="mb-4">
|
|
37
37
|
<AlertDescription>{{ props.error }}</AlertDescription>
|
|
38
38
|
</Alert>
|
|
39
|
-
<Form @submit="form.post('/session')">
|
|
39
|
+
<Form @submit="form.post('{{URL_PREFIX}}/session')">
|
|
40
40
|
<FormField label="이메일" :error="form.errors.email">
|
|
41
41
|
<Input v-model="form.email" type="email" required />
|
|
42
42
|
</FormField>
|
|
@@ -45,10 +45,7 @@ const form = useForm({ email: '', password: '', _csrf: shared.csrf })
|
|
|
45
45
|
</FormField>
|
|
46
46
|
<Button type="submit" class="w-full" :disabled="form.processing">로그인</Button>
|
|
47
47
|
</Form>
|
|
48
|
-
|
|
49
|
-
계정이 없으신가요?
|
|
50
|
-
<Link href="/registration/new" class="font-medium text-primary underline-offset-4 hover:underline">회원가입</Link>
|
|
51
|
-
</p>
|
|
48
|
+
{{SIGNUP_LINK}}
|
|
52
49
|
</CardContent>
|
|
53
50
|
</Card>
|
|
54
51
|
</div>
|
|
@@ -33,7 +33,7 @@ const form = useForm({ name: '', email: '', password: '', _csrf: shared.csrf })
|
|
|
33
33
|
<Alert v-if="props.error" variant="destructive" class="mb-4">
|
|
34
34
|
<AlertDescription>{{ props.error }}</AlertDescription>
|
|
35
35
|
</Alert>
|
|
36
|
-
<Form @submit="form.post('/registration')">
|
|
36
|
+
<Form @submit="form.post('{{URL_PREFIX}}/registration')">
|
|
37
37
|
<FormField label="이름" :error="form.errors.name">
|
|
38
38
|
<Input v-model="form.name" required />
|
|
39
39
|
</FormField>
|
|
@@ -47,7 +47,7 @@ const form = useForm({ name: '', email: '', password: '', _csrf: shared.csrf })
|
|
|
47
47
|
</Form>
|
|
48
48
|
<p class="mt-4 text-center text-sm text-muted-foreground">
|
|
49
49
|
이미 계정이 있으신가요?
|
|
50
|
-
<Link href="/session/new" class="font-medium text-primary underline-offset-4 hover:underline">로그인</Link>
|
|
50
|
+
<Link href="{{URL_PREFIX}}/session/new" class="font-medium text-primary underline-offset-4 hover:underline">로그인</Link>
|
|
51
51
|
</p>
|
|
52
52
|
</CardContent>
|
|
53
53
|
</Card>
|
|
@@ -3,9 +3,9 @@ import { defineAppConfig } from 'gaonjs/config'
|
|
|
3
3
|
import { loadUser } from './auth.js'
|
|
4
4
|
|
|
5
5
|
export default defineAppConfig({
|
|
6
|
-
// 세션 secret 은 32자 이상 — .env 의
|
|
7
|
-
session: { secret: process.env.
|
|
6
|
+
// 세션 secret 은 32자 이상 — .env 의 {{SESSION_SECRET_ENV}} 로 주입한다(앱별 세션 완전 분리).
|
|
7
|
+
session: { secret: process.env.{{SESSION_SECRET_ENV}} ?? 'dev-only-session-secret-{{APP_NAME}}-change-me!!' },
|
|
8
8
|
// 결정 59: 세션 userId → 사용자 로드 배선. 이게 있어야 로그인 후
|
|
9
9
|
// this.currentUser / this.requireAuth() 가 실제 사용자를 받는다.
|
|
10
|
-
auth: { loadUser, loginRedirect: '/session/new' },
|
|
10
|
+
auth: { loadUser, loginRedirect: '{{URL_PREFIX}}/session/new' },
|
|
11
11
|
})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { controller } from 'gaonjs/web'
|
|
3
3
|
|
|
4
4
|
export default controller({
|
|
5
|
-
// GET /dashboard — 로그인해야 볼 수 있는 보호 페이지. 비로그인은 로그인으로 보내진다.
|
|
5
|
+
// GET {{URL_PREFIX}}/dashboard — 로그인해야 볼 수 있는 보호 페이지. 비로그인은 로그인으로 보내진다.
|
|
6
6
|
async show() {
|
|
7
7
|
// 보호만 하고 페이지 데이터는 넘기지 않는다 — 사용자·csrf 는 자동 주입되고
|
|
8
8
|
// (결정 116), 페이지는 useShared() 로 읽는다. requireAuth 로 게이트만 건다.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// 보호 라우트 예시(관리 앱) — gaon g auth --app {{APP_NAME}}. 로그인 + 역할로 지킨다(§7 · 결정 145·155).
|
|
2
|
+
import { controller } from 'gaonjs/web'
|
|
3
|
+
|
|
4
|
+
export default controller({
|
|
5
|
+
// GET {{URL_PREFIX}}/dashboard — 로그인만으론 부족한 관리 화면. 역할(role)로 인가한다.
|
|
6
|
+
async show() {
|
|
7
|
+
const user = this.requireAuth() // 인증(401): 로그인 여부
|
|
8
|
+
// 인가(403): 로그인한 일반 사용자는 막는다. domain/schema/users.ts 에 role 컬럼을 두고
|
|
9
|
+
// (예: role: t.string().default('user')) 아래를 실제 역할 규칙으로 바꾸세요.
|
|
10
|
+
// 지금은 role !== 'admin' 이면 403 — 관리 앱에 로그인 고객이 들어오는 위험 기본을 막는다.
|
|
11
|
+
this.authorize((user as { role?: string }).role === 'admin')
|
|
12
|
+
return this.render('Dashboard', {})
|
|
13
|
+
},
|
|
14
|
+
})
|
|
@@ -3,11 +3,11 @@ import { controller, hashPassword } from 'gaonjs/web'
|
|
|
3
3
|
import { User } from '../../../domain/models/User.js'
|
|
4
4
|
|
|
5
5
|
export default controller({
|
|
6
|
-
// GET /registration/new — 회원가입 폼. csrf 는 자동 주입된다(결정 116).
|
|
6
|
+
// GET {{URL_PREFIX}}/registration/new — 회원가입 폼. csrf 는 자동 주입된다(결정 116).
|
|
7
7
|
async new() {
|
|
8
8
|
return this.render('Auth/Signup', { error: null as string | null })
|
|
9
9
|
},
|
|
10
|
-
// POST /registration — 회원가입
|
|
10
|
+
// POST {{URL_PREFIX}}/registration — 회원가입
|
|
11
11
|
async create() {
|
|
12
12
|
const { name, email, password } = this.params({
|
|
13
13
|
_row: {} as { name: string; email: string; password: string },
|
|
@@ -15,6 +15,6 @@ export default controller({
|
|
|
15
15
|
const passwordDigest = await hashPassword(password)
|
|
16
16
|
const user = await User.create({ name, email, passwordDigest })
|
|
17
17
|
this.auth.login(user)
|
|
18
|
-
return this.redirect('/dashboard')
|
|
18
|
+
return this.redirect('{{URL_PREFIX}}/dashboard')
|
|
19
19
|
},
|
|
20
20
|
})
|
|
@@ -3,26 +3,26 @@ import { controller, verifyPassword } from 'gaonjs/web'
|
|
|
3
3
|
import { User } from '../../../domain/models/User.js'
|
|
4
4
|
|
|
5
5
|
export default controller({
|
|
6
|
-
// GET /session/new — 로그인 폼. csrf 는 자동 주입되므로 넘기지 않는다(결정 116).
|
|
6
|
+
// GET {{URL_PREFIX}}/session/new — 로그인 폼. csrf 는 자동 주입되므로 넘기지 않는다(결정 116).
|
|
7
7
|
async new() {
|
|
8
8
|
return this.render('Auth/Login', { error: null as string | null })
|
|
9
9
|
},
|
|
10
|
-
// POST /session — 로그인
|
|
10
|
+
// POST {{URL_PREFIX}}/session — 로그인
|
|
11
11
|
async create() {
|
|
12
12
|
const { email, password } = this.params({ _row: {} as { email: string; password: string } })
|
|
13
13
|
const user = await User.where('email', '=', email).first()
|
|
14
14
|
// passwordDigest 는 hidden 이지만 서버 코드에서는 투명하게 읽힌다(§4.2).
|
|
15
15
|
if (user && (await verifyPassword(password, user.passwordDigest))) {
|
|
16
16
|
this.auth.login(user)
|
|
17
|
-
return this.redirect('/dashboard')
|
|
17
|
+
return this.redirect('{{URL_PREFIX}}/dashboard')
|
|
18
18
|
}
|
|
19
19
|
return this.render('Auth/Login', {
|
|
20
20
|
error: '이메일 또는 비밀번호가 올바르지 않습니다.' as string | null,
|
|
21
21
|
})
|
|
22
22
|
},
|
|
23
|
-
// DELETE /session — 로그아웃
|
|
23
|
+
// DELETE {{URL_PREFIX}}/session — 로그아웃
|
|
24
24
|
async destroy() {
|
|
25
25
|
this.auth.logout()
|
|
26
|
-
return this.redirect('/session/new')
|
|
26
|
+
return this.redirect('{{URL_PREFIX}}/session/new')
|
|
27
27
|
},
|
|
28
28
|
})
|
|
@@ -18,6 +18,12 @@ STORAGE_BUCKET={{PROJECT_NAME}}
|
|
|
18
18
|
STORAGE_ACCESS_KEY={{PROJECT_NAME}}
|
|
19
19
|
STORAGE_SECRET_KEY={{PROJECT_NAME}}_secret
|
|
20
20
|
|
|
21
|
+
# 메일(§7 · M8) — docker-compose.yaml 의 mailpit 서비스와 정합(dev = MailPit sink · UI :8025).
|
|
22
|
+
# 운영은 실 SMTP 호스트·자격증명·SMTP_SECURE=true 로 교체한다.
|
|
23
|
+
SMTP_HOST=127.0.0.1
|
|
24
|
+
SMTP_PORT=1025
|
|
25
|
+
MAIL_FROM=no-reply@{{PROJECT_NAME}}.test
|
|
26
|
+
|
|
21
27
|
# 세션 · 쿠키 서명 비밀 (32자 이상, 운영은 반드시 교체).
|
|
22
28
|
SESSION_SECRET=change-me-to-a-32-char-random-secret!!
|
|
23
29
|
COOKIE_SECRET=change-me-too-32-char-random-secret!!
|
|
@@ -27,6 +27,8 @@ v0.15+errata→v0.16→v0.17 · 결정 31~89)이며, 관례 문서는 **2층 구
|
|
|
27
27
|
| 잡 · 이벤트 · 리스너 · 아웃박스 · 스케줄 | `agents/async.md` |
|
|
28
28
|
| 채널 · 프레즌스 · 허브 | `agents/realtime.md` |
|
|
29
29
|
| 파일 스토리지 (`Storage.put/url` · s3Disk · presigned · CSP 자동 배선) | `agents/storage.md` |
|
|
30
|
+
| 다국어 (`t()` · 카탈로그 · 요청별 로케일 · `this.setLocale` · 메시지 키 타입) | `agents/i18n.md` |
|
|
31
|
+
| 메일 (`mail()` · `deliver(data, { locale })` · MailPit · 발송 경로) | `agents/mail.md` |
|
|
30
32
|
| 테스트 작성·실행 (실 인프라 · `expectJobProcessed`) | `agents/testing.md` |
|
|
31
33
|
| 보안 기본값 · 탈출구(v-html · raw SQL) 사용 | `agents/security.md` |
|
|
32
34
|
| 페이로드 봉인 (`@gaonjs/seal` · wire/문서/WS 암호화 · 선택 플러그인) | `agents/seal.md` |
|
|
@@ -191,7 +193,7 @@ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍
|
|
|
191
193
|
작업마다 실행한다:
|
|
192
194
|
|
|
193
195
|
```bash
|
|
194
|
-
gaon check # .gaon 재생성 → typecheck + vue-tsc + build
|
|
196
|
+
gaon check # .gaon 재생성 → typecheck + vue-tsc + build + doctor (기본 포함 · --no-doctor 로 뺌 · 결정 157)
|
|
195
197
|
gaon test # vitest — DB·NATS 는 실 인프라 (agents/testing.md)
|
|
196
198
|
gaon doctor # 정적 검사 24종 (§2.2)
|
|
197
199
|
```
|
|
@@ -84,7 +84,7 @@ Gaon 프레임웍 문서: https://gaonjs.dev
|
|
|
84
84
|
## 3. 개발 검증 루프 (작업마다 실행)
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
|
-
gaon check # .gaon 재생성
|
|
87
|
+
gaon check # .gaon 재생성 → 타입검사+build+doctor (CI 한 번에 · --no-doctor 로 doctor 뺌)
|
|
88
88
|
gaon doctor # 정적 검사 24종 (상세 AGENTS §2.2)
|
|
89
89
|
npm test # Vitest · DB 테스트는 실 Docker 필수 (§9)
|
|
90
90
|
```
|