vasp-cli 0.3.1 → 0.9.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/vasp +80 -31
- package/package.json +2 -2
- package/starters/minimal.vasp +1 -1
- package/starters/recipe.vasp +70 -0
- package/starters/todo-auth-ssr.vasp +33 -20
- package/starters/todo.vasp +15 -8
- package/templates/shared/.gitignore.hbs +1 -0
- package/templates/shared/README.md.hbs +53 -0
- package/templates/shared/auth/client/Login.vue.hbs +1 -1
- package/templates/shared/auth/client/Register.vue.hbs +1 -1
- package/templates/shared/auth/server/index.hbs +4 -8
- package/templates/shared/auth/server/middleware.hbs +33 -15
- package/templates/shared/auth/server/plugin.hbs +7 -0
- package/templates/shared/auth/server/providers/github.hbs +1 -1
- package/templates/shared/auth/server/providers/google.hbs +1 -1
- package/templates/shared/auth/server/providers/usernameAndPassword.hbs +3 -6
- package/templates/shared/bunfig.toml.hbs +3 -0
- package/templates/shared/drizzle/schema.hbs +39 -9
- package/templates/shared/jobs/_job.hbs +12 -2
- package/templates/shared/package.json.hbs +14 -4
- package/templates/shared/server/db/client.hbs +19 -1
- package/templates/shared/server/db/seed.hbs +16 -0
- package/templates/shared/server/index.hbs +48 -0
- package/templates/shared/server/middleware/errorHandler.hbs +75 -0
- package/templates/shared/server/middleware/logger.hbs +74 -0
- package/templates/{templates/shared → shared}/server/middleware/rateLimit.hbs +2 -2
- package/templates/shared/server/routes/_vasp.hbs +37 -0
- package/templates/shared/server/routes/actions/_action.hbs +5 -1
- package/templates/shared/server/routes/api/_api.hbs +24 -0
- package/templates/shared/server/routes/crud/_crud.hbs +103 -11
- package/templates/shared/server/routes/queries/_query.hbs +5 -1
- package/templates/shared/server/routes/realtime/_channel.hbs +58 -10
- package/templates/shared/shared/types.hbs +58 -0
- package/templates/shared/shared/validation.hbs +20 -0
- package/templates/shared/tests/actions/_action.test.js.hbs +7 -0
- package/templates/shared/tests/actions/_action.test.ts.hbs +7 -0
- package/templates/shared/tests/auth/login.test.js.hbs +7 -0
- package/templates/shared/tests/auth/login.test.ts.hbs +7 -0
- package/templates/shared/tests/crud/_entity.test.js.hbs +7 -0
- package/templates/shared/tests/crud/_entity.test.ts.hbs +7 -0
- package/templates/shared/tests/queries/_query.test.js.hbs +7 -0
- package/templates/shared/tests/queries/_query.test.ts.hbs +7 -0
- package/templates/shared/tests/setup.js.hbs +5 -0
- package/templates/shared/tests/setup.ts.hbs +5 -0
- package/templates/shared/tests/vitest.config.js.hbs +8 -0
- package/templates/shared/tests/vitest.config.ts.hbs +8 -0
- package/templates/shared/tsconfig.json.hbs +2 -1
- package/templates/spa/js/src/App.vue.hbs +9 -1
- package/templates/spa/js/src/components/VaspErrorBoundary.vue.hbs +33 -0
- package/templates/spa/js/src/components/VaspNotifications.vue.hbs +60 -0
- package/templates/spa/js/src/vasp/auth.js.hbs +31 -15
- package/templates/spa/js/src/vasp/client/actions.js.hbs +7 -1
- package/templates/spa/js/src/vasp/client/crud.js.hbs +94 -5
- package/templates/spa/js/src/vasp/useVaspNotifications.js.hbs +35 -0
- package/templates/spa/js/vite.config.js.hbs +1 -0
- package/templates/spa/ts/src/App.vue.hbs +9 -1
- package/templates/spa/ts/src/components/VaspErrorBoundary.vue.hbs +33 -0
- package/templates/spa/ts/src/components/VaspNotifications.vue.hbs +60 -0
- package/templates/spa/ts/src/vasp/auth.ts.hbs +31 -15
- package/templates/spa/ts/src/vasp/client/actions.ts.hbs +7 -1
- package/templates/spa/ts/src/vasp/client/crud.ts.hbs +96 -10
- package/templates/spa/ts/src/vasp/client/types.ts.hbs +14 -28
- package/templates/spa/ts/src/vasp/useVaspNotifications.ts.hbs +41 -0
- package/templates/spa/ts/vite.config.ts.hbs +1 -0
- package/templates/ssr/js/error.vue.hbs +23 -0
- package/templates/ssr/js/nuxt.config.js.hbs +1 -0
- package/templates/ssr/js/plugins/vasp.client.js.hbs +11 -1
- package/templates/ssr/ts/error.vue.hbs +26 -0
- package/templates/ssr/ts/nuxt.config.ts.hbs +1 -0
- package/templates/ssr/ts/plugins/vasp.client.ts.hbs +11 -1
- package/templates/starters/minimal.vasp +15 -0
- package/templates/starters/recipe.vasp +70 -0
- package/templates/starters/todo-auth-ssr.vasp +65 -0
- package/templates/starters/todo.vasp +42 -0
- package/templates/templates/shared/.gitignore.hbs +0 -8
- package/templates/templates/shared/auth/client/Login.vue.hbs +0 -46
- package/templates/templates/shared/auth/client/Register.vue.hbs +0 -42
- package/templates/templates/shared/auth/server/index.hbs +0 -51
- package/templates/templates/shared/auth/server/middleware.hbs +0 -33
- package/templates/templates/shared/auth/server/providers/github.hbs +0 -48
- package/templates/templates/shared/auth/server/providers/google.hbs +0 -53
- package/templates/templates/shared/auth/server/providers/usernameAndPassword.hbs +0 -69
- package/templates/templates/shared/bunfig.toml.hbs +0 -2
- package/templates/templates/shared/drizzle/schema.hbs +0 -48
- package/templates/templates/shared/jobs/_job.hbs +0 -34
- package/templates/templates/shared/jobs/boss.hbs +0 -15
- package/templates/templates/shared/package.json.hbs +0 -35
- package/templates/templates/shared/server/db/client.hbs +0 -12
- package/templates/templates/shared/server/index.hbs +0 -60
- package/templates/templates/shared/server/routes/actions/_action.hbs +0 -20
- package/templates/templates/shared/server/routes/crud/_crud.hbs +0 -86
- package/templates/templates/shared/server/routes/jobs/_schedule.hbs +0 -12
- package/templates/templates/shared/server/routes/queries/_query.hbs +0 -20
- package/templates/templates/shared/server/routes/realtime/_channel.hbs +0 -78
- package/templates/templates/shared/server/routes/realtime/index.hbs +0 -9
- package/templates/templates/shared/tsconfig.json.hbs +0 -21
- package/templates/templates/spa/js/index.html.hbs +0 -12
- package/templates/templates/spa/js/src/App.vue.hbs +0 -3
- package/templates/templates/spa/js/src/main.js.hbs +0 -9
- package/templates/templates/spa/js/src/router/index.js.hbs +0 -41
- package/templates/templates/spa/js/src/vasp/auth.js.hbs +0 -45
- package/templates/templates/spa/js/src/vasp/client/actions.js.hbs +0 -15
- package/templates/templates/spa/js/src/vasp/client/crud.js.hbs +0 -30
- package/templates/templates/spa/js/src/vasp/client/index.js.hbs +0 -16
- package/templates/templates/spa/js/src/vasp/client/queries.js.hbs +0 -15
- package/templates/templates/spa/js/src/vasp/client/realtime.js.hbs +0 -51
- package/templates/templates/spa/js/src/vasp/plugin.js.hbs +0 -11
- package/templates/templates/spa/js/vite.config.js.hbs +0 -26
- package/templates/templates/spa/ts/index.html.hbs +0 -12
- package/templates/templates/spa/ts/src/App.vue.hbs +0 -3
- package/templates/templates/spa/ts/src/main.ts.hbs +0 -9
- package/templates/templates/spa/ts/src/router/index.ts.hbs +0 -41
- package/templates/templates/spa/ts/src/vasp/auth.ts.hbs +0 -53
- package/templates/templates/spa/ts/src/vasp/client/actions.ts.hbs +0 -19
- package/templates/templates/spa/ts/src/vasp/client/crud.ts.hbs +0 -37
- package/templates/templates/spa/ts/src/vasp/client/index.ts.hbs +0 -17
- package/templates/templates/spa/ts/src/vasp/client/queries.ts.hbs +0 -19
- package/templates/templates/spa/ts/src/vasp/client/realtime.ts.hbs +0 -56
- package/templates/templates/spa/ts/src/vasp/client/types.ts.hbs +0 -33
- package/templates/templates/spa/ts/src/vasp/plugin.ts.hbs +0 -12
- package/templates/templates/spa/ts/vite.config.ts.hbs +0 -26
- package/templates/templates/ssr/js/_page.vue.hbs +0 -10
- package/templates/templates/ssr/js/app.vue.hbs +0 -3
- package/templates/templates/ssr/js/composables/useAuth.js.hbs +0 -52
- package/templates/templates/ssr/js/composables/useVasp.js.hbs +0 -6
- package/templates/templates/ssr/js/middleware/auth.js.hbs +0 -8
- package/templates/templates/ssr/js/nuxt.config.js.hbs +0 -15
- package/templates/templates/ssr/js/plugins/vasp.client.js.hbs +0 -27
- package/templates/templates/ssr/js/plugins/vasp.server.js.hbs +0 -33
- package/templates/templates/ssr/ts/_page.vue.hbs +0 -10
- package/templates/templates/ssr/ts/app.vue.hbs +0 -3
- package/templates/templates/ssr/ts/composables/useAuth.ts.hbs +0 -56
- package/templates/templates/ssr/ts/composables/useVasp.ts.hbs +0 -10
- package/templates/templates/ssr/ts/middleware/auth.ts.hbs +0 -8
- package/templates/templates/ssr/ts/nuxt.config.ts.hbs +0 -19
- package/templates/templates/ssr/ts/plugins/vasp.client.ts.hbs +0 -27
- package/templates/templates/ssr/ts/plugins/vasp.server.ts.hbs +0 -33
- /package/templates/{templates/shared/.env.example.hbs → shared/.env.hbs} +0 -0
- /package/templates/{templates/shared → shared}/drizzle/drizzle.config.hbs +0 -0
- /package/templates/{templates/shared → shared}/server/middleware/csrf.hbs +0 -0
|
@@ -1,35 +1,121 @@
|
|
|
1
1
|
// Auto-generated by Vasp — do not edit
|
|
2
2
|
import { $fetch } from 'ofetch'
|
|
3
|
+
import { safeParse } from 'valibot'
|
|
4
|
+
import { ref } from 'vue'
|
|
3
5
|
import type {
|
|
4
6
|
{{#each cruds}}
|
|
5
7
|
{{pascalCase entity}},
|
|
6
|
-
|
|
8
|
+
Create{{pascalCase entity}}Input,
|
|
9
|
+
Update{{pascalCase entity}}Input,
|
|
7
10
|
{{/each}}
|
|
8
11
|
} from './types.js'
|
|
12
|
+
import {
|
|
13
|
+
{{#each cruds}}
|
|
14
|
+
Create{{pascalCase entity}}Schema,
|
|
15
|
+
Update{{pascalCase entity}}Schema,
|
|
16
|
+
{{/each}}
|
|
17
|
+
} from '@shared/validation.js'
|
|
18
|
+
import { notifyError } from '../useVaspNotifications.js'
|
|
9
19
|
|
|
10
20
|
const API = import.meta.env.VITE_API_URL || '/api'
|
|
11
21
|
|
|
12
22
|
{{#each cruds}}
|
|
13
23
|
export function use{{pascalCase entity}}Crud() {
|
|
14
24
|
const base = `${API}/crud/{{camelCase entity}}`
|
|
25
|
+
const loading = ref(false)
|
|
26
|
+
const error = ref<string | null>(null)
|
|
27
|
+
|
|
28
|
+
const fail = (err: unknown) => {
|
|
29
|
+
error.value = err instanceof Error ? err.message : 'Request failed'
|
|
30
|
+
notifyError(err)
|
|
31
|
+
}
|
|
15
32
|
|
|
16
33
|
return {
|
|
34
|
+
loading,
|
|
35
|
+
error,
|
|
17
36
|
{{#if (includes operations "list")}}
|
|
18
|
-
list: (): Promise<{{pascalCase entity}}[]> =>
|
|
37
|
+
list: async (): Promise<{{pascalCase entity}}[]> => {
|
|
38
|
+
loading.value = true
|
|
39
|
+
error.value = null
|
|
40
|
+
try {
|
|
41
|
+
return await $fetch(base, { credentials: 'include' })
|
|
42
|
+
} catch (err) {
|
|
43
|
+
fail(err)
|
|
44
|
+
throw err
|
|
45
|
+
} finally {
|
|
46
|
+
loading.value = false
|
|
47
|
+
}
|
|
48
|
+
},
|
|
19
49
|
{{/if}}
|
|
20
50
|
{{#if (includes operations "create")}}
|
|
21
|
-
create: (data:
|
|
22
|
-
|
|
51
|
+
create: async (data: Create{{pascalCase entity}}Input): Promise<{{pascalCase entity}}> => {
|
|
52
|
+
const parsed = safeParse(Create{{pascalCase entity}}Schema, data)
|
|
53
|
+
if (!parsed.success) {
|
|
54
|
+
const msg = parsed.issues?.[0]?.message ?? 'Invalid input'
|
|
55
|
+
const err = new Error(msg)
|
|
56
|
+
fail(err)
|
|
57
|
+
throw err
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
loading.value = true
|
|
61
|
+
error.value = null
|
|
62
|
+
try {
|
|
63
|
+
return await $fetch(base, { method: 'POST', body: parsed.output, credentials: 'include' })
|
|
64
|
+
} catch (err) {
|
|
65
|
+
fail(err)
|
|
66
|
+
throw err
|
|
67
|
+
} finally {
|
|
68
|
+
loading.value = false
|
|
69
|
+
}
|
|
70
|
+
},
|
|
23
71
|
{{/if}}
|
|
24
|
-
get: (id: number): Promise<{{pascalCase entity}}> =>
|
|
25
|
-
|
|
72
|
+
get: async (id: number): Promise<{{pascalCase entity}}> => {
|
|
73
|
+
loading.value = true
|
|
74
|
+
error.value = null
|
|
75
|
+
try {
|
|
76
|
+
return await $fetch(`${base}/${id}`, { credentials: 'include' })
|
|
77
|
+
} catch (err) {
|
|
78
|
+
fail(err)
|
|
79
|
+
throw err
|
|
80
|
+
} finally {
|
|
81
|
+
loading.value = false
|
|
82
|
+
}
|
|
83
|
+
},
|
|
26
84
|
{{#if (includes operations "update")}}
|
|
27
|
-
update: (id: number, data:
|
|
28
|
-
|
|
85
|
+
update: async (id: number, data: Update{{pascalCase entity}}Input): Promise<{{pascalCase entity}}> => {
|
|
86
|
+
const parsed = safeParse(Update{{pascalCase entity}}Schema, data)
|
|
87
|
+
if (!parsed.success) {
|
|
88
|
+
const msg = parsed.issues?.[0]?.message ?? 'Invalid input'
|
|
89
|
+
const err = new Error(msg)
|
|
90
|
+
fail(err)
|
|
91
|
+
throw err
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
loading.value = true
|
|
95
|
+
error.value = null
|
|
96
|
+
try {
|
|
97
|
+
return await $fetch(`${base}/${id}`, { method: 'PUT', body: parsed.output, credentials: 'include' })
|
|
98
|
+
} catch (err) {
|
|
99
|
+
fail(err)
|
|
100
|
+
throw err
|
|
101
|
+
} finally {
|
|
102
|
+
loading.value = false
|
|
103
|
+
}
|
|
104
|
+
},
|
|
29
105
|
{{/if}}
|
|
30
106
|
{{#if (includes operations "delete")}}
|
|
31
|
-
remove: (id: number): Promise<{ ok: boolean }> =>
|
|
32
|
-
|
|
107
|
+
remove: async (id: number): Promise<{ ok: boolean }> => {
|
|
108
|
+
loading.value = true
|
|
109
|
+
error.value = null
|
|
110
|
+
try {
|
|
111
|
+
return await $fetch(`${base}/${id}`, { method: 'DELETE', credentials: 'include' })
|
|
112
|
+
} catch (err) {
|
|
113
|
+
fail(err)
|
|
114
|
+
throw err
|
|
115
|
+
} finally {
|
|
116
|
+
loading.value = false
|
|
117
|
+
}
|
|
118
|
+
},
|
|
33
119
|
{{/if}}
|
|
34
120
|
}
|
|
35
121
|
}
|
|
@@ -1,33 +1,19 @@
|
|
|
1
|
-
// Auto-generated by Vasp — entity types
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
{{pascalCase entity}},
|
|
5
|
-
New{{pascalCase entity}},
|
|
6
|
-
{{/each}}
|
|
7
|
-
{{#if hasAuth}}
|
|
8
|
-
User,
|
|
9
|
-
{{/if}}
|
|
10
|
-
} from '../../../../../../drizzle/schema.js'
|
|
1
|
+
// Auto-generated by Vasp — re-exports entity types from shared
|
|
2
|
+
// Entity interfaces and input types are generated in shared/types.ts
|
|
3
|
+
// Query/action type stubs can be customized there
|
|
11
4
|
|
|
5
|
+
export type {
|
|
6
|
+
{{#each entities}}
|
|
7
|
+
{{pascalCase name}},
|
|
8
|
+
Create{{pascalCase name}}Input,
|
|
9
|
+
Update{{pascalCase name}}Input,
|
|
10
|
+
{{/each}}
|
|
12
11
|
{{#each queries}}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export type {{pascalCase name}}Return = unknown
|
|
16
|
-
|
|
12
|
+
{{pascalCase name}}Args,
|
|
13
|
+
{{pascalCase name}}Return,
|
|
17
14
|
{{/each}}
|
|
18
15
|
{{#each actions}}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export type {{pascalCase name}}Return = unknown
|
|
22
|
-
|
|
23
|
-
{{/each}}
|
|
24
|
-
// Re-export entity types for convenience
|
|
25
|
-
export type {
|
|
26
|
-
{{#each cruds}}
|
|
27
|
-
{{pascalCase entity}},
|
|
28
|
-
New{{pascalCase entity}},
|
|
16
|
+
{{pascalCase name}}Args,
|
|
17
|
+
{{pascalCase name}}Return,
|
|
29
18
|
{{/each}}
|
|
30
|
-
|
|
31
|
-
User,
|
|
32
|
-
{{/if}}
|
|
33
|
-
}
|
|
19
|
+
} from '@shared/types.js'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type VaspNotification = {
|
|
4
|
+
id: number
|
|
5
|
+
type: 'error' | 'success' | 'info'
|
|
6
|
+
message: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const notifications = ref<VaspNotification[]>([])
|
|
10
|
+
|
|
11
|
+
export function pushNotification(type: VaspNotification['type'], message: string, timeoutMs = 3500): number {
|
|
12
|
+
const id = Date.now() + Math.floor(Math.random() * 1000)
|
|
13
|
+
notifications.value.push({ id, type, message })
|
|
14
|
+
|
|
15
|
+
if (timeoutMs > 0) {
|
|
16
|
+
setTimeout(() => removeNotification(id), timeoutMs)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return id
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function removeNotification(id: number): void {
|
|
23
|
+
notifications.value = notifications.value.filter((item) => item.id !== id)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function notifyError(error: unknown): void {
|
|
27
|
+
if (error instanceof Error && error.message) {
|
|
28
|
+
pushNotification('error', error.message)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
pushNotification('error', 'Request failed')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useVaspNotifications() {
|
|
35
|
+
return {
|
|
36
|
+
notifications,
|
|
37
|
+
pushNotification,
|
|
38
|
+
removeNotification,
|
|
39
|
+
notifyError,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -7,6 +7,7 @@ export default defineConfig({
|
|
|
7
7
|
resolve: {
|
|
8
8
|
alias: {
|
|
9
9
|
'@src': fileURLToPath(new URL('./src', import.meta.url)),
|
|
10
|
+
'@shared': fileURLToPath(new URL('./shared', import.meta.url)),
|
|
10
11
|
'@vasp-framework/client': fileURLToPath(new URL('./src/vasp/client', import.meta.url)),
|
|
11
12
|
},
|
|
12
13
|
},
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
error: {
|
|
6
|
+
type: Object,
|
|
7
|
+
required: true,
|
|
8
|
+
},
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const messageText = computed(() => props.error.statusMessage || props.error.message || 'Unexpected server error')
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div style="padding:24px; max-width:720px; margin:0 auto;">
|
|
16
|
+
<h1 v-if="props.error.statusCode === 404">Page not found</h1>
|
|
17
|
+
<h1 v-else-if="props.error.statusCode === 401">Authentication required</h1>
|
|
18
|
+
<h1 v-else>Something went wrong</h1>
|
|
19
|
+
|
|
20
|
+
<p>{{ messageText }}</p>
|
|
21
|
+
<NuxtLink to="/">Go back home</NuxtLink>
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
// Runs only on the client after hydration — routes calls to the Elysia backend via ofetch
|
|
3
3
|
import { $fetch } from 'ofetch'
|
|
4
4
|
|
|
5
|
+
function getCsrfToken() {
|
|
6
|
+
const match = document.cookie.match(/(?:^|;\s*)vasp-csrf=([^;]*)/)
|
|
7
|
+
return match ? decodeURIComponent(match[1]) : undefined
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
export default defineNuxtPlugin((nuxtApp) => {
|
|
6
11
|
const config = useRuntimeConfig()
|
|
7
12
|
const baseURL = config.public.apiBase
|
|
@@ -11,7 +16,12 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|
|
11
16
|
return $fetch(`/queries/${name}`, { baseURL, method: 'GET', query: args })
|
|
12
17
|
},
|
|
13
18
|
async action(name, args) {
|
|
14
|
-
return $fetch(`/actions/${name}`, {
|
|
19
|
+
return $fetch(`/actions/${name}`, {
|
|
20
|
+
baseURL,
|
|
21
|
+
method: 'POST',
|
|
22
|
+
body: args,
|
|
23
|
+
headers: { 'x-csrf-token': getCsrfToken() ?? '' },
|
|
24
|
+
})
|
|
15
25
|
},
|
|
16
26
|
})
|
|
17
27
|
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
error: {
|
|
6
|
+
statusCode?: number
|
|
7
|
+
statusMessage?: string
|
|
8
|
+
message?: string
|
|
9
|
+
}
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
const messageText = computed(() => {
|
|
13
|
+
return props.error.statusMessage ?? props.error.message ?? 'Unexpected server error'
|
|
14
|
+
})
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<div style="padding:24px; max-width:720px; margin:0 auto;">
|
|
19
|
+
<h1 v-if="props.error.statusCode === 404">Page not found</h1>
|
|
20
|
+
<h1 v-else-if="props.error.statusCode === 401">Authentication required</h1>
|
|
21
|
+
<h1 v-else>Something went wrong</h1>
|
|
22
|
+
|
|
23
|
+
<p>{{ messageText }}</p>
|
|
24
|
+
<NuxtLink to="/">Go back home</NuxtLink>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
// Runs only on the client after hydration — routes calls to the Elysia backend via ofetch
|
|
3
3
|
import { $fetch } from 'ofetch'
|
|
4
4
|
|
|
5
|
+
function getCsrfToken(): string | undefined {
|
|
6
|
+
const match = document.cookie.match(/(?:^|;\s*)vasp-csrf=([^;]*)/)
|
|
7
|
+
return match ? decodeURIComponent(match[1]) : undefined
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
export default defineNuxtPlugin((nuxtApp) => {
|
|
6
11
|
const config = useRuntimeConfig()
|
|
7
12
|
const baseURL = config.public.apiBase as string
|
|
@@ -11,7 +16,12 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|
|
11
16
|
return $fetch<T>(`/queries/${name}`, { baseURL, method: 'GET', query: args as Record<string, unknown> })
|
|
12
17
|
},
|
|
13
18
|
async action<T = unknown>(name: string, args?: unknown): Promise<T> {
|
|
14
|
-
return $fetch<T>(`/actions/${name}`, {
|
|
19
|
+
return $fetch<T>(`/actions/${name}`, {
|
|
20
|
+
baseURL,
|
|
21
|
+
method: 'POST',
|
|
22
|
+
body: args,
|
|
23
|
+
headers: { 'x-csrf-token': getCsrfToken() ?? '' },
|
|
24
|
+
})
|
|
15
25
|
},
|
|
16
26
|
})
|
|
17
27
|
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
app RecipeApp {
|
|
2
|
+
title: "Recipe App"
|
|
3
|
+
db: Drizzle
|
|
4
|
+
ssr: false
|
|
5
|
+
typescript: false
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
auth RecipeAuth {
|
|
9
|
+
userEntity: User
|
|
10
|
+
methods: [usernameAndPassword]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
entity Recipe {
|
|
14
|
+
id: Int @id
|
|
15
|
+
title: String
|
|
16
|
+
description: String
|
|
17
|
+
ingredients: String
|
|
18
|
+
instructions: String
|
|
19
|
+
author: String
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
route HomeRoute {
|
|
23
|
+
path: "/"
|
|
24
|
+
to: HomePage
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
route RecipesRoute {
|
|
28
|
+
path: "/recipes"
|
|
29
|
+
to: RecipesPage
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
route AddRecipeRoute {
|
|
33
|
+
path: "/recipes/new"
|
|
34
|
+
to: AddRecipePage
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
page HomePage {
|
|
38
|
+
component: import HomePage from "@src/pages/HomePage.vue"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
page RecipesPage {
|
|
42
|
+
component: import RecipesPage from "@src/pages/RecipesPage.vue"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
page AddRecipePage {
|
|
46
|
+
component: import AddRecipePage from "@src/pages/AddRecipePage.vue"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
query getRecipes {
|
|
50
|
+
fn: import { getRecipes } from "@src/queries.js"
|
|
51
|
+
entities: [Recipe]
|
|
52
|
+
auth: true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
action createRecipe {
|
|
56
|
+
fn: import { createRecipe } from "@src/actions.js"
|
|
57
|
+
entities: [Recipe]
|
|
58
|
+
auth: true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
action deleteRecipe {
|
|
62
|
+
fn: import { deleteRecipe } from "@src/actions.js"
|
|
63
|
+
entities: [Recipe]
|
|
64
|
+
auth: true
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
crud Recipe {
|
|
68
|
+
entity: Recipe
|
|
69
|
+
operations: [list, create, update, delete]
|
|
70
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
app TodoAuthSsrApp {
|
|
2
|
+
title: "Todo App"
|
|
3
|
+
db: Drizzle
|
|
4
|
+
ssr: true
|
|
5
|
+
typescript: false
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
auth TodoAuth {
|
|
9
|
+
userEntity: User
|
|
10
|
+
methods: [usernameAndPassword]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
entity User {
|
|
14
|
+
id: Int @id
|
|
15
|
+
username: String @unique
|
|
16
|
+
email: String @unique
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
entity Todo {
|
|
20
|
+
id: Int @id
|
|
21
|
+
title: String
|
|
22
|
+
done: Boolean
|
|
23
|
+
createdAt: DateTime @default(now)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
route HomeRoute {
|
|
27
|
+
path: "/"
|
|
28
|
+
to: HomePage
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
route TodoRoute {
|
|
32
|
+
path: "/todos"
|
|
33
|
+
to: TodoPage
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
page HomePage {
|
|
37
|
+
component: import Home from "@src/pages/Home.vue"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
page TodoPage {
|
|
41
|
+
component: import TodoList from "@src/pages/TodoList.vue"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
query getTodos {
|
|
45
|
+
fn: import { getTodos } from "@src/queries.js"
|
|
46
|
+
entities: [Todo]
|
|
47
|
+
auth: true
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
action createTodo {
|
|
51
|
+
fn: import { createTodo } from "@src/actions.js"
|
|
52
|
+
entities: [Todo]
|
|
53
|
+
auth: true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
action deleteTodo {
|
|
57
|
+
fn: import { deleteTodo } from "@src/actions.js"
|
|
58
|
+
entities: [Todo]
|
|
59
|
+
auth: true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
crud Todo {
|
|
63
|
+
entity: Todo
|
|
64
|
+
operations: [list, create, update, delete]
|
|
65
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
app TodoApp {
|
|
2
|
+
title: "Todo App"
|
|
3
|
+
db: Drizzle
|
|
4
|
+
ssr: false
|
|
5
|
+
typescript: false
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
entity Todo {
|
|
9
|
+
id: Int @id
|
|
10
|
+
title: String
|
|
11
|
+
done: Boolean
|
|
12
|
+
createdAt: DateTime @default(now)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
route HomeRoute {
|
|
16
|
+
path: "/"
|
|
17
|
+
to: HomePage
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
page HomePage {
|
|
21
|
+
component: import Home from "@src/pages/Home.vue"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
query getTodos {
|
|
25
|
+
fn: import { getTodos } from "@src/queries.js"
|
|
26
|
+
entities: [Todo]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
action createTodo {
|
|
30
|
+
fn: import { createTodo } from "@src/actions.js"
|
|
31
|
+
entities: [Todo]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
action deleteTodo {
|
|
35
|
+
fn: import { deleteTodo } from "@src/actions.js"
|
|
36
|
+
entities: [Todo]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
crud Todo {
|
|
40
|
+
entity: Todo
|
|
41
|
+
operations: [list, create, update, delete]
|
|
42
|
+
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="auth-form">
|
|
3
|
-
<h2>Login</h2>
|
|
4
|
-
<form @submit.prevent="handleLogin">
|
|
5
|
-
<input v-model="username" type="text" placeholder="Username" required />
|
|
6
|
-
<input v-model="password" type="password" placeholder="Password" required />
|
|
7
|
-
<button type="submit" :disabled="loading">
|
|
8
|
-
\{{ loading ? 'Logging in...' : 'Login' }}
|
|
9
|
-
</button>
|
|
10
|
-
<p v-if="error" class="error">\{{ error }}</p>
|
|
11
|
-
</form>
|
|
12
|
-
<p>Don't have an account? <RouterLink to="/register">Register</RouterLink></p>
|
|
13
|
-
{{#if (includes authMethods "google")}}
|
|
14
|
-
<a href="/auth/google" class="oauth-btn">Continue with Google</a>
|
|
15
|
-
{{/if}}
|
|
16
|
-
{{#if (includes authMethods "github")}}
|
|
17
|
-
<a href="/auth/github" class="oauth-btn">Continue with GitHub</a>
|
|
18
|
-
{{/if}}
|
|
19
|
-
</div>
|
|
20
|
-
</template>
|
|
21
|
-
|
|
22
|
-
<script setup>
|
|
23
|
-
import { ref } from 'vue'
|
|
24
|
-
import { useRouter } from 'vue-router'
|
|
25
|
-
import { useAuth } from '../vasp/auth.js'
|
|
26
|
-
|
|
27
|
-
const router = useRouter()
|
|
28
|
-
const { login } = useAuth()
|
|
29
|
-
const username = ref('')
|
|
30
|
-
const password = ref('')
|
|
31
|
-
const loading = ref(false)
|
|
32
|
-
const error = ref('')
|
|
33
|
-
|
|
34
|
-
async function handleLogin() {
|
|
35
|
-
loading.value = true
|
|
36
|
-
error.value = ''
|
|
37
|
-
try {
|
|
38
|
-
await login(username.value, password.value)
|
|
39
|
-
router.push('/')
|
|
40
|
-
} catch (err) {
|
|
41
|
-
error.value = err?.data?.error || err?.message || 'Login failed'
|
|
42
|
-
} finally {
|
|
43
|
-
loading.value = false
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
</script>
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="auth-form">
|
|
3
|
-
<h2>Create Account</h2>
|
|
4
|
-
<form @submit.prevent="handleRegister">
|
|
5
|
-
<input v-model="username" type="text" placeholder="Username" required />
|
|
6
|
-
<input v-model="email" type="email" placeholder="Email (optional)" />
|
|
7
|
-
<input v-model="password" type="password" placeholder="Password (min 8 chars)" required />
|
|
8
|
-
<button type="submit" :disabled="loading">
|
|
9
|
-
\{{ loading ? 'Creating account...' : 'Register' }}
|
|
10
|
-
</button>
|
|
11
|
-
<p v-if="error" class="error">\{{ error }}</p>
|
|
12
|
-
</form>
|
|
13
|
-
<p>Already have an account? <RouterLink to="/login">Login</RouterLink></p>
|
|
14
|
-
</div>
|
|
15
|
-
</template>
|
|
16
|
-
|
|
17
|
-
<script setup>
|
|
18
|
-
import { ref } from 'vue'
|
|
19
|
-
import { useRouter } from 'vue-router'
|
|
20
|
-
import { useAuth } from '../vasp/auth.js'
|
|
21
|
-
|
|
22
|
-
const router = useRouter()
|
|
23
|
-
const { register } = useAuth()
|
|
24
|
-
const username = ref('')
|
|
25
|
-
const email = ref('')
|
|
26
|
-
const password = ref('')
|
|
27
|
-
const loading = ref(false)
|
|
28
|
-
const error = ref('')
|
|
29
|
-
|
|
30
|
-
async function handleRegister() {
|
|
31
|
-
loading.value = true
|
|
32
|
-
error.value = ''
|
|
33
|
-
try {
|
|
34
|
-
await register(username.value, password.value, email.value || undefined)
|
|
35
|
-
router.push('/')
|
|
36
|
-
} catch (err) {
|
|
37
|
-
error.value = err?.data?.error || err?.message || 'Registration failed'
|
|
38
|
-
} finally {
|
|
39
|
-
loading.value = false
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
</script>
|