@gaonjs/cli 0.13.0 → 0.13.1
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/templates/auth/Dashboard.vue.tpl +11 -5
- package/dist/templates/auth/Login.vue.tpl +9 -6
- package/dist/templates/auth/Signup.vue.tpl +9 -7
- package/dist/templates/auth/dashboard.controller.ts.tpl +2 -1
- package/dist/templates/project/AGENTS.md.tpl +3 -2
- package/dist/templates/project/agents/realtime.md.tpl +13 -5
- package/dist/templates/project/agents/web.md.tpl +19 -3
- package/package.json +4 -4
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { pageProps } from 'gaonjs/vue'
|
|
2
|
+
import { pageProps, router } from 'gaonjs/vue'
|
|
3
3
|
|
|
4
4
|
// dashboard#show 의 render props — user 는 직렬화되며 passwordDigest 는 없다(§4.2).
|
|
5
|
-
const { user } = pageProps<'{{APP_NAME}}:dashboard#show'>()
|
|
5
|
+
const { user, csrf } = pageProps<'{{APP_NAME}}:dashboard#show'>()
|
|
6
|
+
|
|
7
|
+
// 로그아웃 = DELETE /session (r.resource('session') 의 destroy).
|
|
8
|
+
// HTML <form> 은 DELETE 를 보낼 수 없으므로 Inertia 라우터로 실제 메서드를
|
|
9
|
+
// 보낸다(결정 64) — `?_method=DELETE` 우회는 서버가 해석하지 않아 POST /session
|
|
10
|
+
// (= 로그인 create) 으로 잘못 라우팅됐다.
|
|
11
|
+
function logout(): void {
|
|
12
|
+
router.delete('/session', { headers: { 'x-csrf-token': csrf } })
|
|
13
|
+
}
|
|
6
14
|
</script>
|
|
7
15
|
|
|
8
16
|
<template>
|
|
9
17
|
<main>
|
|
10
18
|
<h1>환영합니다, {{ user.name }}님</h1>
|
|
11
19
|
<p>{{ user.email }}</p>
|
|
12
|
-
<
|
|
13
|
-
<button type="submit">로그아웃</button>
|
|
14
|
-
</form>
|
|
20
|
+
<button type="button" @click="logout">로그아웃</button>
|
|
15
21
|
</main>
|
|
16
22
|
</template>
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { pageProps } from 'gaonjs/vue'
|
|
2
|
+
import { pageProps, useForm } from 'gaonjs/vue'
|
|
3
3
|
|
|
4
4
|
// 컨트롤러 session#new 의 render props 타입이 그대로 흐른다(§6.2).
|
|
5
5
|
const { error, csrf } = pageProps<'{{APP_NAME}}:session#new'>()
|
|
6
|
+
|
|
7
|
+
// 세션 앱 폼 = Inertia SPA 제출(결정 64) — fetch() 로 만들지 않는다.
|
|
8
|
+
// 서버는 redirect(Inertia 응답)로 답하고, 실패 시 같은 페이지를 다시 render 한다.
|
|
9
|
+
const form = useForm({ email: '', password: '', _csrf: csrf })
|
|
6
10
|
</script>
|
|
7
11
|
|
|
8
12
|
<template>
|
|
9
|
-
<form
|
|
10
|
-
<input type="hidden" name="_csrf" :value="csrf" />
|
|
13
|
+
<form @submit.prevent="form.post('/session')">
|
|
11
14
|
<h1>로그인</h1>
|
|
12
15
|
<p v-if="error" class="error">{{ error }}</p>
|
|
13
|
-
<label>이메일 <input
|
|
14
|
-
<label>비밀번호 <input
|
|
15
|
-
<button type="submit">로그인</button>
|
|
16
|
+
<label>이메일 <input v-model="form.email" type="email" required /></label>
|
|
17
|
+
<label>비밀번호 <input v-model="form.password" type="password" required /></label>
|
|
18
|
+
<button type="submit" :disabled="form.processing">로그인</button>
|
|
16
19
|
<a href="/registration/new">회원가입</a>
|
|
17
20
|
</form>
|
|
18
21
|
</template>
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { pageProps } from 'gaonjs/vue'
|
|
2
|
+
import { pageProps, useForm } from 'gaonjs/vue'
|
|
3
3
|
|
|
4
4
|
const { error, csrf } = pageProps<'{{APP_NAME}}:registration#new'>()
|
|
5
|
+
|
|
6
|
+
// 세션 앱 폼 = Inertia SPA 제출(결정 64) — fetch() 로 만들지 않는다.
|
|
7
|
+
const form = useForm({ name: '', email: '', password: '', _csrf: csrf })
|
|
5
8
|
</script>
|
|
6
9
|
|
|
7
10
|
<template>
|
|
8
|
-
<form
|
|
9
|
-
<input type="hidden" name="_csrf" :value="csrf" />
|
|
11
|
+
<form @submit.prevent="form.post('/registration')">
|
|
10
12
|
<h1>회원가입</h1>
|
|
11
13
|
<p v-if="error" class="error">{{ error }}</p>
|
|
12
|
-
<label>이름 <input
|
|
13
|
-
<label>이메일 <input
|
|
14
|
-
<label>비밀번호 <input
|
|
15
|
-
<button type="submit">회원가입</button>
|
|
14
|
+
<label>이름 <input v-model="form.name" required /></label>
|
|
15
|
+
<label>이메일 <input v-model="form.email" type="email" required /></label>
|
|
16
|
+
<label>비밀번호 <input v-model="form.password" type="password" required /></label>
|
|
17
|
+
<button type="submit" :disabled="form.processing">회원가입</button>
|
|
16
18
|
<a href="/session/new">로그인</a>
|
|
17
19
|
</form>
|
|
18
20
|
</template>
|
|
@@ -5,6 +5,7 @@ export default controller({
|
|
|
5
5
|
// GET /dashboard — 로그인해야 볼 수 있는 보호 페이지. 비로그인은 로그인으로 보내진다.
|
|
6
6
|
async show() {
|
|
7
7
|
const user = this.requireAuth()
|
|
8
|
-
|
|
8
|
+
// csrf 는 로그아웃(DELETE /session · 결정 64)이 헤더로 실어 보낸다.
|
|
9
|
+
return this.render('Dashboard', { user, csrf: this.csrfToken() })
|
|
9
10
|
},
|
|
10
11
|
})
|
|
@@ -67,8 +67,9 @@ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍
|
|
|
67
67
|
설정으로만 (`agents/security.md`).
|
|
68
68
|
5. **테스트는 Docker 실인프라 필수** — DB·NATS 목업·인메모리 대체 절대
|
|
69
69
|
금지 (§9 · `agents/testing.md`).
|
|
70
|
-
6. **인증·폼은 Inertia SPA 방식** (SSR 아님). 폼은 `
|
|
71
|
-
|
|
70
|
+
6. **인증·폼은 Inertia SPA 방식** (SSR 아님). 폼은 `gaonjs/vue` 의
|
|
71
|
+
`useForm(...).post(...)` → 서버 redirect (HTML 폼이 못 보내는 메서드는
|
|
72
|
+
`router.delete(...)` 등). REST + `fetch()` 는 **API 앱(JWT) 전용**.
|
|
72
73
|
7. **한 액션은 한 종류 응답만** (render 또는 JSON 또는 redirect —
|
|
73
74
|
혼용 금지 · doctor response-mixing).
|
|
74
75
|
8. **`.gaon/` 자동 생성 파일 편집 금지** — `routes.d.ts`·`tables.d.ts`
|
|
@@ -94,14 +94,22 @@ const members = await ctx.presence()
|
|
|
94
94
|
브라우저는 표준 WebSocket 으로 채널에 접속한다. 세션 앱은 세션 쿠키로,
|
|
95
95
|
JWT 앱은 쿼리(`access_token`)로 인증한다.
|
|
96
96
|
|
|
97
|
+
**경로는 `<앱 프리픽스>/gaon/ws/<채널명>`** 이고, 보내는 프레임은 `{ t: 'msg',
|
|
98
|
+
data }` **봉투** 다 — 날 페이로드를 그대로 보내면 서버가 `onMessage` 로
|
|
99
|
+
흘리지 않는다. 받는 프레임도 같은 모양(`t` 로 종류를 가른다).
|
|
100
|
+
|
|
97
101
|
```ts
|
|
98
|
-
// 브라우저측
|
|
99
|
-
|
|
102
|
+
// 브라우저측 — 채널 구독 래핑은 컴포저블에 (agents/frontend.md)
|
|
103
|
+
// web 앱(프리픽스 /)의 room 채널 = /gaon/ws/room. admin 앱이면 /admin/gaon/ws/room.
|
|
104
|
+
const ws = new WebSocket(`wss://example.com/gaon/ws/room?room=42`)
|
|
100
105
|
ws.onmessage = (ev) => {
|
|
101
|
-
const
|
|
102
|
-
|
|
106
|
+
const frame = JSON.parse(ev.data) // { t: 'msg' | 'presence' | ... , data }
|
|
107
|
+
if (frame.t === 'msg') {
|
|
108
|
+
// 서버가 broadcast/send 한 데이터
|
|
109
|
+
}
|
|
103
110
|
}
|
|
104
|
-
|
|
111
|
+
// 보낼 때도 봉투로 감싼다 — 이래야 채널의 onMessage 가 호출된다.
|
|
112
|
+
ws.send(JSON.stringify({ t: 'msg', data: { text: '안녕하세요' } }))
|
|
105
113
|
```
|
|
106
114
|
|
|
107
115
|
### 5. 허브 프로세스 (`gaon hub`)
|
|
@@ -105,10 +105,26 @@ export default controller({
|
|
|
105
105
|
데이터가 필요할 때는 루트 `AGENTS.md` 의 데이터 경로 4종 판단표를 따른다
|
|
106
106
|
(Inertia partial reload · 채널/프레즌스 · JSON 액션 + `api()` · 별도 API 앱 + JWT).
|
|
107
107
|
**Inertia = SPA + 서버 라우팅**이지 SSR 이 아니다. 로그인·회원가입도 컨트롤러
|
|
108
|
-
`this.render('
|
|
108
|
+
`this.render('Auth/Login')` + Vue 페이지의 `useForm(...).post(...)` → 서버
|
|
109
109
|
redirect 로 처리한다 — 전체 페이지 리로드도, 별도 REST 엔드포인트도
|
|
110
110
|
없다. REST + `fetch()` 는 **API 앱(JWT) 전용**.
|
|
111
111
|
|
|
112
|
+
폼 API 는 **`gaonjs/vue` 의 `useForm`·`router`** 뿐이다(결정 64) —
|
|
113
|
+
`@inertiajs/vue3` 를 직접 import 하지 않는다(파사드가 The One Way).
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// 로그인 폼 — 제출은 Inertia SPA 방식, 서버는 redirect 로 답한다.
|
|
117
|
+
const { error, csrf } = pageProps<'web:session#new'>()
|
|
118
|
+
const form = useForm({ email: '', password: '', _csrf: csrf })
|
|
119
|
+
// <form @submit.prevent="form.post('/session')">
|
|
120
|
+
|
|
121
|
+
// HTML <form> 이 못 보내는 메서드(DELETE 등)는 router 로 보낸다.
|
|
122
|
+
router.delete('/session', { headers: { 'x-csrf-token': csrf } })
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`?_method=DELETE` 같은 우회는 **서버가 해석하지 않는다** — POST 로 나가
|
|
126
|
+
엉뚱한 액션(create)에 도달한다.
|
|
127
|
+
|
|
112
128
|
### 5. 비밀번호 해싱 — `hashPassword` · `verifyPassword` (`gaonjs/web`)
|
|
113
129
|
|
|
114
130
|
회원가입·로그인에서 비밀번호를 다룰 때는 **직접 crypto/bcrypt 를 import 하거나
|
|
@@ -220,8 +236,8 @@ export default controller({
|
|
|
220
236
|
- **render/JSON/redirect 를 한 액션에서 조건 혼용하면 doctor
|
|
221
237
|
response-mixing 위반** — 액션을 나눈다. 예외는 폼 액션의
|
|
222
238
|
"실패 render + 성공 redirect" 조합 하나뿐(결정 57 보완).
|
|
223
|
-
- **`fetch()` 로 로그인 폼 구현 금지** — 세션 앱 폼은 `
|
|
224
|
-
REST + fetch 는 API 앱(JWT) 전용.
|
|
239
|
+
- **`fetch()` 로 로그인 폼 구현 금지** — 세션 앱 폼은 `gaonjs/vue` 의
|
|
240
|
+
`useForm(...).post(...)`(결정 64). REST + fetch 는 API 앱(JWT) 전용.
|
|
225
241
|
- **컨트롤러에 비즈니스 로직 인라인 금지** (§5.3 One Way) — 여러 모델·
|
|
226
242
|
트랜잭션·외부 API 가 얽히면 `domain/services/`.
|
|
227
243
|
- **컨트롤러에서 메일·외부 발송 직접 호출 금지** — 잡 발행만
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaonjs/cli",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "Gaon CLI 구현: 제너레이터·스캐폴딩·로드맵 출력 (M1 스텁)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"vite": "^7.0.0",
|
|
30
30
|
"@gaonjs/async": "0.3.1",
|
|
31
31
|
"@gaonjs/config": "0.3.0",
|
|
32
|
-
"@gaonjs/
|
|
33
|
-
"@gaonjs/mail": "0.1.1",
|
|
32
|
+
"@gaonjs/core": "0.1.4",
|
|
34
33
|
"@gaonjs/data": "0.8.4",
|
|
35
|
-
"@gaonjs/
|
|
34
|
+
"@gaonjs/web": "0.5.3",
|
|
35
|
+
"@gaonjs/mail": "0.1.1"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"require('fs').cpSync('src/templates','dist/templates',{recursive:true})\""
|