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.
Files changed (130) hide show
  1. package/dist/vasp +23 -19
  2. package/package.json +3 -5
  3. package/templates/shared/.env.example.hbs +14 -0
  4. package/templates/shared/.gitignore.hbs +8 -0
  5. package/templates/shared/auth/client/Login.vue.hbs +46 -0
  6. package/templates/shared/auth/client/Register.vue.hbs +42 -0
  7. package/templates/shared/auth/server/index.hbs +51 -0
  8. package/templates/shared/auth/server/middleware.hbs +33 -0
  9. package/templates/shared/auth/server/providers/github.hbs +48 -0
  10. package/templates/shared/auth/server/providers/google.hbs +53 -0
  11. package/templates/shared/auth/server/providers/usernameAndPassword.hbs +69 -0
  12. package/templates/shared/bunfig.toml.hbs +2 -0
  13. package/templates/shared/drizzle/schema.hbs +37 -0
  14. package/templates/shared/jobs/_job.hbs +24 -0
  15. package/templates/shared/jobs/boss.hbs +15 -0
  16. package/templates/shared/package.json.hbs +32 -0
  17. package/templates/shared/server/db/client.hbs +12 -0
  18. package/templates/shared/server/index.hbs +52 -0
  19. package/templates/shared/server/routes/actions/_action.hbs +20 -0
  20. package/templates/shared/server/routes/crud/_crud.hbs +42 -0
  21. package/templates/shared/server/routes/jobs/_schedule.hbs +12 -0
  22. package/templates/shared/server/routes/queries/_query.hbs +20 -0
  23. package/templates/shared/server/routes/realtime/_channel.hbs +30 -0
  24. package/templates/shared/server/routes/realtime/index.hbs +9 -0
  25. package/templates/shared/tsconfig.json.hbs +21 -0
  26. package/templates/spa/js/index.html.hbs +12 -0
  27. package/templates/spa/js/src/App.vue.hbs +3 -0
  28. package/templates/spa/js/src/main.js.hbs +9 -0
  29. package/templates/spa/js/src/router/index.js.hbs +41 -0
  30. package/templates/spa/js/src/vasp/auth.js.hbs +45 -0
  31. package/templates/spa/js/src/vasp/client/actions.js.hbs +15 -0
  32. package/templates/spa/js/src/vasp/client/crud.js.hbs +30 -0
  33. package/templates/spa/js/src/vasp/client/index.js.hbs +16 -0
  34. package/templates/spa/js/src/vasp/client/queries.js.hbs +15 -0
  35. package/templates/spa/js/src/vasp/client/realtime.js.hbs +51 -0
  36. package/templates/spa/js/src/vasp/plugin.js.hbs +11 -0
  37. package/templates/spa/js/vite.config.js.hbs +26 -0
  38. package/templates/spa/ts/index.html.hbs +12 -0
  39. package/templates/spa/ts/src/App.vue.hbs +3 -0
  40. package/templates/spa/ts/src/main.ts.hbs +9 -0
  41. package/templates/spa/ts/src/router/index.ts.hbs +41 -0
  42. package/templates/spa/ts/src/vasp/auth.ts.hbs +53 -0
  43. package/templates/spa/ts/src/vasp/client/actions.ts.hbs +19 -0
  44. package/templates/spa/ts/src/vasp/client/crud.ts.hbs +37 -0
  45. package/templates/spa/ts/src/vasp/client/index.ts.hbs +17 -0
  46. package/templates/spa/ts/src/vasp/client/queries.ts.hbs +19 -0
  47. package/templates/spa/ts/src/vasp/client/realtime.ts.hbs +56 -0
  48. package/templates/spa/ts/src/vasp/client/types.ts.hbs +33 -0
  49. package/templates/spa/ts/src/vasp/plugin.ts.hbs +12 -0
  50. package/templates/spa/ts/vite.config.ts.hbs +26 -0
  51. package/templates/ssr/js/_page.vue.hbs +10 -0
  52. package/templates/ssr/js/app.vue.hbs +3 -0
  53. package/templates/ssr/js/composables/useAuth.js.hbs +52 -0
  54. package/templates/ssr/js/composables/useVasp.js.hbs +6 -0
  55. package/templates/ssr/js/middleware/auth.js.hbs +8 -0
  56. package/templates/ssr/js/nuxt.config.js.hbs +15 -0
  57. package/templates/ssr/js/plugins/vasp.client.js.hbs +17 -0
  58. package/templates/ssr/js/plugins/vasp.server.js.hbs +33 -0
  59. package/templates/ssr/ts/_page.vue.hbs +10 -0
  60. package/templates/ssr/ts/app.vue.hbs +3 -0
  61. package/templates/ssr/ts/composables/useAuth.ts.hbs +56 -0
  62. package/templates/ssr/ts/composables/useVasp.ts.hbs +10 -0
  63. package/templates/ssr/ts/middleware/auth.ts.hbs +8 -0
  64. package/templates/ssr/ts/nuxt.config.ts.hbs +19 -0
  65. package/templates/ssr/ts/plugins/vasp.client.ts.hbs +17 -0
  66. package/templates/ssr/ts/plugins/vasp.server.ts.hbs +33 -0
  67. package/templates/templates/shared/.env.example.hbs +14 -0
  68. package/templates/templates/shared/.gitignore.hbs +8 -0
  69. package/templates/templates/shared/auth/client/Login.vue.hbs +46 -0
  70. package/templates/templates/shared/auth/client/Register.vue.hbs +42 -0
  71. package/templates/templates/shared/auth/server/index.hbs +51 -0
  72. package/templates/templates/shared/auth/server/middleware.hbs +33 -0
  73. package/templates/templates/shared/auth/server/providers/github.hbs +48 -0
  74. package/templates/templates/shared/auth/server/providers/google.hbs +53 -0
  75. package/templates/templates/shared/auth/server/providers/usernameAndPassword.hbs +69 -0
  76. package/templates/templates/shared/bunfig.toml.hbs +2 -0
  77. package/templates/templates/shared/drizzle/schema.hbs +37 -0
  78. package/templates/templates/shared/jobs/_job.hbs +24 -0
  79. package/templates/templates/shared/jobs/boss.hbs +15 -0
  80. package/templates/templates/shared/package.json.hbs +32 -0
  81. package/templates/templates/shared/server/db/client.hbs +12 -0
  82. package/templates/templates/shared/server/index.hbs +52 -0
  83. package/templates/templates/shared/server/routes/actions/_action.hbs +20 -0
  84. package/templates/templates/shared/server/routes/crud/_crud.hbs +42 -0
  85. package/templates/templates/shared/server/routes/jobs/_schedule.hbs +12 -0
  86. package/templates/templates/shared/server/routes/queries/_query.hbs +20 -0
  87. package/templates/templates/shared/server/routes/realtime/_channel.hbs +30 -0
  88. package/templates/templates/shared/server/routes/realtime/index.hbs +9 -0
  89. package/templates/templates/shared/tsconfig.json.hbs +21 -0
  90. package/templates/templates/spa/js/index.html.hbs +12 -0
  91. package/templates/templates/spa/js/src/App.vue.hbs +3 -0
  92. package/templates/templates/spa/js/src/main.js.hbs +9 -0
  93. package/templates/templates/spa/js/src/router/index.js.hbs +41 -0
  94. package/templates/templates/spa/js/src/vasp/auth.js.hbs +45 -0
  95. package/templates/templates/spa/js/src/vasp/client/actions.js.hbs +15 -0
  96. package/templates/templates/spa/js/src/vasp/client/crud.js.hbs +30 -0
  97. package/templates/templates/spa/js/src/vasp/client/index.js.hbs +16 -0
  98. package/templates/templates/spa/js/src/vasp/client/queries.js.hbs +15 -0
  99. package/templates/templates/spa/js/src/vasp/client/realtime.js.hbs +51 -0
  100. package/templates/templates/spa/js/src/vasp/plugin.js.hbs +11 -0
  101. package/templates/templates/spa/js/vite.config.js.hbs +26 -0
  102. package/templates/templates/spa/ts/index.html.hbs +12 -0
  103. package/templates/templates/spa/ts/src/App.vue.hbs +3 -0
  104. package/templates/templates/spa/ts/src/main.ts.hbs +9 -0
  105. package/templates/templates/spa/ts/src/router/index.ts.hbs +41 -0
  106. package/templates/templates/spa/ts/src/vasp/auth.ts.hbs +53 -0
  107. package/templates/templates/spa/ts/src/vasp/client/actions.ts.hbs +19 -0
  108. package/templates/templates/spa/ts/src/vasp/client/crud.ts.hbs +37 -0
  109. package/templates/templates/spa/ts/src/vasp/client/index.ts.hbs +17 -0
  110. package/templates/templates/spa/ts/src/vasp/client/queries.ts.hbs +19 -0
  111. package/templates/templates/spa/ts/src/vasp/client/realtime.ts.hbs +56 -0
  112. package/templates/templates/spa/ts/src/vasp/client/types.ts.hbs +33 -0
  113. package/templates/templates/spa/ts/src/vasp/plugin.ts.hbs +12 -0
  114. package/templates/templates/spa/ts/vite.config.ts.hbs +26 -0
  115. package/templates/templates/ssr/js/_page.vue.hbs +10 -0
  116. package/templates/templates/ssr/js/app.vue.hbs +3 -0
  117. package/templates/templates/ssr/js/composables/useAuth.js.hbs +52 -0
  118. package/templates/templates/ssr/js/composables/useVasp.js.hbs +6 -0
  119. package/templates/templates/ssr/js/middleware/auth.js.hbs +8 -0
  120. package/templates/templates/ssr/js/nuxt.config.js.hbs +15 -0
  121. package/templates/templates/ssr/js/plugins/vasp.client.js.hbs +17 -0
  122. package/templates/templates/ssr/js/plugins/vasp.server.js.hbs +33 -0
  123. package/templates/templates/ssr/ts/_page.vue.hbs +10 -0
  124. package/templates/templates/ssr/ts/app.vue.hbs +3 -0
  125. package/templates/templates/ssr/ts/composables/useAuth.ts.hbs +56 -0
  126. package/templates/templates/ssr/ts/composables/useVasp.ts.hbs +10 -0
  127. package/templates/templates/ssr/ts/middleware/auth.ts.hbs +8 -0
  128. package/templates/templates/ssr/ts/nuxt.config.ts.hbs +19 -0
  129. package/templates/templates/ssr/ts/plugins/vasp.client.ts.hbs +17 -0
  130. package/templates/templates/ssr/ts/plugins/vasp.server.ts.hbs +33 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vasp-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "The Vasp CLI — declarative full-stack framework for Vue developers",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -10,16 +10,14 @@
