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,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,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
|
+
)
|
|
@@ -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
|
+
.get('/api/queries/{{camelCase name}}', async ({ query, user }) => {
|
|
12
|
+
const result = await {{namedExport}}({ db, user, args: query })
|
|
13
|
+
return result
|
|
14
|
+
})
|
|
15
|
+
{{else}}
|
|
16
|
+
.get('/api/queries/{{camelCase name}}', async ({ query }) => {
|
|
17
|
+
const result = await {{namedExport}}({ db, args: query })
|
|
18
|
+
return result
|
|
19
|
+
})
|
|
20
|
+
{{/if}}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Elysia } from 'elysia'
|
|
2
|
+
|
|
3
|
+
// In-process subscriber set for '{{name}}' channel
|
|
4
|
+
// Swap this for a Redis adapter in production multi-instance deployments
|
|
5
|
+
const subscribers = new Set()
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Publish a realtime event to all connected subscribers of '{{name}}'.
|
|
9
|
+
* Called automatically by CRUD mutation handlers.
|
|
10
|
+
*/
|
|
11
|
+
export function publish{{pascalCase name}}(event, data) {
|
|
12
|
+
const message = JSON.stringify({ channel: '{{camelCase name}}', event, data })
|
|
13
|
+
for (const ws of subscribers) {
|
|
14
|
+
try { ws.send(message) } catch { subscribers.delete(ws) }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const {{camelCase name}}Channel = new Elysia()
|
|
19
|
+
.ws('/ws/{{camelCase name}}', {
|
|
20
|
+
open(ws) {
|
|
21
|
+
subscribers.add(ws)
|
|
22
|
+
},
|
|
23
|
+
message(ws, msg) {
|
|
24
|
+
// Echo back for debugging; extend for room-based subscriptions
|
|
25
|
+
ws.send(JSON.stringify({ ack: msg }))
|
|
26
|
+
},
|
|
27
|
+
close(ws) {
|
|
28
|
+
subscribers.delete(ws)
|
|
29
|
+
},
|
|
30
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noUnusedLocals": true,
|
|
9
|
+
"noUnusedParameters": true,
|
|
10
|
+
"noFallthroughCasesInSwitch": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"jsx": "preserve",
|
|
14
|
+
"paths": {
|
|
15
|
+
"@src/*": ["./src/*"],
|
|
16
|
+
"@vasp-framework/client": ["./src/vasp/client/index.ts"]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"include": ["src/**/*.ts", "src/**/*.vue", "server/**/*.ts", "drizzle/**/*.ts"],
|
|
20
|
+
"exclude": ["node_modules", "dist", ".vasp-gen"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{{appTitle}}</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app"></div>
|
|
10
|
+
<script type="module" src="/src/main.js"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
2
|
+
{{#if hasAuth}}
|
|
3
|
+
import { useAuth } from '../vasp/auth.js'
|
|
4
|
+
{{/if}}
|
|
5
|
+
|
|
6
|
+
const routes = [
|
|
7
|
+
{{#each routes}}
|
|
8
|
+
{
|
|
9
|
+
path: '{{path}}',
|
|
10
|
+
component: () => import('{{lookup ../pagesMap to}}'),
|
|
11
|
+
},
|
|
12
|
+
{{/each}}
|
|
13
|
+
{{#if hasAuth}}
|
|
14
|
+
{
|
|
15
|
+
path: '/login',
|
|
16
|
+
component: () => import('../pages/Login.vue'),
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
path: '/register',
|
|
20
|
+
component: () => import('../pages/Register.vue'),
|
|
21
|
+
},
|
|
22
|
+
{{/if}}
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const router = createRouter({
|
|
26
|
+
history: createWebHistory(),
|
|
27
|
+
routes,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
{{#if hasAuth}}
|
|
31
|
+
router.beforeEach(async (to) => {
|
|
32
|
+
const { user, checkAuth } = useAuth()
|
|
33
|
+
await checkAuth()
|
|
34
|
+
const publicPaths = ['/login', '/register']
|
|
35
|
+
if (!user.value && !publicPaths.includes(to.path)) {
|
|
36
|
+
return '/login'
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
{{/if}}
|
|
40
|
+
|
|
41
|
+
export default router
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
import { $fetch } from 'ofetch'
|
|
3
|
+
|
|
4
|
+
const user = ref(null)
|
|
5
|
+
let checked = false
|
|
6
|
+
|
|
7
|
+
const API = import.meta.env.VITE_API_URL || '/api'
|
|
8
|
+
|
|
9
|
+
export function useAuth() {
|
|
10
|
+
async function checkAuth() {
|
|
11
|
+
if (checked) return
|
|
12
|
+
checked = true
|
|
13
|
+
try {
|
|
14
|
+
user.value = await $fetch(`${API}/auth/me`, { credentials: 'include' })
|
|
15
|
+
} catch {
|
|
16
|
+
user.value = null
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function login(username, password) {
|
|
21
|
+
user.value = await $fetch(`${API}/auth/login`, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
body: { username, password },
|
|
24
|
+
credentials: 'include',
|
|
25
|
+
})
|
|
26
|
+
return user.value
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function register(username, password, email) {
|
|
30
|
+
user.value = await $fetch(`${API}/auth/register`, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
body: { username, password, email },
|
|
33
|
+
credentials: 'include',
|
|
34
|
+
})
|
|
35
|
+
return user.value
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function logout() {
|
|
39
|
+
await $fetch(`${API}/auth/logout`, { method: 'POST', credentials: 'include' })
|
|
40
|
+
user.value = null
|
|
41
|
+
checked = false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { user, checkAuth, login, register, logout }
|
|
45
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Auto-generated by Vasp — do not edit
|
|
2
|
+
import { useVasp } from '@vasp-framework/runtime'
|
|
3
|
+
|
|
4
|
+
{{#each actions}}
|
|
5
|
+
/**
|
|
6
|
+
* Call the '{{name}}' action on the Vasp backend.
|
|
7
|
+
* @param {unknown} args - Arguments to pass to the action function
|
|
8
|
+
* @returns {Promise<unknown>}
|
|
9
|
+
*/
|
|
10
|
+
export async function {{camelCase name}}(args) {
|
|
11
|
+
const { $vasp } = useVasp()
|
|
12
|
+
return $vasp.action('{{camelCase name}}', args)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
{{/each}}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Auto-generated by Vasp — do not edit
|
|
2
|
+
import { $fetch } from 'ofetch'
|
|
3
|
+
|
|
4
|
+
const API = import.meta.env.VITE_API_URL || '/api'
|
|
5
|
+
|
|
6
|
+
{{#each cruds}}
|
|
7
|
+
/**
|
|
8
|
+
* CRUD helpers for the '{{entity}}' entity.
|
|
9
|
+
*/
|
|
10
|
+
export function use{{pascalCase entity}}Crud() {
|
|
11
|
+
const base = `${API}/crud/{{camelCase entity}}`
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
{{#if (includes operations "list")}}
|
|
15
|
+
list: () => $fetch(base, { credentials: 'include' }),
|
|
16
|
+
{{/if}}
|
|
17
|
+
{{#if (includes operations "create")}}
|
|
18
|
+
create: (data) => $fetch(base, { method: 'POST', body: data, credentials: 'include' }),
|
|
19
|
+
{{/if}}
|
|
20
|
+
get: (id) => $fetch(`${base}/${id}`, { credentials: 'include' }),
|
|
21
|
+
{{#if (includes operations "update")}}
|
|
22
|
+
update: (id, data) => $fetch(`${base}/${id}`, { method: 'PUT', body: data, credentials: 'include' }),
|
|
23
|
+
{{/if}}
|
|
24
|
+
{{#if (includes operations "delete")}}
|
|
25
|
+
remove: (id) => $fetch(`${base}/${id}`, { method: 'DELETE', credentials: 'include' }),
|
|
26
|
+
{{/if}}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
{{/each}}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Auto-generated by Vasp — do not edit directly
|
|
2
|
+
// Re-run `vasp build` or restart `vasp start` to regenerate
|
|
3
|
+
|
|
4
|
+
export { useVasp } from '@vasp-framework/runtime'
|
|
5
|
+
{{#each queries}}
|
|
6
|
+
export { {{camelCase name}} } from './queries.js'
|
|
7
|
+
{{/each}}
|
|
8
|
+
{{#each actions}}
|
|
9
|
+
export { {{camelCase name}} } from './actions.js'
|
|
10
|
+
{{/each}}
|
|
11
|
+
{{#each cruds}}
|
|
12
|
+
export { use{{pascalCase entity}}Crud } from './crud.js'
|
|
13
|
+
{{/each}}
|
|
14
|
+
{{#if hasRealtime}}
|
|
15
|
+
export { useRealtime } from './realtime.js'
|
|
16
|
+
{{/if}}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Auto-generated by Vasp — do not edit
|
|
2
|
+
import { useVasp } from '@vasp-framework/runtime'
|
|
3
|
+
|
|
4
|
+
{{#each queries}}
|
|
5
|
+
/**
|
|
6
|
+
* Call the '{{name}}' query on the Vasp backend.
|
|
7
|
+
* @param {unknown} [args] - Arguments to pass to the query function
|
|
8
|
+
* @returns {Promise<unknown>}
|
|
9
|
+
*/
|
|
10
|
+
export async function {{camelCase name}}(args) {
|
|
11
|
+
const { $vasp } = useVasp()
|
|
12
|
+
return $vasp.query('{{camelCase name}}', args)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
{{/each}}
|