vasp-cli 0.1.2 → 0.1.4
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 +23 -19
- package/package.json +3 -5
- package/templates/shared/.env.example.hbs +14 -0
- package/templates/shared/.gitignore.hbs +8 -0
- package/templates/shared/auth/client/Login.vue.hbs +46 -0
- package/templates/shared/auth/client/Register.vue.hbs +42 -0
- package/templates/shared/auth/server/index.hbs +51 -0
- package/templates/shared/auth/server/middleware.hbs +33 -0
- package/templates/shared/auth/server/providers/github.hbs +48 -0
- package/templates/shared/auth/server/providers/google.hbs +53 -0
- package/templates/shared/auth/server/providers/usernameAndPassword.hbs +69 -0
- package/templates/shared/bunfig.toml.hbs +2 -0
- package/templates/shared/drizzle/schema.hbs +37 -0
- package/templates/shared/jobs/_job.hbs +24 -0
- package/templates/shared/jobs/boss.hbs +15 -0
- package/templates/shared/package.json.hbs +32 -0
- package/templates/shared/server/db/client.hbs +12 -0
- package/templates/shared/server/index.hbs +52 -0
- package/templates/shared/server/routes/actions/_action.hbs +20 -0
- package/templates/shared/server/routes/crud/_crud.hbs +42 -0
- package/templates/shared/server/routes/jobs/_schedule.hbs +12 -0
- package/templates/shared/server/routes/queries/_query.hbs +20 -0
- package/templates/shared/server/routes/realtime/_channel.hbs +30 -0
- package/templates/shared/server/routes/realtime/index.hbs +9 -0
- package/templates/shared/tsconfig.json.hbs +21 -0
- package/templates/spa/js/index.html.hbs +12 -0
- package/templates/spa/js/src/App.vue.hbs +3 -0
- package/templates/spa/js/src/main.js.hbs +9 -0
- package/templates/spa/js/src/router/index.js.hbs +41 -0
- package/templates/spa/js/src/vasp/auth.js.hbs +45 -0
- package/templates/spa/js/src/vasp/client/actions.js.hbs +15 -0
- package/templates/spa/js/src/vasp/client/crud.js.hbs +30 -0
- package/templates/spa/js/src/vasp/client/index.js.hbs +16 -0
- package/templates/spa/js/src/vasp/client/queries.js.hbs +15 -0
- package/templates/spa/js/src/vasp/client/realtime.js.hbs +51 -0
- package/templates/spa/js/src/vasp/plugin.js.hbs +11 -0
- package/templates/spa/js/vite.config.js.hbs +26 -0
- package/templates/spa/ts/index.html.hbs +12 -0
- package/templates/spa/ts/src/App.vue.hbs +3 -0
- package/templates/spa/ts/src/main.ts.hbs +9 -0
- package/templates/spa/ts/src/router/index.ts.hbs +41 -0
- package/templates/spa/ts/src/vasp/auth.ts.hbs +53 -0
- package/templates/spa/ts/src/vasp/client/actions.ts.hbs +19 -0
- package/templates/spa/ts/src/vasp/client/crud.ts.hbs +37 -0
- package/templates/spa/ts/src/vasp/client/index.ts.hbs +17 -0
- package/templates/spa/ts/src/vasp/client/queries.ts.hbs +19 -0
- package/templates/spa/ts/src/vasp/client/realtime.ts.hbs +56 -0
- package/templates/spa/ts/src/vasp/client/types.ts.hbs +33 -0
- package/templates/spa/ts/src/vasp/plugin.ts.hbs +12 -0
- package/templates/spa/ts/vite.config.ts.hbs +26 -0
- package/templates/ssr/js/_page.vue.hbs +10 -0
- package/templates/ssr/js/app.vue.hbs +3 -0
- package/templates/ssr/js/composables/useAuth.js.hbs +52 -0
- package/templates/ssr/js/composables/useVasp.js.hbs +6 -0
- package/templates/ssr/js/middleware/auth.js.hbs +8 -0
- package/templates/ssr/js/nuxt.config.js.hbs +15 -0
- package/templates/ssr/js/plugins/vasp.client.js.hbs +17 -0
- package/templates/ssr/js/plugins/vasp.server.js.hbs +33 -0
- package/templates/ssr/ts/_page.vue.hbs +10 -0
- package/templates/ssr/ts/app.vue.hbs +3 -0
- package/templates/ssr/ts/composables/useAuth.ts.hbs +56 -0
- package/templates/ssr/ts/composables/useVasp.ts.hbs +10 -0
- package/templates/ssr/ts/middleware/auth.ts.hbs +8 -0
- package/templates/ssr/ts/nuxt.config.ts.hbs +19 -0
- package/templates/ssr/ts/plugins/vasp.client.ts.hbs +17 -0
- package/templates/ssr/ts/plugins/vasp.server.ts.hbs +33 -0
- package/templates/templates/shared/.env.example.hbs +14 -0
- package/templates/templates/shared/.gitignore.hbs +8 -0
- package/templates/templates/shared/auth/client/Login.vue.hbs +46 -0
- package/templates/templates/shared/auth/client/Register.vue.hbs +42 -0
- package/templates/templates/shared/auth/server/index.hbs +51 -0
- package/templates/templates/shared/auth/server/middleware.hbs +33 -0
- package/templates/templates/shared/auth/server/providers/github.hbs +48 -0
- package/templates/templates/shared/auth/server/providers/google.hbs +53 -0
- package/templates/templates/shared/auth/server/providers/usernameAndPassword.hbs +69 -0
- package/templates/templates/shared/bunfig.toml.hbs +2 -0
- package/templates/templates/shared/drizzle/schema.hbs +37 -0
- package/templates/templates/shared/jobs/_job.hbs +24 -0
- package/templates/templates/shared/jobs/boss.hbs +15 -0
- package/templates/templates/shared/package.json.hbs +32 -0
- package/templates/templates/shared/server/db/client.hbs +12 -0
- package/templates/templates/shared/server/index.hbs +52 -0
- package/templates/templates/shared/server/routes/actions/_action.hbs +20 -0
- package/templates/templates/shared/server/routes/crud/_crud.hbs +42 -0
- package/templates/templates/shared/server/routes/jobs/_schedule.hbs +12 -0
- package/templates/templates/shared/server/routes/queries/_query.hbs +20 -0
- package/templates/templates/shared/server/routes/realtime/_channel.hbs +30 -0
- package/templates/templates/shared/server/routes/realtime/index.hbs +9 -0
- package/templates/templates/shared/tsconfig.json.hbs +21 -0
- package/templates/templates/spa/js/index.html.hbs +12 -0
- package/templates/templates/spa/js/src/App.vue.hbs +3 -0
- package/templates/templates/spa/js/src/main.js.hbs +9 -0
- package/templates/templates/spa/js/src/router/index.js.hbs +41 -0
- package/templates/templates/spa/js/src/vasp/auth.js.hbs +45 -0
- package/templates/templates/spa/js/src/vasp/client/actions.js.hbs +15 -0
- package/templates/templates/spa/js/src/vasp/client/crud.js.hbs +30 -0
- package/templates/templates/spa/js/src/vasp/client/index.js.hbs +16 -0
- package/templates/templates/spa/js/src/vasp/client/queries.js.hbs +15 -0
- package/templates/templates/spa/js/src/vasp/client/realtime.js.hbs +51 -0
- package/templates/templates/spa/js/src/vasp/plugin.js.hbs +11 -0
- package/templates/templates/spa/js/vite.config.js.hbs +26 -0
- package/templates/templates/spa/ts/index.html.hbs +12 -0
- package/templates/templates/spa/ts/src/App.vue.hbs +3 -0
- package/templates/templates/spa/ts/src/main.ts.hbs +9 -0
- package/templates/templates/spa/ts/src/router/index.ts.hbs +41 -0
- package/templates/templates/spa/ts/src/vasp/auth.ts.hbs +53 -0
- package/templates/templates/spa/ts/src/vasp/client/actions.ts.hbs +19 -0
- package/templates/templates/spa/ts/src/vasp/client/crud.ts.hbs +37 -0
- package/templates/templates/spa/ts/src/vasp/client/index.ts.hbs +17 -0
- package/templates/templates/spa/ts/src/vasp/client/queries.ts.hbs +19 -0
- package/templates/templates/spa/ts/src/vasp/client/realtime.ts.hbs +56 -0
- package/templates/templates/spa/ts/src/vasp/client/types.ts.hbs +33 -0
- package/templates/templates/spa/ts/src/vasp/plugin.ts.hbs +12 -0
- package/templates/templates/spa/ts/vite.config.ts.hbs +26 -0
- package/templates/templates/ssr/js/_page.vue.hbs +10 -0
- package/templates/templates/ssr/js/app.vue.hbs +3 -0
- package/templates/templates/ssr/js/composables/useAuth.js.hbs +52 -0
- package/templates/templates/ssr/js/composables/useVasp.js.hbs +6 -0
- package/templates/templates/ssr/js/middleware/auth.js.hbs +8 -0
- package/templates/templates/ssr/js/nuxt.config.js.hbs +15 -0
- package/templates/templates/ssr/js/plugins/vasp.client.js.hbs +17 -0
- package/templates/templates/ssr/js/plugins/vasp.server.js.hbs +33 -0
- package/templates/templates/ssr/ts/_page.vue.hbs +10 -0
- package/templates/templates/ssr/ts/app.vue.hbs +3 -0
- package/templates/templates/ssr/ts/composables/useAuth.ts.hbs +56 -0
- package/templates/templates/ssr/ts/composables/useVasp.ts.hbs +10 -0
- package/templates/templates/ssr/ts/middleware/auth.ts.hbs +8 -0
- package/templates/templates/ssr/ts/nuxt.config.ts.hbs +19 -0
- package/templates/templates/ssr/ts/plugins/vasp.client.ts.hbs +17 -0
- package/templates/templates/ssr/ts/plugins/vasp.server.ts.hbs +33 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Auto-generated by Vasp — entity types inferred from Drizzle schema
|
|
2
|
+
import type {
|
|
3
|
+
{{#each cruds}}
|
|
4
|
+
{{pascalCase entity}},
|
|
5
|
+
New{{pascalCase entity}},
|
|
6
|
+
{{/each}}
|
|
7
|
+
{{#if hasAuth}}
|
|
8
|
+
User,
|
|
9
|
+
{{/if}}
|
|
10
|
+
} from '../../../../../../drizzle/schema.js'
|
|
11
|
+
|
|
12
|
+
{{#each queries}}
|
|
13
|
+
// Arguments and return type for '{{name}}' — customize as needed
|
|
14
|
+
export type {{pascalCase name}}Args = Record<string, unknown>
|
|
15
|
+
export type {{pascalCase name}}Return = unknown
|
|
16
|
+
|
|
17
|
+
{{/each}}
|
|
18
|
+
{{#each actions}}
|
|
19
|
+
// Arguments and return type for '{{name}}' — customize as needed
|
|
20
|
+
export type {{pascalCase name}}Args = Record<string, unknown>
|
|
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}},
|
|
29
|
+
{{/each}}
|
|
30
|
+
{{#if hasAuth}}
|
|
31
|
+
User,
|
|
32
|
+
{{/if}}
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { App } from 'vue'
|
|
2
|
+
import { createVaspClient } from '@vasp-framework/runtime'
|
|
3
|
+
|
|
4
|
+
export const vaspPlugin = {
|
|
5
|
+
install(app: App) {
|
|
6
|
+
const client = createVaspClient({
|
|
7
|
+
baseURL: import.meta.env.VITE_API_URL || '/api',
|
|
8
|
+
})
|
|
9
|
+
app.provide('$vasp', client)
|
|
10
|
+
app.config.globalProperties.$vasp = client
|
|
11
|
+
},
|
|
12
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import vue from '@vitejs/plugin-vue'
|
|
3
|
+
import { fileURLToPath, URL } from 'node:url'
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [vue()],
|
|
7
|
+
resolve: {
|
|
8
|
+
alias: {
|
|
9
|
+
'@src': fileURLToPath(new URL('./src', import.meta.url)),
|
|
10
|
+
'@vasp-framework/client': fileURLToPath(new URL('./src/vasp/client', import.meta.url)),
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
server: {
|
|
14
|
+
port: {{frontendPort}},
|
|
15
|
+
proxy: {
|
|
16
|
+
'/api': {
|
|
17
|
+
target: 'http://localhost:{{backendPort}}',
|
|
18
|
+
changeOrigin: true,
|
|
19
|
+
},
|
|
20
|
+
'/ws': {
|
|
21
|
+
target: 'ws://localhost:{{backendPort}}',
|
|
22
|
+
ws: true,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Auto-generated by Vasp — do not edit directly
|
|
2
|
+
// SSR-aware auth composable — uses cookies (httpOnly) and Nuxt's useRequestHeaders for server-side access
|
|
3
|
+
import { $fetch } from 'ofetch'
|
|
4
|
+
|
|
5
|
+
const _user = ref(null)
|
|
6
|
+
let _checked = false
|
|
7
|
+
|
|
8
|
+
export function useAuth() {
|
|
9
|
+
const config = useRuntimeConfig()
|
|
10
|
+
const baseURL = config.public.apiBase
|
|
11
|
+
|
|
12
|
+
// On server, forward cookies from the incoming request
|
|
13
|
+
const headers = import.meta.server ? useRequestHeaders(['cookie']) : {}
|
|
14
|
+
|
|
15
|
+
async function checkAuth() {
|
|
16
|
+
if (_checked) return
|
|
17
|
+
_checked = true
|
|
18
|
+
try {
|
|
19
|
+
_user.value = await $fetch(`${baseURL}/auth/me`, { headers, credentials: 'include' })
|
|
20
|
+
} catch {
|
|
21
|
+
_user.value = null
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function login(username, password) {
|
|
26
|
+
_user.value = await $fetch(`${baseURL}/auth/login`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
body: { username, password },
|
|
29
|
+
credentials: 'include',
|
|
30
|
+
})
|
|
31
|
+
_checked = true
|
|
32
|
+
return _user.value
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function register(username, password, email) {
|
|
36
|
+
_user.value = await $fetch(`${baseURL}/auth/register`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
body: { username, password, email },
|
|
39
|
+
credentials: 'include',
|
|
40
|
+
})
|
|
41
|
+
_checked = true
|
|
42
|
+
return _user.value
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function logout() {
|
|
46
|
+
await $fetch(`${baseURL}/auth/logout`, { method: 'POST', credentials: 'include' })
|
|
47
|
+
_user.value = null
|
|
48
|
+
_checked = false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { user: _user, checkAuth, login, register, logout }
|
|
52
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Auto-generated by Vasp — do not edit directly
|
|
2
|
+
export default defineNuxtRouteMiddleware((to) => {
|
|
3
|
+
const { user } = useAuth()
|
|
4
|
+
const publicPaths = ['/login', '/register']
|
|
5
|
+
if (!user.value && !publicPaths.includes(to.path)) {
|
|
6
|
+
return navigateTo('/login')
|
|
7
|
+
}
|
|
8
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Auto-generated by Vasp — do not edit directly
|
|
2
|
+
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
3
|
+
export default defineNuxtConfig({
|
|
4
|
+
compatibilityDate: '2024-11-01',
|
|
5
|
+
devtools: { enabled: true },
|
|
6
|
+
alias: {
|
|
7
|
+
'@src': '~/src',
|
|
8
|
+
},
|
|
9
|
+
runtimeConfig: {
|
|
10
|
+
backendUrl: process.env.BACKEND_URL || 'http://localhost:{{backendPort}}',
|
|
11
|
+
public: {
|
|
12
|
+
apiBase: process.env.API_BASE || 'http://localhost:{{backendPort}}/api',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Auto-generated by Vasp — do not edit directly
|
|
2
|
+
// Runs only on the client after hydration — routes calls to the Elysia backend via ofetch
|
|
3
|
+
import { $fetch } from 'ofetch'
|
|
4
|
+
|
|
5
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
6
|
+
const config = useRuntimeConfig()
|
|
7
|
+
const baseURL = config.public.apiBase
|
|
8
|
+
|
|
9
|
+
nuxtApp.provide('vasp', {
|
|
10
|
+
async query(name, args) {
|
|
11
|
+
return $fetch(`/queries/${name}`, { baseURL, method: 'GET', query: args })
|
|
12
|
+
},
|
|
13
|
+
async action(name, args) {
|
|
14
|
+
return $fetch(`/actions/${name}`, { baseURL, method: 'POST', body: args })
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Auto-generated by Vasp — do not edit directly
|
|
2
|
+
// Runs only on the server during SSR render — calls query/action functions directly (zero HTTP overhead)
|
|
3
|
+
{{#each queries}}
|
|
4
|
+
import { {{importName fn}} } from '{{fn.source}}'
|
|
5
|
+
{{/each}}
|
|
6
|
+
{{#each actions}}
|
|
7
|
+
import { {{importName fn}} } from '{{fn.source}}'
|
|
8
|
+
{{/each}}
|
|
9
|
+
|
|
10
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
11
|
+
const queryFns = {
|
|
12
|
+
{{#each queries}}
|
|
13
|
+
{{camelCase name}}: {{importName fn}},
|
|
14
|
+
{{/each}}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const actionFns = {
|
|
18
|
+
{{#each actions}}
|
|
19
|
+
{{camelCase name}}: {{importName fn}},
|
|
20
|
+
{{/each}}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
nuxtApp.provide('vasp', {
|
|
24
|
+
async query(name, args) {
|
|
25
|
+
if (!queryFns[name]) throw new Error(`[Vasp] Unknown query: ${name}`)
|
|
26
|
+
return queryFns[name](args)
|
|
27
|
+
},
|
|
28
|
+
async action(name, args) {
|
|
29
|
+
if (!actionFns[name]) throw new Error(`[Vasp] Unknown action: ${name}`)
|
|
30
|
+
return actionFns[name](args)
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Auto-generated by Vasp — do not edit directly
|
|
2
|
+
// SSR-aware auth composable — uses cookies (httpOnly) and Nuxt's useRequestHeaders for server-side access
|
|
3
|
+
import { $fetch } from 'ofetch'
|
|
4
|
+
|
|
5
|
+
interface AuthUser {
|
|
6
|
+
id: number
|
|
7
|
+
username: string
|
|
8
|
+
email?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const _user = ref<AuthUser | null>(null)
|
|
12
|
+
let _checked = false
|
|
13
|
+
|
|
14
|
+
export function useAuth() {
|
|
15
|
+
const config = useRuntimeConfig()
|
|
16
|
+
const baseURL = config.public.apiBase as string
|
|
17
|
+
const headers = import.meta.server ? useRequestHeaders(['cookie']) : {}
|
|
18
|
+
|
|
19
|
+
async function checkAuth(): Promise<void> {
|
|
20
|
+
if (_checked) return
|
|
21
|
+
_checked = true
|
|
22
|
+
try {
|
|
23
|
+
_user.value = await $fetch<AuthUser>(`${baseURL}/auth/me`, { headers, credentials: 'include' })
|
|
24
|
+
} catch {
|
|
25
|
+
_user.value = null
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function login(username: string, password: string): Promise<AuthUser> {
|
|
30
|
+
_user.value = await $fetch<AuthUser>(`${baseURL}/auth/login`, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
body: { username, password },
|
|
33
|
+
credentials: 'include',
|
|
34
|
+
})
|
|
35
|
+
_checked = true
|
|
36
|
+
return _user.value!
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function register(username: string, password: string, email?: string): Promise<AuthUser> {
|
|
40
|
+
_user.value = await $fetch<AuthUser>(`${baseURL}/auth/register`, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
body: { username, password, email },
|
|
43
|
+
credentials: 'include',
|
|
44
|
+
})
|
|
45
|
+
_checked = true
|
|
46
|
+
return _user.value!
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function logout(): Promise<void> {
|
|
50
|
+
await $fetch(`${baseURL}/auth/logout`, { method: 'POST', credentials: 'include' })
|
|
51
|
+
_user.value = null
|
|
52
|
+
_checked = false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { user: _user, checkAuth, login, register, logout }
|
|
56
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Auto-generated by Vasp — do not edit directly
|
|
2
|
+
interface VaspClient {
|
|
3
|
+
query<T = unknown>(name: string, args?: unknown): Promise<T>
|
|
4
|
+
action<T = unknown>(name: string, args?: unknown): Promise<T>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const useVasp = (): { $vasp: VaspClient } => {
|
|
8
|
+
const { $vasp } = useNuxtApp()
|
|
9
|
+
return { $vasp: $vasp as VaspClient }
|
|
10
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Auto-generated by Vasp — do not edit directly
|
|
2
|
+
export default defineNuxtRouteMiddleware((to) => {
|
|
3
|
+
const { user } = useAuth()
|
|
4
|
+
const publicPaths = ['/login', '/register']
|
|
5
|
+
if (!user.value && !publicPaths.includes(to.path)) {
|
|
6
|
+
return navigateTo('/login')
|
|
7
|
+
}
|
|
8
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Auto-generated by Vasp — do not edit directly
|
|
2
|
+
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
3
|
+
export default defineNuxtConfig({
|
|
4
|
+
compatibilityDate: '2024-11-01',
|
|
5
|
+
devtools: { enabled: true },
|
|
6
|
+
typescript: {
|
|
7
|
+
strict: true,
|
|
8
|
+
typeCheck: true,
|
|
9
|
+
},
|
|
10
|
+
alias: {
|
|
11
|
+
'@src': '~/src',
|
|
12
|
+
},
|
|
13
|
+
runtimeConfig: {
|
|
14
|
+
backendUrl: process.env.BACKEND_URL || 'http://localhost:{{backendPort}}',
|
|
15
|
+
public: {
|
|
16
|
+
apiBase: process.env.API_BASE || 'http://localhost:{{backendPort}}/api',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Auto-generated by Vasp — do not edit directly
|
|
2
|
+
// Runs only on the client after hydration — routes calls to the Elysia backend via ofetch
|
|
3
|
+
import { $fetch } from 'ofetch'
|
|
4
|
+
|
|
5
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
6
|
+
const config = useRuntimeConfig()
|
|
7
|
+
const baseURL = config.public.apiBase as string
|
|
8
|
+
|
|
9
|
+
nuxtApp.provide('vasp', {
|
|
10
|
+
async query<T = unknown>(name: string, args?: unknown): Promise<T> {
|
|
11
|
+
return $fetch<T>(`/queries/${name}`, { baseURL, method: 'GET', query: args as Record<string, unknown> })
|
|
12
|
+
},
|
|
13
|
+
async action<T = unknown>(name: string, args?: unknown): Promise<T> {
|
|
14
|
+
return $fetch<T>(`/actions/${name}`, { baseURL, method: 'POST', body: args })
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Auto-generated by Vasp — do not edit directly
|
|
2
|
+
// Runs only on the server during SSR render — calls query/action functions directly (zero HTTP overhead)
|
|
3
|
+
{{#each queries}}
|
|
4
|
+
import { {{importName fn}} } from '{{fn.source}}'
|
|
5
|
+
{{/each}}
|
|
6
|
+
{{#each actions}}
|
|
7
|
+
import { {{importName fn}} } from '{{fn.source}}'
|
|
8
|
+
{{/each}}
|
|
9
|
+
|
|
10
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
11
|
+
const queryFns: Record<string, (args?: unknown) => Promise<unknown>> = {
|
|
12
|
+
{{#each queries}}
|
|
13
|
+
{{camelCase name}}: {{importName fn}} as (args?: unknown) => Promise<unknown>,
|
|
14
|
+
{{/each}}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const actionFns: Record<string, (args?: unknown) => Promise<unknown>> = {
|
|
18
|
+
{{#each actions}}
|
|
19
|
+
{{camelCase name}}: {{importName fn}} as (args?: unknown) => Promise<unknown>,
|
|
20
|
+
{{/each}}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
nuxtApp.provide('vasp', {
|
|
24
|
+
async query<T = unknown>(name: string, args?: unknown): Promise<T> {
|
|
25
|
+
if (!queryFns[name]) throw new Error(`[Vasp] Unknown query: ${name}`)
|
|
26
|
+
return queryFns[name](args) as Promise<T>
|
|
27
|
+
},
|
|
28
|
+
async action<T = unknown>(name: string, args?: unknown): Promise<T> {
|
|
29
|
+
if (!actionFns[name]) throw new Error(`[Vasp] Unknown action: ${name}`)
|
|
30
|
+
return actionFns[name](args) as Promise<T>
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
DATABASE_URL=postgres://user:password@localhost:5432/{{kebabCase appName}}
|
|
2
|
+
PORT={{backendPort}}
|
|
3
|
+
VITE_API_URL=http://localhost:{{backendPort}}/api
|
|
4
|
+
{{#if hasAuth}}
|
|
5
|
+
JWT_SECRET=change-me-in-production
|
|
6
|
+
{{#if (includes authMethods "google")}}
|
|
7
|
+
GOOGLE_CLIENT_ID=
|
|
8
|
+
GOOGLE_CLIENT_SECRET=
|
|
9
|
+
{{/if}}
|
|
10
|
+
{{#if (includes authMethods "github")}}
|
|
11
|
+
GITHUB_CLIENT_ID=
|
|
12
|
+
GITHUB_CLIENT_SECRET=
|
|
13
|
+
{{/if}}
|
|
14
|
+
{{/if}}
|
|
@@ -0,0 +1,46 @@
|
|
|
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 || 'Login failed'
|
|
42
|
+
} finally {
|
|
43
|
+
loading.value = false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
@@ -0,0 +1,42 @@
|
|
|
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 || 'Registration failed'
|
|
38
|
+
} finally {
|
|
39
|
+
loading.value = false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Elysia } from 'elysia'
|
|
2
|
+
import { jwt } from '@elysiajs/jwt'
|
|
3
|
+
import { cookie } from '@elysiajs/cookie'
|
|
4
|
+
import { db } from '../db/client.{{ext}}'
|
|
5
|
+
import { users } from '../../drizzle/schema.{{ext}}'
|
|
6
|
+
import { eq } from 'drizzle-orm'
|
|
7
|
+
{{#if (includes authMethods "usernameAndPassword")}}
|
|
8
|
+
import { usernameAndPasswordRoutes } from './providers/usernameAndPassword.{{ext}}'
|
|
9
|
+
{{/if}}
|
|
10
|
+
{{#if (includes authMethods "google")}}
|
|
11
|
+
import { googleRoutes } from './providers/google.{{ext}}'
|
|
12
|
+
{{/if}}
|
|
13
|
+
{{#if (includes authMethods "github")}}
|
|
14
|
+
import { githubRoutes } from './providers/github.{{ext}}'
|
|
15
|
+
{{/if}}
|
|
16
|
+
|
|
17
|
+
const JWT_SECRET = process.env.JWT_SECRET || 'change-me-in-production'
|
|
18
|
+
|
|
19
|
+
export const authPlugin = new Elysia({ name: 'auth-plugin' })
|
|
20
|
+
.use(jwt({ name: 'jwt', secret: JWT_SECRET }))
|
|
21
|
+
.use(cookie())
|
|
22
|
+
|
|
23
|
+
export const authRoutes = new Elysia({ prefix: '/auth' })
|
|
24
|
+
.use(authPlugin)
|
|
25
|
+
{{#if (includes authMethods "usernameAndPassword")}}
|
|
26
|
+
.use(usernameAndPasswordRoutes)
|
|
27
|
+
{{/if}}
|
|
28
|
+
{{#if (includes authMethods "google")}}
|
|
29
|
+
.use(googleRoutes)
|
|
30
|
+
{{/if}}
|
|
31
|
+
{{#if (includes authMethods "github")}}
|
|
32
|
+
.use(githubRoutes)
|
|
33
|
+
{{/if}}
|
|
34
|
+
.get('/me', async ({ jwt, cookie: { token }, set }) => {
|
|
35
|
+
const payload = await jwt.verify(token?.value ?? '')
|
|
36
|
+
if (!payload || typeof payload.userId !== 'number') {
|
|
37
|
+
set.status = 401
|
|
38
|
+
return { error: 'Unauthorized' }
|
|
39
|
+
}
|
|
40
|
+
const [user] = await db.select().from(users).where(eq(users.id, payload.userId)).limit(1)
|
|
41
|
+
if (!user) {
|
|
42
|
+
set.status = 401
|
|
43
|
+
return { error: 'User not found' }
|
|
44
|
+
}
|
|
45
|
+
const { passwordHash: _ph, ...safeUser } = user
|
|
46
|
+
return safeUser
|
|
47
|
+
})
|
|
48
|
+
.post('/logout', ({ cookie: { token }, set }) => {
|
|
49
|
+
token?.remove()
|
|
50
|
+
return { ok: true }
|
|
51
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Elysia } from 'elysia'
|
|
2
|
+
import { jwt } from '@elysiajs/jwt'
|
|
3
|
+
import { cookie } from '@elysiajs/cookie'
|
|
4
|
+
import { db } from '../db/client.{{ext}}'
|
|
5
|
+
import { users } from '../../drizzle/schema.{{ext}}'
|
|
6
|
+
import { eq } from 'drizzle-orm'
|
|
7
|
+
|
|
8
|
+
const JWT_SECRET = process.env.JWT_SECRET || 'change-me-in-production'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* requireAuth — Elysia plugin that verifies the JWT cookie and injects `user` into the context.
|
|
12
|
+
* Use on any route that requires authentication.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* new Elysia().use(requireAuth).get('/protected', ({ user }) => user)
|
|
16
|
+
*/
|
|
17
|
+
export const requireAuth = new Elysia({ name: 'require-auth' })
|
|
18
|
+
.use(jwt({ name: 'jwt', secret: JWT_SECRET }))
|
|
19
|
+
.use(cookie())
|
|
20
|
+
.derive(async ({ jwt, cookie: { token }, set }) => {
|
|
21
|
+
const payload = await jwt.verify(token?.value ?? '')
|
|
22
|
+
if (!payload || typeof payload.userId !== 'number') {
|
|
23
|
+
set.status = 401
|
|
24
|
+
throw new Error('Unauthorized')
|
|
25
|
+
}
|
|
26
|
+
const [user] = await db.select().from(users).where(eq(users.id, payload.userId)).limit(1)
|
|
27
|
+
if (!user) {
|
|
28
|
+
set.status = 401
|
|
29
|
+
throw new Error('User not found')
|
|
30
|
+
}
|
|
31
|
+
const { passwordHash: _ph, ...safeUser } = user
|
|
32
|
+
return { user: safeUser }
|
|
33
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Elysia } from 'elysia'
|
|
2
|
+
import { db } from '../../db/client.{{ext}}'
|
|
3
|
+
import { users } from '../../../drizzle/schema.{{ext}}'
|
|
4
|
+
import { eq } from 'drizzle-orm'
|
|
5
|
+
import { authPlugin } from '../index.{{ext}}'
|
|
6
|
+
|
|
7
|
+
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || ''
|
|
8
|
+
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET || ''
|
|
9
|
+
const REDIRECT_URI = process.env.GITHUB_REDIRECT_URI || 'http://localhost:{{backendPort}}/auth/github/callback'
|
|
10
|
+
|
|
11
|
+
export const githubRoutes = new Elysia()
|
|
12
|
+
.use(authPlugin)
|
|
13
|
+
.get('/github', ({ set }) => {
|
|
14
|
+
const params = new URLSearchParams({
|
|
15
|
+
client_id: GITHUB_CLIENT_ID,
|
|
16
|
+
redirect_uri: REDIRECT_URI,
|
|
17
|
+
scope: 'read:user user:email',
|
|
18
|
+
})
|
|
19
|
+
set.redirect = `https://github.com/login/oauth/authorize?${params}`
|
|
20
|
+
})
|
|
21
|
+
.get('/github/callback', async ({ query, jwt, cookie: { token }, set }) => {
|
|
22
|
+
const { code } = query
|
|
23
|
+
if (!code) { set.status = 400; return { error: 'Missing code' } }
|
|
24
|
+
|
|
25
|
+
const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
28
|
+
body: JSON.stringify({ client_id: GITHUB_CLIENT_ID, client_secret: GITHUB_CLIENT_SECRET, code }),
|
|
29
|
+
})
|
|
30
|
+
const { access_token } = await tokenRes.json()
|
|
31
|
+
|
|
32
|
+
const userRes = await fetch('https://api.github.com/user', {
|
|
33
|
+
headers: { Authorization: `Bearer ${access_token}`, Accept: 'application/json' },
|
|
34
|
+
})
|
|
35
|
+
const ghUser = await userRes.json()
|
|
36
|
+
const githubId = String(ghUser.id)
|
|
37
|
+
const email = ghUser.email || `${ghUser.login}@github.local`
|
|
38
|
+
|
|
39
|
+
let [user] = await db.select().from(users).where(eq(users.githubId, githubId)).limit(1)
|
|
40
|
+
if (!user) {
|
|
41
|
+
;[user] = await db.insert(users).values({ username: ghUser.login, email, githubId }).returning()
|
|
42
|
+
}
|
|
43
|
+
if (!user) { set.status = 500; return { error: 'Failed to create user' } }
|
|
44
|
+
|
|
45
|
+
const tokenValue = await jwt.sign({ userId: user.id })
|
|
46
|
+
token.set({ value: tokenValue, httpOnly: true, sameSite: 'lax', path: '/' })
|
|
47
|
+
set.redirect = '/'
|
|
48
|
+
})
|