10
10
  "files": [
11
11
  "dist",
12
12
  "starters",
13
+ "templates",
13
14
  "README.md"
14
15
  ],
15
16
  "scripts": {
16
- "build": "bun build ./bin/vasp.ts --target=bun --outfile=dist/vasp --minify --banner='#!/usr/bin/env bun' && chmod +x dist/vasp",
17
+ "build": "bun build ./bin/vasp.ts --target=bun --outfile=dist/vasp --minify --banner='#!/usr/bin/env bun' && chmod +x dist/vasp && cp -r ../../templates ./templates",
17
18
  "test": "vitest run"
18
19
  },
19
20
  "dependencies": {
20
- "@vasp-framework/core": "workspace:*",
21
- "@vasp-framework/generator": "workspace:*",
22
- "@vasp-framework/parser": "workspace:*",
23
21
  "picocolors": "^1.1.0"
24
22
  },
25
23
  "devDependencies": {
@@ -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,8 @@
1
+ node_modules
2
+ dist
3
+ .output
4
+ .nuxt
5
+ .vasp-gen
6
+ .env
7
+ .env.local
8
+ *.log
@@ -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
+ })
@@ -0,0 +1,53 @@
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 GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || ''
8
+ const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || ''
9
+ const REDIRECT_URI = process.env.GOOGLE_REDIRECT_URI || 'http://localhost:{{backendPort}}/auth/google/callback'
10
+
11
+ export const googleRoutes = new Elysia()
12
+ .use(authPlugin)
13
+ .get('/google', ({ set }) => {
14
+ const params = new URLSearchParams({
15
+ client_id: GOOGLE_CLIENT_ID,
16
+ redirect_uri: REDIRECT_URI,
17
+ response_type: 'code',
18
+ scope: 'openid email profile',
19
+ })
20
+ set.redirect = `https://accounts.google.com/o/oauth2/v2/auth?${params}`
21
+ })
22
+ .get('/google/callback', async ({ query, jwt, cookie: { token }, set }) => {
23
+ const { code } = query
24
+ if (!code) { set.status = 400; return { error: 'Missing code' } }
25
+
26
+ // Exchange code for tokens
27
+ const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
28
+ method: 'POST',
29
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
30
+ body: new URLSearchParams({
31
+ code,
32
+ client_id: GOOGLE_CLIENT_ID,
33
+ client_secret: GOOGLE_CLIENT_SECRET,
34
+ redirect_uri: REDIRECT_URI,
35
+ grant_type: 'authorization_code',
36
+ }),
37
+ })
38
+ const tokenData = await tokenRes.json()
39
+ const idToken = tokenData.id_token
40
+ const payload = JSON.parse(Buffer.from(idToken.split('.')[1], 'base64').toString())
41
+ const googleId = payload.sub
42
+ const email = payload.email
43
+
44
+ let [user] = await db.select().from(users).where(eq(users.googleId, googleId)).limit(1)
45
+ if (!user) {
46
+ ;[user] = await db.insert(users).values({ username: email, email, googleId }).returning()
47
+ }
48
+ if (!user) { set.status = 500; return { error: 'Failed to create user' } }
49
+
50
+ const tokenValue = await jwt.sign({ userId: user.id })
51
+ token.set({ value: tokenValue, httpOnly: true, sameSite: 'lax', path: '/' })
52
+ set.redirect = '/'
53
+ })
@@ -0,0 +1,69 @@
1
+ import { Elysia, t } 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
+ async function hashPassword(password{{#if isTypeScript}}: string{{/if}}) {
8
+ const encoder = new TextEncoder()
9
+ const data = encoder.encode(password)
10
+ const hash = await crypto.subtle.digest('SHA-256', data)
11
+ return Buffer.from(hash).toString('hex')
12
+ }
13
+
14
+ async function verifyPassword(password{{#if isTypeScript}}: string{{/if}}, hash{{#if isTypeScript}}: string{{/if}}) {
15
+ return (await hashPassword(password)) === hash
16
+ }
17
+
18
+ export const usernameAndPasswordRoutes = new Elysia()
19
+ .use(authPlugin)
20
+ .post(
21
+ '/register',
22
+ async ({ body, jwt, cookie: { token }, set }) => {
23
+ const existing = await db.select().from(users).where(eq(users.username, body.username)).limit(1)
24
+ if (existing.length > 0) {
25
+ set.status = 400
26
+ return { error: 'Username already taken' }
27
+ }
28
+ const passwordHash = await hashPassword(body.password)
29
+ const [user] = await db
30
+ .insert(users)
31
+ .values({ username: body.username, email: body.email ?? null, passwordHash })
32
+ .returning()
33
+ if (!user) {
34
+ set.status = 500
35
+ return { error: 'Failed to create user' }
36
+ }
37
+ const tokenValue = await jwt.sign({ userId: user.id })
38
+ token.set({ value: tokenValue, httpOnly: true, sameSite: 'lax', path: '/' })
39
+ const { passwordHash: _ph, ...safeUser } = user
40
+ return safeUser
41
+ },
42
+ {
43
+ body: t.Object({
44
+ username: t.String({ minLength: 3 }),
45
+ password: t.String({ minLength: 8 }),
46
+ email: t.Optional(t.String({ format: 'email' })),
47
+ }),
48
+ },
49
+ )
50
+ .post(
51
+ '/login',
52
+ async ({ body, jwt, cookie: { token }, set }) => {
53
+ const [user] = await db.select().from(users).where(eq(users.username, body.username)).limit(1)
54
+ if (!user || !user.passwordHash || !(await verifyPassword(body.password, user.passwordHash))) {
55
+ set.status = 401
56
+ return { error: 'Invalid username or password' }
57
+ }
58
+ const tokenValue = await jwt.sign({ userId: user.id })
59
+ token.set({ value: tokenValue, httpOnly: true, sameSite: 'lax', path: '/' })
60
+ const { passwordHash: _ph, ...safeUser } = user
61
+ return safeUser
62
+ },
63
+ {
64
+ body: t.Object({
65
+ username: t.String(),
66
+ password: t.String(),
67
+ }),
68
+ },
69
+ )
@@ -0,0 +1,2 @@
1
+ [install]
2
+ exact = false
@@ -0,0 +1,37 @@
1
+ import { pgTable, serial, text, timestamp, boolean } from 'drizzle-orm/pg-core'
2
+ {{#if isTypeScript}}
3
+ import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'
4
+ {{/if}}
5
+
6
+ {{#if hasAuth}}
7
+ // Users table — generated by Vasp auth system
8
+ export const users = pgTable('users', {
9
+ id: serial('id').primaryKey(),
10
+ username: text('username').notNull().unique(),
11
+ email: text('email').unique(),
12
+ passwordHash: text('password_hash'),
13
+ googleId: text('google_id').unique(),
14
+ githubId: text('github_id').unique(),
15
+ createdAt: timestamp('created_at').defaultNow().notNull(),
16
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
17
+ })
18
+ {{#if isTypeScript}}
19
+ export type User = InferSelectModel<typeof users>
20
+ export type NewUser = InferInsertModel<typeof users>
21
+ {{/if}}
22
+
23
+ {{/if}}
24
+ {{#each cruds}}
25
+ // {{entity}} table — add your columns below
26
+ export const {{camelCase entity}}s = pgTable('{{camelCase entity}}s', {
27
+ id: serial('id').primaryKey(),
28
+ // TODO: Add your {{entity}} columns here
29
+ createdAt: timestamp('created_at').defaultNow().notNull(),
30
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
31
+ })
32
+ {{#if ../isTypeScript}}
33
+ export type {{pascalCase entity}} = InferSelectModel<typeof {{camelCase entity}}s>
34
+ export type New{{pascalCase entity}} = InferInsertModel<typeof {{camelCase entity}}s>
35
+ {{/if}}
36
+
37
+ {{/each}}
@@ -0,0 +1,24 @@
1
+ import { getBoss } from './boss.{{ext}}'
2
+ import { {{namedExport}} } from '{{importPath fnSource ext}}'
3
+
4
+ const JOB_NAME = '{{camelCase name}}'
5
+
6
+ /**
7
+ * Register the '{{name}}' job worker with PgBoss.
8
+ * Called once on server startup.
9
+ */
10
+ export async function register{{pascalCase name}}Worker() {
11
+ const boss = await getBoss()
12
+ await boss.work(JOB_NAME, async (job) => {
13
+ await {{namedExport}}(job.data)
14
+ })
15
+ }
16
+
17
+ /**
18
+ * Schedule a '{{name}}' job.
19
+ * @param {unknown} data - Data to pass to the job handler
20
+ */
21
+ export async function schedule{{pascalCase name}}(data) {
22
+ const boss = await getBoss()
23
+ return boss.send(JOB_NAME, data)
24
+ }
@@ -0,0 +1,15 @@
1
+ import PgBoss from 'pg-boss'
2
+
3
+ const connectionString = process.env.DATABASE_URL
4
+ if (!connectionString) throw new Error('DATABASE_URL is required for PgBoss job queue')
5
+
6
+ // Singleton PgBoss instance shared across all job workers
7
+ let boss = null
8
+
9
+ export async function getBoss() {
10
+ if (!boss) {
11
+ boss = new PgBoss(connectionString)
12
+ await boss.start()
13
+ }
14
+ return boss
15
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "{{kebabCase appName}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vasp start",
8
+ "build": "vasp build",
9
+ "dev:server": "bun --hot server/index.{{ext}}",
10
+ "dev:client": "{{#if isSpa}}vite{{else}}nuxt dev{{/if}}"
11
+ },
12
+ "dependencies": {
13
+ "@vasp-framework/runtime": "^0.1.0",
14
+ "elysia": "^1.1.0",
15
+ "@elysiajs/cors": "^1.1.0",
16
+ "@elysiajs/static": "^1.1.0",
17
+ "drizzle-orm": "^0.36.0",
18
+ "postgres": "^3.4.0",
19
+ "ofetch": "^1.3.4",
20
+ "vue": "^3.5.0"{{#if isSpa}},
21
+ "vue-router": "^4.4.0"{{else}},
22
+ "nuxt": "^4.0.0"{{/if}}{{#if hasJobs}},
23
+ "pg-boss": "^10.0.0"{{/if}}
24
+ },
25
+ "devDependencies": {
26
+ "drizzle-kit": "^0.28.0"{{#if isSpa}},
27
+ "@vitejs/plugin-vue": "^5.2.0",
28
+ "vite": "^6.0.0"{{/if}}{{#if isTypeScript}},
29
+ "typescript": "^5.6.0",
30
+ "vue-tsc": "^2.0.0"{{/if}}
31
+ }
32
+ }
@@ -0,0 +1,12 @@
1
+ import { drizzle } from 'drizzle-orm/postgres-js'
2
+ import postgres from 'postgres'
3
+ import * as schema from '../../drizzle/schema.{{ext}}'
4
+
5
+ const connectionString = process.env.DATABASE_URL
6
+
7
+ if (!connectionString) {
8
+ throw new Error('DATABASE_URL environment variable is required')
9
+ }
10
+
11
+ const client = postgres(connectionString)
12
+ export const db = drizzle(client, { schema })
@@ -0,0 +1,52 @@
1
+ import { Elysia } from 'elysia'
2
+ import { cors } from '@elysiajs/cors'
3
+ import { staticPlugin } from '@elysiajs/static'
4
+ import { db } from './db/client.{{ext}}'
5
+ {{#if hasAuth}}
6
+ import { authRoutes } from './auth/index.{{ext}}'
7
+ {{/if}}
8
+ {{#each queries}}
9
+ import { {{camelCase name}}Route } from './routes/queries/{{camelCase name}}.{{../ext}}'
10
+ {{/each}}
11
+ {{#each actions}}
12
+ import { {{camelCase name}}Route } from './routes/actions/{{camelCase name}}.{{../ext}}'
13
+ {{/each}}
14
+ {{#each cruds}}
15
+ import { {{camelCase entity}}CrudRoutes } from './routes/crud/{{camelCase entity}}.{{../ext}}'
16
+ {{/each}}
17
+ {{#if hasRealtime}}
18
+ import { realtimeRoutes } from './routes/realtime/index.{{ext}}'
19
+ {{/if}}
20
+ {{#each jobs}}
21
+ import { {{camelCase name}}ScheduleRoute } from './routes/jobs/{{camelCase name}}Schedule.{{../ext}}'
22
+ {{/each}}
23
+
24
+ const PORT = Number(process.env.PORT) || {{backendPort}}
25
+
26
+ const app = new Elysia()
27
+ .use(cors({
28
+ origin: process.env.CORS_ORIGIN || 'http://localhost:{{frontendPort}}',
29
+ credentials: true,
30
+ }))
31
+ .get('/api/health', () => ({ status: 'ok', version: '{{vaspVersion}}' }))
32
+ {{#if hasAuth}}
33
+ .use(authRoutes)
34
+ {{/if}}
35
+ {{#each queries}}
36
+ .use({{camelCase name}}Route)
37
+ {{/each}}
38
+ {{#each actions}}
39
+ .use({{camelCase name}}Route)
40
+ {{/each}}
41
+ {{#each cruds}}
42
+ .use({{camelCase entity}}CrudRoutes)
43
+ {{/each}}
44
+ {{#if hasRealtime}}
45
+ .use(realtimeRoutes)
46
+ {{/if}}
47
+ {{#each jobs}}
48
+ .use({{camelCase name}}ScheduleRoute)
49
+ {{/each}}
50
+ .listen(PORT)
51
+
52
+ console.log(`🚀 Vasp backend running at http://localhost:${PORT}`)
@@ -0,0 +1,20 @@
1
+ import { Elysia } from 'elysia'
2
+ import { db } from '../../db/client.{{ext}}'
3
+ {{#if requiresAuth}}
4
+ import { requireAuth } from '../../auth/middleware.{{ext}}'
5
+ {{/if}}
6
+ import { {{namedExport}} } from '{{importPath fnSource ext}}'
7
+
8
+ export const {{camelCase name}}Route = new Elysia()
9
+ {{#if requiresAuth}}
10
+ .use(requireAuth)
11
+ .post('/api/actions/{{camelCase name}}', async ({ body, user }) => {
12
+ const result = await {{namedExport}}({ db, user, args: body })
13
+ return result
14
+ })
15
+ {{else}}
16
+ .post('/api/actions/{{camelCase name}}', async ({ body }) => {
17
+ const result = await {{namedExport}}({ db, args: body })
18
+ return result
19
+ })
20
+ {{/if}}
@@ -0,0 +1,42 @@
1
+ import { Elysia, t } from 'elysia'
2
+ import { db } from '../../db/client.{{ext}}'
3
+ import { eq } from 'drizzle-orm'
4
+ import { {{camelCase entity}}s } from '../../../drizzle/schema.{{ext}}'
5
+ {{#if hasAuth}}
6
+ import { requireAuth } from '../../auth/middleware.{{ext}}'
7
+ {{/if}}
8
+
9
+ export const {{camelCase entity}}CrudRoutes = new Elysia({ prefix: '/api/crud/{{camelCase entity}}' })
10
+ {{#if hasAuth}}
11
+ .use(requireAuth)
12
+ {{/if}}
13
+ {{#if (includes operations "list")}}
14
+ .get('/', async () => {
15
+ return db.select().from({{camelCase entity}}s)
16
+ })
17
+ {{/if}}
18
+ {{#if (includes operations "create")}}
19
+ .post('/', async ({ body }) => {
20
+ const [created] = await db.insert({{camelCase entity}}s).values(body).returning()
21
+ return created
22
+ })
23
+ {{/if}}
24
+ .get('/:id', async ({ params: { id }, set }) => {
25
+ const [item] = await db.select().from({{camelCase entity}}s).where(eq({{camelCase entity}}s.id, Number(id))).limit(1)
26
+ if (!item) { set.status = 404; return { error: 'Not found' } }
27
+ return item
28
+ })
29
+ {{#if (includes operations "update")}}
30
+ .put('/:id', async ({ params: { id }, body, set }) => {
31
+ const [updated] = await db.update({{camelCase entity}}s).set(body).where(eq({{camelCase entity}}s.id, Number(id))).returning()
32
+ if (!updated) { set.status = 404; return { error: 'Not found' } }
33
+ return updated
34
+ })
35
+ {{/if}}
36
+ {{#if (includes operations "delete")}}
37
+ .delete('/:id', async ({ params: { id }, set }) => {
38
+ const [deleted] = await db.delete({{camelCase entity}}s).where(eq({{camelCase entity}}s.id, Number(id))).returning()
39
+ if (!deleted) { set.status = 404; return { error: 'Not found' } }
40
+ return { ok: true }
41
+ })
42
+ {{/if}}
@@ -0,0 +1,12 @@
1
+ import { Elysia, t } from 'elysia'
2
+ import { schedule{{pascalCase name}} } from '../../../jobs/{{camelCase name}}.{{ext}}'
3
+
4
+ export const {{camelCase name}}ScheduleRoute = new Elysia()
5
+ .post(
6
+ '/api/jobs/{{camelCase name}}/schedule',
7
+ async ({ body }) => {
8
+ const id = await schedule{{pascalCase name}}(body)
9
+ return { jobId: id }
10
+ },
11
+ { body: t.Unknown() },
12
+ )