vasp-cli 0.4.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/vasp +82 -34
- package/package.json +2 -2
- package/starters/minimal.vasp +1 -1
- package/starters/recipe.vasp +11 -20
- package/starters/todo-auth-ssr.vasp +33 -20
- package/starters/todo.vasp +15 -8
- package/templates/shared/.gitignore.hbs +1 -0
- package/templates/shared/auth/server/index.hbs +4 -8
- package/templates/shared/auth/server/middleware.hbs +33 -15
- package/templates/{templates/shared → shared}/auth/server/plugin.hbs +0 -2
- package/templates/shared/auth/server/providers/github.hbs +1 -1
- package/templates/shared/auth/server/providers/google.hbs +1 -1
- package/templates/shared/auth/server/providers/usernameAndPassword.hbs +3 -6
- package/templates/shared/bunfig.toml.hbs +3 -0
- package/templates/shared/drizzle/schema.hbs +38 -19
- package/templates/shared/package.json.hbs +11 -4
- package/templates/shared/server/db/client.hbs +19 -1
- package/templates/shared/server/db/seed.hbs +16 -0
- package/templates/shared/server/index.hbs +40 -0
- package/templates/shared/server/middleware/errorHandler.hbs +75 -0
- package/templates/shared/server/middleware/logger.hbs +74 -0
- package/templates/shared/server/middleware/rateLimit.hbs +2 -2
- package/templates/shared/server/routes/_vasp.hbs +37 -0
- package/templates/shared/server/routes/actions/_action.hbs +5 -1
- package/templates/shared/server/routes/api/_api.hbs +24 -0
- package/templates/shared/server/routes/crud/_crud.hbs +58 -10
- package/templates/shared/server/routes/queries/_query.hbs +5 -1
- package/templates/shared/shared/types.hbs +58 -0
- package/templates/shared/shared/validation.hbs +20 -0
- package/templates/shared/tests/actions/_action.test.js.hbs +7 -0
- package/templates/shared/tests/actions/_action.test.ts.hbs +7 -0
- package/templates/shared/tests/auth/login.test.js.hbs +7 -0
- package/templates/shared/tests/auth/login.test.ts.hbs +7 -0
- package/templates/shared/tests/crud/_entity.test.js.hbs +7 -0
- package/templates/shared/tests/crud/_entity.test.ts.hbs +7 -0
- package/templates/shared/tests/queries/_query.test.js.hbs +7 -0
- package/templates/shared/tests/queries/_query.test.ts.hbs +7 -0
- package/templates/shared/tests/setup.js.hbs +5 -0
- package/templates/shared/tests/setup.ts.hbs +5 -0
- package/templates/shared/tests/vitest.config.js.hbs +8 -0
- package/templates/shared/tests/vitest.config.ts.hbs +8 -0
- package/templates/shared/tsconfig.json.hbs +2 -1
- package/templates/spa/js/src/App.vue.hbs +9 -1
- package/templates/spa/js/src/components/VaspErrorBoundary.vue.hbs +33 -0
- package/templates/spa/js/src/components/VaspNotifications.vue.hbs +60 -0
- package/templates/spa/js/src/vasp/auth.js.hbs +31 -15
- package/templates/spa/js/src/vasp/client/actions.js.hbs +7 -1
- package/templates/spa/js/src/vasp/client/crud.js.hbs +94 -5
- package/templates/spa/js/src/vasp/useVaspNotifications.js.hbs +35 -0
- package/templates/spa/js/vite.config.js.hbs +1 -0
- package/templates/spa/ts/src/App.vue.hbs +9 -1
- package/templates/spa/ts/src/components/VaspErrorBoundary.vue.hbs +33 -0
- package/templates/spa/ts/src/components/VaspNotifications.vue.hbs +60 -0
- package/templates/spa/ts/src/vasp/auth.ts.hbs +31 -15
- package/templates/spa/ts/src/vasp/client/actions.ts.hbs +7 -1
- package/templates/spa/ts/src/vasp/client/crud.ts.hbs +96 -10
- package/templates/spa/ts/src/vasp/client/types.ts.hbs +14 -28
- package/templates/spa/ts/src/vasp/useVaspNotifications.ts.hbs +41 -0
- package/templates/spa/ts/vite.config.ts.hbs +1 -0
- package/templates/ssr/js/error.vue.hbs +23 -0
- package/templates/ssr/js/nuxt.config.js.hbs +1 -0
- package/templates/ssr/ts/error.vue.hbs +26 -0
- package/templates/ssr/ts/nuxt.config.ts.hbs +1 -0
- package/templates/starters/minimal.vasp +15 -0
- package/templates/starters/recipe.vasp +70 -0
- package/templates/starters/todo-auth-ssr.vasp +65 -0
- package/templates/starters/todo.vasp +42 -0
- package/templates/templates/shared/.env.example.hbs +0 -14
- package/templates/templates/shared/.gitignore.hbs +0 -8
- package/templates/templates/shared/auth/client/Login.vue.hbs +0 -46
- package/templates/templates/shared/auth/client/Register.vue.hbs +0 -42
- package/templates/templates/shared/auth/server/index.hbs +0 -46
- package/templates/templates/shared/auth/server/middleware.hbs +0 -33
- package/templates/templates/shared/auth/server/providers/github.hbs +0 -48
- package/templates/templates/shared/auth/server/providers/google.hbs +0 -53
- package/templates/templates/shared/auth/server/providers/usernameAndPassword.hbs +0 -66
- package/templates/templates/shared/bunfig.toml.hbs +0 -5
- package/templates/templates/shared/drizzle/drizzle.config.hbs +0 -10
- package/templates/templates/shared/drizzle/schema.hbs +0 -48
- package/templates/templates/shared/jobs/_job.hbs +0 -34
- package/templates/templates/shared/jobs/boss.hbs +0 -15
- package/templates/templates/shared/package.json.hbs +0 -38
- package/templates/templates/shared/server/db/client.hbs +0 -12
- package/templates/templates/shared/server/index.hbs +0 -73
- package/templates/templates/shared/server/middleware/csrf.hbs +0 -34
- package/templates/templates/shared/server/middleware/rateLimit.hbs +0 -44
- package/templates/templates/shared/server/routes/actions/_action.hbs +0 -20
- package/templates/templates/shared/server/routes/crud/_crud.hbs +0 -86
- package/templates/templates/shared/server/routes/jobs/_schedule.hbs +0 -12
- package/templates/templates/shared/server/routes/queries/_query.hbs +0 -20
- package/templates/templates/shared/server/routes/realtime/_channel.hbs +0 -78
- package/templates/templates/shared/server/routes/realtime/index.hbs +0 -9
- package/templates/templates/shared/tsconfig.json.hbs +0 -21
- package/templates/templates/spa/js/index.html.hbs +0 -12
- package/templates/templates/spa/js/src/App.vue.hbs +0 -3
- package/templates/templates/spa/js/src/main.js.hbs +0 -9
- package/templates/templates/spa/js/src/router/index.js.hbs +0 -41
- package/templates/templates/spa/js/src/vasp/auth.js.hbs +0 -45
- package/templates/templates/spa/js/src/vasp/client/actions.js.hbs +0 -15
- package/templates/templates/spa/js/src/vasp/client/crud.js.hbs +0 -30
- package/templates/templates/spa/js/src/vasp/client/index.js.hbs +0 -16
- package/templates/templates/spa/js/src/vasp/client/queries.js.hbs +0 -15
- package/templates/templates/spa/js/src/vasp/client/realtime.js.hbs +0 -51
- package/templates/templates/spa/js/src/vasp/plugin.js.hbs +0 -11
- package/templates/templates/spa/js/vite.config.js.hbs +0 -26
- package/templates/templates/spa/ts/index.html.hbs +0 -12
- package/templates/templates/spa/ts/src/App.vue.hbs +0 -3
- package/templates/templates/spa/ts/src/main.ts.hbs +0 -9
- package/templates/templates/spa/ts/src/router/index.ts.hbs +0 -41
- package/templates/templates/spa/ts/src/vasp/auth.ts.hbs +0 -53
- package/templates/templates/spa/ts/src/vasp/client/actions.ts.hbs +0 -19
- package/templates/templates/spa/ts/src/vasp/client/crud.ts.hbs +0 -37
- package/templates/templates/spa/ts/src/vasp/client/index.ts.hbs +0 -17
- package/templates/templates/spa/ts/src/vasp/client/queries.ts.hbs +0 -19
- package/templates/templates/spa/ts/src/vasp/client/realtime.ts.hbs +0 -56
- package/templates/templates/spa/ts/src/vasp/client/types.ts.hbs +0 -33
- package/templates/templates/spa/ts/src/vasp/plugin.ts.hbs +0 -12
- package/templates/templates/spa/ts/vite.config.ts.hbs +0 -26
- package/templates/templates/ssr/js/_page.vue.hbs +0 -10
- package/templates/templates/ssr/js/app.vue.hbs +0 -3
- package/templates/templates/ssr/js/composables/useAuth.js.hbs +0 -52
- package/templates/templates/ssr/js/composables/useVasp.js.hbs +0 -6
- package/templates/templates/ssr/js/middleware/auth.js.hbs +0 -8
- package/templates/templates/ssr/js/nuxt.config.js.hbs +0 -15
- package/templates/templates/ssr/js/plugins/vasp.client.js.hbs +0 -27
- package/templates/templates/ssr/js/plugins/vasp.server.js.hbs +0 -33
- package/templates/templates/ssr/ts/_page.vue.hbs +0 -10
- package/templates/templates/ssr/ts/app.vue.hbs +0 -3
- package/templates/templates/ssr/ts/composables/useAuth.ts.hbs +0 -56
- package/templates/templates/ssr/ts/composables/useVasp.ts.hbs +0 -10
- package/templates/templates/ssr/ts/middleware/auth.ts.hbs +0 -8
- package/templates/templates/ssr/ts/nuxt.config.ts.hbs +0 -19
- package/templates/templates/ssr/ts/plugins/vasp.client.ts.hbs +0 -27
- package/templates/templates/ssr/ts/plugins/vasp.server.ts.hbs +0 -33
- /package/templates/{templates/shared → shared}/.env.hbs +0 -0
- /package/templates/{templates/shared → shared}/README.md.hbs +0 -0
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { pgTable, serial, text, integer, boolean, timestamp, doublePrecision } 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 crudsWithFields}}
|
|
25
|
-
{{#if hasEntity}}
|
|
26
|
-
// {{entity}} table — generated from entity block
|
|
27
|
-
export const {{camelCase entity}}s = pgTable('{{camelCase entity}}s', {
|
|
28
|
-
{{#each fields}}
|
|
29
|
-
{{camelCase name}}: {{{drizzleColumn name type modifiers}}},
|
|
30
|
-
{{/each}}
|
|
31
|
-
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
32
|
-
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
33
|
-
})
|
|
34
|
-
{{else}}
|
|
35
|
-
// {{entity}} table — no entity block found, add your columns below
|
|
36
|
-
export const {{camelCase entity}}s = pgTable('{{camelCase entity}}s', {
|
|
37
|
-
id: serial('id').primaryKey(),
|
|
38
|
-
// TODO: Add your {{entity}} columns here
|
|
39
|
-
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
40
|
-
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
41
|
-
})
|
|
42
|
-
{{/if}}
|
|
43
|
-
{{#if ../isTypeScript}}
|
|
44
|
-
export type {{pascalCase entity}} = InferSelectModel<typeof {{camelCase entity}}s>
|
|
45
|
-
export type New{{pascalCase entity}} = InferInsertModel<typeof {{camelCase entity}}s>
|
|
46
|
-
{{/if}}
|
|
47
|
-
|
|
48
|
-
{{/each}}
|
|
@@ -1,34 +0,0 @@
|
|
|
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
|
-
{{#if hasSchedule}}
|
|
16
|
-
|
|
17
|
-
// Register cron schedule: {{schedule}}
|
|
18
|
-
await boss.schedule(JOB_NAME, '{{schedule}}', {})
|
|
19
|
-
{{/if}}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Schedule a '{{name}}' job on demand.
|
|
24
|
-
* @param {unknown} data - Data to pass to the job handler
|
|
25
|
-
*/
|
|
26
|
-
export async function schedule{{pascalCase name}}(data) {
|
|
27
|
-
const boss = await getBoss()
|
|
28
|
-
return boss.send(JOB_NAME, data, {
|
|
29
|
-
retryLimit: Number(process.env.JOB_RETRY_LIMIT) || 3,
|
|
30
|
-
retryDelay: Number(process.env.JOB_RETRY_DELAY) || 60,
|
|
31
|
-
retryBackoff: true,
|
|
32
|
-
expireInMinutes: Number(process.env.JOB_EXPIRE_MINUTES) || 15,
|
|
33
|
-
})
|
|
34
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
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
|
-
"db:generate": "bunx drizzle-kit generate",
|
|
12
|
-
"db:migrate": "bunx drizzle-kit migrate",
|
|
13
|
-
"db:push": "bunx drizzle-kit push",
|
|
14
|
-
"db:studio": "bunx drizzle-kit studio"
|
|
15
|
-
},
|
|
16
|
-
"dependencies": {
|
|
17
|
-
"@vasp-framework/runtime": "^0.1.0",
|
|
18
|
-
"elysia": "^1.1.0",
|
|
19
|
-
"@elysiajs/cors": "^1.1.0",
|
|
20
|
-
"@elysiajs/static": "^1.1.0",{{#if auth}}
|
|
21
|
-
"@elysiajs/jwt": "^1.1.0",
|
|
22
|
-
"@elysiajs/cookie": "^0.8.0",{{/if}}
|
|
23
|
-
"drizzle-orm": "^0.36.0",
|
|
24
|
-
"postgres": "^3.4.0",
|
|
25
|
-
"ofetch": "^1.3.4",
|
|
26
|
-
"vue": "^3.5.0"{{#if isSpa}},
|
|
27
|
-
"vue-router": "^4.4.0"{{else}},
|
|
28
|
-
"nuxt": "^4.0.0"{{/if}}{{#if hasJobs}},
|
|
29
|
-
"pg-boss": "^10.0.0"{{/if}}
|
|
30
|
-
},
|
|
31
|
-
"devDependencies": {
|
|
32
|
-
"drizzle-kit": "^0.28.0"{{#if isSpa}},
|
|
33
|
-
"@vitejs/plugin-vue": "^5.2.0",
|
|
34
|
-
"vite": "^6.0.0"{{/if}}{{#if isTypeScript}},
|
|
35
|
-
"typescript": "^5.6.0",
|
|
36
|
-
"vue-tsc": "^2.0.0"{{/if}}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
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 })
|
|
@@ -1,73 +0,0 @@
|
|
|
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
|
-
import { rateLimit } from './middleware/rateLimit.{{ext}}'
|
|
6
|
-
{{#if isSsr}}
|
|
7
|
-
import { csrfProtection } from './middleware/csrf.{{ext}}'
|
|
8
|
-
{{/if}}
|
|
9
|
-
{{#if hasAuth}}
|
|
10
|
-
import { authRoutes } from './auth/index.{{ext}}'
|
|
11
|
-
{{/if}}
|
|
12
|
-
{{#each queries}}
|
|
13
|
-
import { {{camelCase name}}Route } from './routes/queries/{{camelCase name}}.{{../ext}}'
|
|
14
|
-
{{/each}}
|
|
15
|
-
{{#each actions}}
|
|
16
|
-
import { {{camelCase name}}Route } from './routes/actions/{{camelCase name}}.{{../ext}}'
|
|
17
|
-
{{/each}}
|
|
18
|
-
{{#each cruds}}
|
|
19
|
-
import { {{camelCase entity}}CrudRoutes } from './routes/crud/{{camelCase entity}}.{{../ext}}'
|
|
20
|
-
{{/each}}
|
|
21
|
-
{{#if hasRealtime}}
|
|
22
|
-
import { realtimeRoutes } from './routes/realtime/index.{{ext}}'
|
|
23
|
-
{{/if}}
|
|
24
|
-
{{#each jobs}}
|
|
25
|
-
import { {{camelCase name}}ScheduleRoute } from './routes/jobs/{{camelCase name}}Schedule.{{../ext}}'
|
|
26
|
-
{{/each}}
|
|
27
|
-
|
|
28
|
-
const PORT = Number(process.env.PORT) || {{backendPort}}
|
|
29
|
-
|
|
30
|
-
const app = new Elysia()
|
|
31
|
-
.use(cors({
|
|
32
|
-
origin: process.env.CORS_ORIGIN || 'http://localhost:{{frontendPort}}',
|
|
33
|
-
credentials: true,
|
|
34
|
-
}))
|
|
35
|
-
.use(rateLimit())
|
|
36
|
-
{{#if isSsr}}
|
|
37
|
-
.use(csrfProtection())
|
|
38
|
-
{{/if}}
|
|
39
|
-
.get('/api/health', () => ({ status: 'ok', version: '{{vaspVersion}}' }))
|
|
40
|
-
{{#if hasAuth}}
|
|
41
|
-
.use(authRoutes)
|
|
42
|
-
{{/if}}
|
|
43
|
-
{{#each queries}}
|
|
44
|
-
.use({{camelCase name}}Route)
|
|
45
|
-
{{/each}}
|
|
46
|
-
{{#each actions}}
|
|
47
|
-
.use({{camelCase name}}Route)
|
|
48
|
-
{{/each}}
|
|
49
|
-
{{#each cruds}}
|
|
50
|
-
.use({{camelCase entity}}CrudRoutes)
|
|
51
|
-
{{/each}}
|
|
52
|
-
{{#if hasRealtime}}
|
|
53
|
-
.use(realtimeRoutes)
|
|
54
|
-
{{/if}}
|
|
55
|
-
{{#each jobs}}
|
|
56
|
-
.use({{camelCase name}}ScheduleRoute)
|
|
57
|
-
{{/each}}
|
|
58
|
-
.onError(({ code, error, set }) => {
|
|
59
|
-
if (code === 'NOT_FOUND') {
|
|
60
|
-
set.status = 404
|
|
61
|
-
return { error: 'Not found' }
|
|
62
|
-
}
|
|
63
|
-
if (code === 'VALIDATION') {
|
|
64
|
-
set.status = 400
|
|
65
|
-
return { error: 'Validation failed', details: error.message }
|
|
66
|
-
}
|
|
67
|
-
console.error('[server error]', error)
|
|
68
|
-
set.status = 500
|
|
69
|
-
return { error: 'Internal server error' }
|
|
70
|
-
})
|
|
71
|
-
.listen(PORT)
|
|
72
|
-
|
|
73
|
-
console.log(`🚀 Vasp backend running at http://localhost:${PORT}`)
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { Elysia } from 'elysia'
|
|
2
|
-
import { randomBytes } from 'node:crypto'
|
|
3
|
-
|
|
4
|
-
const CSRF_COOKIE = 'vasp-csrf'
|
|
5
|
-
const CSRF_HEADER = 'x-csrf-token'
|
|
6
|
-
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS'])
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* CSRF protection middleware — double-submit cookie pattern.
|
|
10
|
-
* On every response, sets a CSRF cookie with a random token.
|
|
11
|
-
* On state-changing requests (POST, PUT, DELETE), validates that the
|
|
12
|
-
* x-csrf-token header matches the cookie value.
|
|
13
|
-
*/
|
|
14
|
-
export function csrfProtection() {
|
|
15
|
-
return new Elysia({ name: 'csrf' })
|
|
16
|
-
.onRequest(({ request, set, cookie }) => {
|
|
17
|
-
// Generate token if not already set
|
|
18
|
-
if (!cookie[CSRF_COOKIE]?.value) {
|
|
19
|
-
const token = randomBytes(32).toString('hex')
|
|
20
|
-
cookie[CSRF_COOKIE].set({ value: token, httpOnly: false, sameSite: 'strict', path: '/' })
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Skip validation for safe methods
|
|
24
|
-
if (SAFE_METHODS.has(request.method)) return
|
|
25
|
-
|
|
26
|
-
const cookieToken = cookie[CSRF_COOKIE]?.value
|
|
27
|
-
const headerToken = request.headers.get(CSRF_HEADER)
|
|
28
|
-
|
|
29
|
-
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
|
|
30
|
-
set.status = 403
|
|
31
|
-
return { error: 'CSRF token mismatch' }
|
|
32
|
-
}
|
|
33
|
-
})
|
|
34
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { Elysia } from 'elysia'
|
|
2
|
-
|
|
3
|
-
const MAX_REQUESTS = Number(process.env.RATE_LIMIT_MAX) || 100
|
|
4
|
-
const WINDOW_MS = Number(process.env.RATE_LIMIT_WINDOW_MS) || 60_000
|
|
5
|
-
|
|
6
|
-
const hits = new Map()
|
|
7
|
-
|
|
8
|
-
// Clean up expired entries every 60 seconds
|
|
9
|
-
setInterval(() => {
|
|
10
|
-
const now = Date.now()
|
|
11
|
-
for (const [key, entry] of hits) {
|
|
12
|
-
if (now - entry.start > WINDOW_MS) hits.delete(key)
|
|
13
|
-
}
|
|
14
|
-
}, 60_000)
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Simple in-memory sliding-window rate limiter.
|
|
18
|
-
* Limits each IP to MAX_REQUESTS per WINDOW_MS.
|
|
19
|
-
*/
|
|
20
|
-
export function rateLimit() {
|
|
21
|
-
return new Elysia({ name: 'rate-limit' })
|
|
22
|
-
.onBeforeHandle(({ request, set }) => {
|
|
23
|
-
const ip = request.headers.get('x-forwarded-for')
|
|
24
|
-
|| request.headers.get('x-real-ip')
|
|
25
|
-
|| 'unknown'
|
|
26
|
-
const now = Date.now()
|
|
27
|
-
let entry = hits.get(ip)
|
|
28
|
-
|
|
29
|
-
if (!entry || now - entry.start > WINDOW_MS) {
|
|
30
|
-
entry = { start: now, count: 0 }
|
|
31
|
-
hits.set(ip, entry)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
entry.count++
|
|
35
|
-
|
|
36
|
-
set.headers['x-ratelimit-limit'] = String(MAX_REQUESTS)
|
|
37
|
-
set.headers['x-ratelimit-remaining'] = String(Math.max(0, MAX_REQUESTS - entry.count))
|
|
38
|
-
|
|
39
|
-
if (entry.count > MAX_REQUESTS) {
|
|
40
|
-
set.status = 429
|
|
41
|
-
return { error: 'Too many requests. Please try again later.' }
|
|
42
|
-
}
|
|
43
|
-
})
|
|
44
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
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}}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { Elysia, t } from 'elysia'
|
|
2
|
-
import { db } from '../../db/client.{{ext}}'
|
|
3
|
-
import { eq, sql, asc, desc, and, ilike } 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
|
-
{{#if hasRealtime}}
|
|
9
|
-
import { publish{{pascalCase entity}} } from '../realtime/{{camelCase realtimeName}}.{{ext}}'
|
|
10
|
-
{{/if}}
|
|
11
|
-
|
|
12
|
-
export const {{camelCase entity}}CrudRoutes = new Elysia({ prefix: '/api/crud/{{camelCase entity}}' })
|
|
13
|
-
{{#if hasAuth}}
|
|
14
|
-
.use(requireAuth)
|
|
15
|
-
{{/if}}
|
|
16
|
-
{{#if (includes operations "list")}}
|
|
17
|
-
.get('/', async ({ query }) => {
|
|
18
|
-
const limit = Math.min(Math.max(Number(query.limit) || 20, 1), 100)
|
|
19
|
-
const offset = Math.max(Number(query.offset) || 0, 0)
|
|
20
|
-
|
|
21
|
-
const table = {{camelCase entity}}s
|
|
22
|
-
|
|
23
|
-
// Multi-column sorting: orderBy=col1,col2 & dir=asc,desc
|
|
24
|
-
const orderByFields = (query.orderBy ?? 'id').split(',')
|
|
25
|
-
const directions = (query.dir ?? 'asc').split(',')
|
|
26
|
-
const orderClauses = orderByFields.map((field, i) => {
|
|
27
|
-
const col = table[field.trim()] ?? table.id
|
|
28
|
-
const dirFn = (directions[i] ?? directions[0] ?? 'asc').trim() === 'desc' ? desc : asc
|
|
29
|
-
return dirFn(col)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
// Build WHERE conditions from filter.* query params
|
|
33
|
-
const conditions = []
|
|
34
|
-
for (const [key, value] of Object.entries(query)) {
|
|
35
|
-
if (!key.startsWith('filter.')) continue
|
|
36
|
-
const field = key.slice(7)
|
|
37
|
-
if (!table[field]) continue
|
|
38
|
-
conditions.push(eq(table[field], value))
|
|
39
|
-
}
|
|
40
|
-
const where = conditions.length > 0 ? and(...conditions) : undefined
|
|
41
|
-
|
|
42
|
-
const baseQuery = db.select().from(table)
|
|
43
|
-
const countQuery = db.select({ count: sql`count(*)::int` }).from(table)
|
|
44
|
-
|
|
45
|
-
const [data, countResult] = await Promise.all([
|
|
46
|
-
(where ? baseQuery.where(where) : baseQuery).orderBy(...orderClauses).limit(limit).offset(offset),
|
|
47
|
-
where ? countQuery.where(where) : countQuery,
|
|
48
|
-
])
|
|
49
|
-
|
|
50
|
-
return { data, total: countResult[0]?.count ?? 0, limit, offset }
|
|
51
|
-
})
|
|
52
|
-
{{/if}}
|
|
53
|
-
{{#if (includes operations "create")}}
|
|
54
|
-
.post('/', async ({ body }) => {
|
|
55
|
-
const [created] = await db.insert({{camelCase entity}}s).values(body).returning()
|
|
56
|
-
{{#if hasRealtime}}
|
|
57
|
-
publish{{pascalCase entity}}('created', created)
|
|
58
|
-
{{/if}}
|
|
59
|
-
return created
|
|
60
|
-
})
|
|
61
|
-
{{/if}}
|
|
62
|
-
.get('/:id', async ({ params: { id }, set }) => {
|
|
63
|
-
const [item] = await db.select().from({{camelCase entity}}s).where(eq({{camelCase entity}}s.id, Number(id))).limit(1)
|
|
64
|
-
if (!item) { set.status = 404; return { error: 'Not found' } }
|
|
65
|
-
return item
|
|
66
|
-
})
|
|
67
|
-
{{#if (includes operations "update")}}
|
|
68
|
-
.put('/:id', async ({ params: { id }, body, set }) => {
|
|
69
|
-
const [updated] = await db.update({{camelCase entity}}s).set(body).where(eq({{camelCase entity}}s.id, Number(id))).returning()
|
|
70
|
-
if (!updated) { set.status = 404; return { error: 'Not found' } }
|
|
71
|
-
{{#if hasRealtime}}
|
|
72
|
-
publish{{pascalCase entity}}('updated', updated)
|
|
73
|
-
{{/if}}
|
|
74
|
-
return updated
|
|
75
|
-
})
|
|
76
|
-
{{/if}}
|
|
77
|
-
{{#if (includes operations "delete")}}
|
|
78
|
-
.delete('/:id', async ({ params: { id }, set }) => {
|
|
79
|
-
const [deleted] = await db.delete({{camelCase entity}}s).where(eq({{camelCase entity}}s.id, Number(id))).returning()
|
|
80
|
-
if (!deleted) { set.status = 404; return { error: 'Not found' } }
|
|
81
|
-
{{#if hasRealtime}}
|
|
82
|
-
publish{{pascalCase entity}}('deleted', deleted)
|
|
83
|
-
{{/if}}
|
|
84
|
-
return { ok: true }
|
|
85
|
-
})
|
|
86
|
-
{{/if}}
|
|
@@ -1,12 +0,0 @@
|
|
|
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
|
-
)
|
|
@@ -1,20 +0,0 @@
|
|
|
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}}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { Elysia } from 'elysia'
|
|
2
|
-
{{#if ../hasAuth}}
|
|
3
|
-
import jwt from '@elysiajs/jwt'
|
|
4
|
-
{{/if}}
|
|
5
|
-
|
|
6
|
-
// Room-based subscriber map for '{{name}}' channel
|
|
7
|
-
// Each room key maps to a set of connected WebSocket clients
|
|
8
|
-
const rooms = new Map()
|
|
9
|
-
|
|
10
|
-
function getRoom(roomId) {
|
|
11
|
-
if (!rooms.has(roomId)) rooms.set(roomId, new Set())
|
|
12
|
-
return rooms.get(roomId)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Publish a realtime event to all subscribers in a specific room of '{{name}}'.
|
|
17
|
-
* Called automatically by CRUD mutation handlers.
|
|
18
|
-
*/
|
|
19
|
-
export function publish{{pascalCase name}}(event, data, roomId = 'default') {
|
|
20
|
-
const room = rooms.get(roomId)
|
|
21
|
-
if (!room) return
|
|
22
|
-
const message = JSON.stringify({ channel: '{{camelCase name}}', room: roomId, event, data })
|
|
23
|
-
for (const ws of room) {
|
|
24
|
-
try { ws.send(message) } catch { room.delete(ws) }
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Broadcast to all rooms of '{{name}}'.
|
|
30
|
-
*/
|
|
31
|
-
export function broadcast{{pascalCase name}}(event, data) {
|
|
32
|
-
const message = JSON.stringify({ channel: '{{camelCase name}}', event, data })
|
|
33
|
-
for (const [, room] of rooms) {
|
|
34
|
-
for (const ws of room) {
|
|
35
|
-
try { ws.send(message) } catch { room.delete(ws) }
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export const {{camelCase name}}Channel = new Elysia()
|
|
41
|
-
{{#if ../hasAuth}}
|
|
42
|
-
.use(jwt({ name: 'jwt', secret: process.env.JWT_SECRET || 'vasp-dev-secret' }))
|
|
43
|
-
{{/if}}
|
|
44
|
-
.ws('/ws/{{camelCase name}}', {
|
|
45
|
-
{{#if ../hasAuth}}
|
|
46
|
-
async beforeHandle({ jwt: jwtPlugin, request }) {
|
|
47
|
-
const url = new URL(request.url)
|
|
48
|
-
const token = url.searchParams.get('token')
|
|
49
|
-
if (!token) return new Response('Unauthorized', { status: 401 })
|
|
50
|
-
const payload = await jwtPlugin.verify(token)
|
|
51
|
-
if (!payload) return new Response('Unauthorized', { status: 401 })
|
|
52
|
-
},
|
|
53
|
-
{{/if}}
|
|
54
|
-
open(ws) {
|
|
55
|
-
const room = ws.data?.query?.room ?? 'default'
|
|
56
|
-
getRoom(room).add(ws)
|
|
57
|
-
ws.data._room = room
|
|
58
|
-
},
|
|
59
|
-
message(ws, msg) {
|
|
60
|
-
// Client can switch rooms via { action: 'join', room: 'roomId' }
|
|
61
|
-
try {
|
|
62
|
-
const parsed = typeof msg === 'string' ? JSON.parse(msg) : msg
|
|
63
|
-
if (parsed.action === 'join' && parsed.room) {
|
|
64
|
-
const oldRoom = ws.data._room
|
|
65
|
-
if (oldRoom) rooms.get(oldRoom)?.delete(ws)
|
|
66
|
-
ws.data._room = parsed.room
|
|
67
|
-
getRoom(parsed.room).add(ws)
|
|
68
|
-
ws.send(JSON.stringify({ ack: 'joined', room: parsed.room }))
|
|
69
|
-
return
|
|
70
|
-
}
|
|
71
|
-
} catch { /* ignore parse errors */ }
|
|
72
|
-
ws.send(JSON.stringify({ ack: msg }))
|
|
73
|
-
},
|
|
74
|
-
close(ws) {
|
|
75
|
-
const room = ws.data?._room
|
|
76
|
-
if (room) rooms.get(room)?.delete(ws)
|
|
77
|
-
},
|
|
78
|
-
})
|
|
@@ -1,21 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
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>
|
|
@@ -1,41 +0,0 @@
|
|
|
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
|