vasp-cli 0.4.0 → 1.0.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/README.md +57 -2
- package/dist/vasp +201 -35
- 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 -3
- package/templates/shared/server/db/client.hbs +19 -1
- package/templates/shared/server/db/seed.hbs +16 -0
- package/templates/shared/server/index.hbs +47 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vasp-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "The Vasp CLI — declarative full-stack framework for Vue developers",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"README.md"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"build": "bun build ./bin/vasp.ts --target=bun --outfile=dist/vasp --minify --banner='#!/usr/bin/env bun' && chmod +x dist/vasp && cp -
|
|
17
|
+
"build": "rm -rf dist templates starters && bun build ./bin/vasp.ts --target=bun --outfile=dist/vasp --minify --banner='#!/usr/bin/env bun' && chmod +x dist/vasp && mkdir -p templates starters && cp -R ../../templates/. ./templates && cp -R ../../templates/starters/. ./starters",
|
|
18
18
|
"test": "vitest run"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
package/starters/minimal.vasp
CHANGED
package/starters/recipe.vasp
CHANGED
|
@@ -10,23 +10,13 @@ auth RecipeAuth {
|
|
|
10
10
|
methods: [usernameAndPassword]
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
entity User {
|
|
14
|
-
id: Int @id
|
|
15
|
-
username: String @unique
|
|
16
|
-
password: String
|
|
17
|
-
createdAt: DateTime @default(now)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
13
|
entity Recipe {
|
|
21
14
|
id: Int @id
|
|
22
15
|
title: String
|
|
23
16
|
description: String
|
|
24
17
|
ingredients: String
|
|
25
18
|
instructions: String
|
|
26
|
-
|
|
27
|
-
servings: Int
|
|
28
|
-
imageUrl: String
|
|
29
|
-
createdAt: DateTime @default(now)
|
|
19
|
+
author: String
|
|
30
20
|
}
|
|
31
21
|
|
|
32
22
|
route HomeRoute {
|
|
@@ -40,30 +30,26 @@ route RecipesRoute {
|
|
|
40
30
|
}
|
|
41
31
|
|
|
42
32
|
route AddRecipeRoute {
|
|
43
|
-
path: "/recipes/
|
|
33
|
+
path: "/recipes/new"
|
|
44
34
|
to: AddRecipePage
|
|
45
35
|
}
|
|
46
36
|
|
|
47
37
|
page HomePage {
|
|
48
|
-
component: import
|
|
38
|
+
component: import HomePage from "@src/pages/HomePage.vue"
|
|
49
39
|
}
|
|
50
40
|
|
|
51
41
|
page RecipesPage {
|
|
52
|
-
component: import
|
|
42
|
+
component: import RecipesPage from "@src/pages/RecipesPage.vue"
|
|
53
43
|
}
|
|
54
44
|
|
|
55
45
|
page AddRecipePage {
|
|
56
|
-
component: import
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
crud Recipe {
|
|
60
|
-
entity: Recipe
|
|
61
|
-
operations: [list, create, update, delete]
|
|
46
|
+
component: import AddRecipePage from "@src/pages/AddRecipePage.vue"
|
|
62
47
|
}
|
|
63
48
|
|
|
64
49
|
query getRecipes {
|
|
65
50
|
fn: import { getRecipes } from "@src/queries.js"
|
|
66
51
|
entities: [Recipe]
|
|
52
|
+
auth: true
|
|
67
53
|
}
|
|
68
54
|
|
|
69
55
|
action createRecipe {
|
|
@@ -77,3 +63,8 @@ action deleteRecipe {
|
|
|
77
63
|
entities: [Recipe]
|
|
78
64
|
auth: true
|
|
79
65
|
}
|
|
66
|
+
|
|
67
|
+
crud Recipe {
|
|
68
|
+
entity: Recipe
|
|
69
|
+
operations: [list, create, update, delete]
|
|
70
|
+
}
|
|
@@ -1,52 +1,65 @@
|
|
|
1
|
-
app
|
|
2
|
-
title: "Todo App
|
|
1
|
+
app TodoAuthSsrApp {
|
|
2
|
+
title: "Todo App"
|
|
3
3
|
db: Drizzle
|
|
4
4
|
ssr: true
|
|
5
|
-
typescript:
|
|
5
|
+
typescript: false
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
auth
|
|
8
|
+
auth TodoAuth {
|
|
9
9
|
userEntity: User
|
|
10
10
|
methods: [usernameAndPassword]
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
entity User {
|
|
14
|
+
id: Int @id
|
|
15
|
+
username: String @unique
|
|
16
|
+
email: String @unique
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
entity Todo {
|
|
20
|
+
id: Int @id
|
|
21
|
+
title: String
|
|
22
|
+
done: Boolean
|
|
23
|
+
createdAt: DateTime @default(now)
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
route HomeRoute {
|
|
27
|
+
path: "/"
|
|
28
|
+
to: HomePage
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
route TodoRoute {
|
|
32
|
+
path: "/todos"
|
|
33
|
+
to: TodoPage
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
page HomePage {
|
|
37
|
+
component: import Home from "@src/pages/Home.vue"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
page TodoPage {
|
|
41
|
+
component: import TodoList from "@src/pages/TodoList.vue"
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
query getTodos {
|
|
37
|
-
fn: import { getTodos } from "@src/queries.
|
|
45
|
+
fn: import { getTodos } from "@src/queries.js"
|
|
38
46
|
entities: [Todo]
|
|
39
47
|
auth: true
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
action createTodo {
|
|
43
|
-
fn: import { createTodo } from "@src/actions.
|
|
51
|
+
fn: import { createTodo } from "@src/actions.js"
|
|
44
52
|
entities: [Todo]
|
|
45
53
|
auth: true
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
action deleteTodo {
|
|
49
|
-
fn: import { deleteTodo } from "@src/actions.
|
|
57
|
+
fn: import { deleteTodo } from "@src/actions.js"
|
|
50
58
|
entities: [Todo]
|
|
51
59
|
auth: true
|
|
52
60
|
}
|
|
61
|
+
|
|
62
|
+
crud Todo {
|
|
63
|
+
entity: Todo
|
|
64
|
+
operations: [list, create, update, delete]
|
|
65
|
+
}
|
package/starters/todo.vasp
CHANGED
|
@@ -5,18 +5,20 @@ app TodoApp {
|
|
|
5
5
|
typescript: false
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
entity Todo {
|
|
9
|
+
id: Int @id
|
|
10
|
+
title: String
|
|
11
|
+
done: Boolean
|
|
12
|
+
createdAt: DateTime @default(now)
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
route HomeRoute {
|
|
16
|
+
path: "/"
|
|
17
|
+
to: HomePage
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
operations: [list, create, update, delete]
|
|
20
|
+
page HomePage {
|
|
21
|
+
component: import Home from "@src/pages/Home.vue"
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
query getTodos {
|
|
@@ -33,3 +35,8 @@ action deleteTodo {
|
|
|
33
35
|
fn: import { deleteTodo } from "@src/actions.js"
|
|
34
36
|
entities: [Todo]
|
|
35
37
|
}
|
|
38
|
+
|
|
39
|
+
crud Todo {
|
|
40
|
+
entity: Todo
|
|
41
|
+
operations: [list, create, update, delete]
|
|
42
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Elysia } from 'elysia'
|
|
2
|
-
import { jwt } from '@elysiajs/jwt'
|
|
3
|
-
import { cookie } from '@elysiajs/cookie'
|
|
4
2
|
import { db } from '../db/client.{{ext}}'
|
|
5
3
|
import { users } from '../../drizzle/schema.{{ext}}'
|
|
6
4
|
import { eq } from 'drizzle-orm'
|
|
5
|
+
import { authPlugin } from './plugin.{{ext}}'
|
|
6
|
+
import { VaspError } from '../middleware/errorHandler.{{ext}}'
|
|
7
7
|
{{#if (includes authMethods "usernameAndPassword")}}
|
|
8
8
|
import { usernameAndPasswordRoutes } from './providers/usernameAndPassword.{{ext}}'
|
|
9
9
|
{{/if}}
|
|
@@ -14,13 +14,9 @@ import { googleRoutes } from './providers/google.{{ext}}'
|
|
|
14
14
|
import { githubRoutes } from './providers/github.{{ext}}'
|
|
15
15
|
{{/if}}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
export { authPlugin } from './plugin.{{ext}}'
|
|
18
18
|
|
|
19
|
-
export const
|
|
20
|
-
.use(jwt({ name: 'jwt', secret: JWT_SECRET }))
|
|
21
|
-
.use(cookie())
|
|
22
|
-
|
|
23
|
-
export const authRoutes = new Elysia({ prefix: '/auth' })
|
|
19
|
+
export const authRoutes = new Elysia({ prefix: '/api/auth' })
|
|
24
20
|
.use(authPlugin)
|
|
25
21
|
{{#if (includes authMethods "usernameAndPassword")}}
|
|
26
22
|
.use(usernameAndPasswordRoutes)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Elysia } from 'elysia'
|
|
2
|
-
import {
|
|
3
|
-
import { cookie } from '@elysiajs/cookie'
|
|
2
|
+
import { jwtVerify } from 'jose'
|
|
4
3
|
import { db } from '../db/client.{{ext}}'
|
|
5
4
|
import { users } from '../../drizzle/schema.{{ext}}'
|
|
6
5
|
import { eq } from 'drizzle-orm'
|
|
6
|
+
import { VaspError } from '../middleware/errorHandler.{{ext}}'
|
|
7
7
|
|
|
8
|
-
const JWT_SECRET = process.env.JWT_SECRET || 'change-me-in-production'
|
|
8
|
+
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET || 'change-me-in-production')
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* requireAuth — Elysia plugin that verifies the JWT cookie and injects `user` into the context.
|
|
@@ -15,19 +15,37 @@ const JWT_SECRET = process.env.JWT_SECRET || 'change-me-in-production'
|
|
|
15
15
|
* new Elysia().use(requireAuth).get('/protected', ({ user }) => user)
|
|
16
16
|
*/
|
|
17
17
|
export const requireAuth = new Elysia({ name: 'require-auth' })
|
|
18
|
-
.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
.resolve({ as: 'scoped' }, async ({ cookie }) => {
|
|
19
|
+
const tokenValue = cookie?.token?.value ?? ''
|
|
20
|
+
if (!tokenValue) return { user: null }
|
|
21
|
+
try {
|
|
22
|
+
const { payload } = await jwtVerify(tokenValue, JWT_SECRET)
|
|
23
|
+
if (!payload || typeof payload.userId !== 'number') {
|
|
24
|
+
return { user: null }
|
|
25
|
+
}
|
|
26
|
+
const [user] = await db.select().from(users).where(eq(users.id, payload.userId)).limit(1)
|
|
27
|
+
if (!user) {
|
|
28
|
+
return { user: null }
|
|
29
|
+
}
|
|
30
|
+
const { passwordHash: _ph, ...safeUser } = user
|
|
31
|
+
return { user: safeUser }
|
|
32
|
+
} catch {
|
|
33
|
+
return { user: null }
|
|
25
34
|
}
|
|
26
|
-
|
|
35
|
+
})
|
|
36
|
+
.onBeforeHandle({ as: 'scoped' }, ({ user }) => {
|
|
27
37
|
if (!user) {
|
|
28
|
-
|
|
29
|
-
throw new Error('User not found')
|
|
38
|
+
throw new VaspError('AUTH_REQUIRED', 'Authentication required', 401)
|
|
30
39
|
}
|
|
31
|
-
const { passwordHash: _ph, ...safeUser } = user
|
|
32
|
-
return { user: safeUser }
|
|
33
40
|
})
|
|
41
|
+
|
|
42
|
+
export function requireRole(roles{{#if isTypeScript}}: string[]{{/if}}) {
|
|
43
|
+
return new Elysia({ name: 'require-role' })
|
|
44
|
+
.use(requireAuth)
|
|
45
|
+
.onBeforeHandle({ as: 'scoped' }, ({ user }) => {
|
|
46
|
+
const userRole = typeof user?.role === 'string' ? user.role : ''
|
|
47
|
+
if (!roles.includes(userRole)) {
|
|
48
|
+
throw new VaspError('AUTH_FORBIDDEN', 'Insufficient permissions', 403)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { Elysia } from 'elysia'
|
|
2
2
|
import { jwt } from '@elysiajs/jwt'
|
|
3
|
-
import { cookie } from '@elysiajs/cookie'
|
|
4
3
|
|
|
5
4
|
const JWT_SECRET = process.env.JWT_SECRET || 'change-me-in-production'
|
|
6
5
|
|
|
7
6
|
export const authPlugin = new Elysia({ name: 'auth-plugin' })
|
|
8
7
|
.use(jwt({ name: 'jwt', secret: JWT_SECRET }))
|
|
9
|
-
.use(cookie())
|
|
@@ -2,7 +2,7 @@ import { Elysia } from 'elysia'
|
|
|
2
2
|
import { db } from '../../db/client.{{ext}}'
|
|
3
3
|
import { users } from '../../../drizzle/schema.{{ext}}'
|
|
4
4
|
import { eq } from 'drizzle-orm'
|
|
5
|
-
import { authPlugin } from '../
|
|
5
|
+
import { authPlugin } from '../plugin.{{ext}}'
|
|
6
6
|
|
|
7
7
|
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || ''
|
|
8
8
|
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET || ''
|
|
@@ -2,7 +2,7 @@ import { Elysia } from 'elysia'
|
|
|
2
2
|
import { db } from '../../db/client.{{ext}}'
|
|
3
3
|
import { users } from '../../../drizzle/schema.{{ext}}'
|
|
4
4
|
import { eq } from 'drizzle-orm'
|
|
5
|
-
import { authPlugin } from '../
|
|
5
|
+
import { authPlugin } from '../plugin.{{ext}}'
|
|
6
6
|
|
|
7
7
|
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || ''
|
|
8
8
|
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || ''
|
|
@@ -2,17 +2,14 @@ import { Elysia, t } from 'elysia'
|
|
|
2
2
|
import { db } from '../../db/client.{{ext}}'
|
|
3
3
|
import { users } from '../../../drizzle/schema.{{ext}}'
|
|
4
4
|
import { eq } from 'drizzle-orm'
|
|
5
|
-
import { authPlugin } from '../
|
|
5
|
+
import { authPlugin } from '../plugin.{{ext}}'
|
|
6
6
|
|
|
7
7
|
async function hashPassword(password{{#if isTypeScript}}: string{{/if}}) {
|
|
8
|
-
|
|
9
|
-
const data = encoder.encode(password)
|
|
10
|
-
const hash = await crypto.subtle.digest('SHA-256', data)
|
|
11
|
-
return Buffer.from(hash).toString('hex')
|
|
8
|
+
return Bun.password.hash(password, 'argon2id')
|
|
12
9
|
}
|
|
13
10
|
|
|
14
11
|
async function verifyPassword(password{{#if isTypeScript}}: string{{/if}}, hash{{#if isTypeScript}}: string{{/if}}) {
|
|
15
|
-
return (
|
|
12
|
+
return Bun.password.verify(password, hash)
|
|
16
13
|
}
|
|
17
14
|
|
|
18
15
|
export const usernameAndPasswordRoutes = new Elysia()
|
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
import { pgTable,
|
|
1
|
+
import { pgTable, text, integer, boolean, timestamp, doublePrecision, jsonb{{#if hasAnyRelations}}, relations{{/if}}{{#if hasEnums}}, pgEnum{{/if}} } from 'drizzle-orm/pg-core'
|
|
2
2
|
{{#if isTypeScript}}
|
|
3
3
|
import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'
|
|
4
4
|
{{/if}}
|
|
5
5
|
|
|
6
|
+
{{#if hasEnums}}
|
|
7
|
+
{{#each enumDeclarations}}
|
|
8
|
+
export const {{fnName}} = pgEnum('{{dbName}}', [{{#each values}}'{{this}}'{{#unless @last}}, {{/unless}}{{/each}}])
|
|
9
|
+
{{/each}}
|
|
10
|
+
{{/if}}
|
|
6
11
|
{{#if hasAuth}}
|
|
7
12
|
// Users table — generated by Vasp auth system
|
|
8
13
|
export const users = pgTable('users', {
|
|
9
|
-
id:
|
|
14
|
+
id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
|
|
10
15
|
username: text('username').notNull().unique(),
|
|
11
16
|
email: text('email').unique(),
|
|
12
17
|
passwordHash: text('password_hash'),
|
|
13
18
|
googleId: text('google_id').unique(),
|
|
14
19
|
githubId: text('github_id').unique(),
|
|
20
|
+
{{#each authUserExtraFields}}
|
|
21
|
+
{{camelCase name}}: {{{drizzleColumn name type modifiers nullable defaultValue isUpdatedAt}}},
|
|
22
|
+
{{/each}}
|
|
15
23
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
16
24
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
17
25
|
})
|
|
@@ -21,28 +29,39 @@ export type NewUser = InferInsertModel<typeof users>
|
|
|
21
29
|
{{/if}}
|
|
22
30
|
|
|
23
31
|
{{/if}}
|
|
24
|
-
{{#each
|
|
25
|
-
{{
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
{{#
|
|
29
|
-
{{camelCase name}}: {{
|
|
30
|
-
{{/each}}
|
|
31
|
-
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
32
|
-
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
33
|
-
})
|
|
32
|
+
{{#each entitiesWithSchema}}
|
|
33
|
+
// {{name}} table — generated from entity block
|
|
34
|
+
export const {{camelCase name}}s = pgTable('{{camelCase name}}s', {
|
|
35
|
+
{{#each scalarFields}}
|
|
36
|
+
{{#if isForeignKey}}
|
|
37
|
+
{{camelCase name}}: integer('{{camelCase name}}').notNull().references(() => {{referencedTable}}.id, { onDelete: '{{onDelete}}' }),
|
|
34
38
|
{{else}}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
{{#if isEnum}}
|
|
40
|
+
{{camelCase name}}: {{enumFnName}}('{{camelCase name}}'){{#unless nullable}}.notNull(){{/unless}},
|
|
41
|
+
{{else}}
|
|
42
|
+
{{camelCase name}}: {{{drizzleColumn name type modifiers nullable defaultValue isUpdatedAt}}},
|
|
43
|
+
{{/if}}
|
|
44
|
+
{{/if}}
|
|
45
|
+
{{/each}}
|
|
39
46
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
40
47
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
41
48
|
})
|
|
42
|
-
{{/if}}
|
|
43
49
|
{{#if ../isTypeScript}}
|
|
44
|
-
export type {{pascalCase
|
|
45
|
-
export type New{{pascalCase
|
|
50
|
+
export type {{pascalCase name}} = InferSelectModel<typeof {{camelCase name}}s>
|
|
51
|
+
export type New{{pascalCase name}} = InferInsertModel<typeof {{camelCase name}}s>
|
|
46
52
|
{{/if}}
|
|
47
53
|
|
|
48
54
|
{{/each}}
|
|
55
|
+
{{#each entitiesWithSchema}}
|
|
56
|
+
{{#if hasRelations}}
|
|
57
|
+
export const {{camelCase name}}sRelations = relations({{camelCase name}}s, ({ one, many }) => ({
|
|
58
|
+
{{#each manyToOne}}
|
|
59
|
+
{{camelCase name}}: one({{relatedTable}}, { fields: [{{camelCase ../name}}s.{{camelCase localField}}], references: [{{relatedTable}}.id] }),
|
|
60
|
+
{{/each}}
|
|
61
|
+
{{#each oneToMany}}
|
|
62
|
+
{{camelCase fieldName}}: many({{relatedTable}}),
|
|
63
|
+
{{/each}}
|
|
64
|
+
}))
|
|
65
|
+
|
|
66
|
+
{{/if}}
|
|
67
|
+
{{/each}}
|
|
@@ -6,17 +6,24 @@
|
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "vasp start",
|
|
8
8
|
"build": "vasp build",
|
|
9
|
+
"test": "vitest run",
|
|
9
10
|
"dev:server": "bun --hot server/index.{{ext}}",
|
|
10
11
|
"dev:client": "{{#if isSpa}}vite{{else}}nuxt dev{{/if}}",
|
|
11
12
|
"db:generate": "bunx drizzle-kit generate",
|
|
12
13
|
"db:migrate": "bunx drizzle-kit migrate",
|
|
13
|
-
"db:
|
|
14
|
+
"db:push": "bunx drizzle-kit push",
|
|
15
|
+
"db:studio": "bunx drizzle-kit studio"{{#if seed}},
|
|
16
|
+
"db:seed": "bun server/db/seed.{{ext}}"{{/if}}
|
|
14
17
|
},
|
|
15
18
|
"dependencies": {
|
|
16
|
-
"@vasp-framework/runtime": "^
|
|
19
|
+
"@vasp-framework/runtime": "^1.0.0",
|
|
17
20
|
"elysia": "^1.1.0",
|
|
18
21
|
"@elysiajs/cors": "^1.1.0",
|
|
19
22
|
"@elysiajs/static": "^1.1.0",
|
|
23
|
+
"@elysiajs/swagger": "^1.1.0",{{#if auth}}
|
|
24
|
+
"@elysiajs/jwt": "^1.1.0",
|
|
25
|
+
"jose": "^5.0.0",{{/if}}
|
|
26
|
+
"valibot": "^1.0.0",
|
|
20
27
|
"drizzle-orm": "^0.36.0",
|
|
21
28
|
"postgres": "^3.4.0",
|
|
22
29
|
"ofetch": "^1.3.4",
|
|
@@ -26,7 +33,8 @@
|
|
|
26
33
|
"pg-boss": "^10.0.0"{{/if}}
|
|
27
34
|
},
|
|
28
35
|
"devDependencies": {
|
|
29
|
-
"drizzle-kit": "^0.28.0"
|
|
36
|
+
"drizzle-kit": "^0.28.0",
|
|
37
|
+
"vitest": "^2.1.9"{{#if isSpa}},
|
|
30
38
|
"@vitejs/plugin-vue": "^5.2.0",
|
|
31
39
|
"vite": "^6.0.0"{{/if}}{{#if isTypeScript}},
|
|
32
40
|
"typescript": "^5.6.0",
|
|
@@ -5,8 +5,26 @@ import * as schema from '../../drizzle/schema.{{ext}}'
|
|
|
5
5
|
const connectionString = process.env.DATABASE_URL
|
|
6
6
|
|
|
7
7
|
if (!connectionString) {
|
|
8
|
-
|
|
8
|
+
console.error('\n✗ DATABASE_URL environment variable is required. Set it in .env\n')
|
|
9
|
+
process.exit(1)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (connectionString.startsWith('postgres://user:password@localhost')) {
|
|
13
|
+
console.warn(
|
|
14
|
+
'\n⚠ DATABASE_URL looks like a placeholder. Edit .env to set your real database credentials.\n'
|
|
15
|
+
)
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
const client = postgres(connectionString)
|
|
19
|
+
|
|
20
|
+
// Verify database connectivity at startup
|
|
21
|
+
try {
|
|
22
|
+
await client`SELECT 1`
|
|
23
|
+
} catch (err) {
|
|
24
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
25
|
+
console.error('\n✗ Cannot connect to database. Verify DATABASE_URL in .env')
|
|
26
|
+
console.error(` ${message}\n`)
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
export const db = drizzle(client, { schema })
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { db } from './client.{{ext}}'
|
|
2
|
+
{{#if (eq seedImportKind "default")}}
|
|
3
|
+
import {{seedImportName}} from '{{importPath fnSource ext}}'
|
|
4
|
+
{{else}}
|
|
5
|
+
import { {{seedImportName}} } from '{{importPath fnSource ext}}'
|
|
6
|
+
{{/if}}
|
|
7
|
+
|
|
8
|
+
async function runSeed() {
|
|
9
|
+
await {{seedImportName}}({ db })
|
|
10
|
+
console.log('✅ Seed completed')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
runSeed().catch((error) => {
|
|
14
|
+
console.error('❌ Seed failed', error)
|
|
15
|
+
process.exit(1)
|
|
16
|
+
})
|
|
@@ -1,20 +1,36 @@
|
|
|
1
1
|
import { Elysia } from 'elysia'
|
|
2
2
|
import { cors } from '@elysiajs/cors'
|
|
3
3
|
import { staticPlugin } from '@elysiajs/static'
|
|
4
|
+
import { swagger } from '@elysiajs/swagger'
|
|
4
5
|
import { db } from './db/client.{{ext}}'
|
|
6
|
+
import { logger } from './middleware/logger.{{ext}}'
|
|
5
7
|
import { rateLimit } from './middleware/rateLimit.{{ext}}'
|
|
8
|
+
import { errorHandler } from './middleware/errorHandler.{{ext}}'
|
|
9
|
+
import { vaspDiagnosticRoutes } from './routes/_vasp.{{ext}}'
|
|
6
10
|
{{#if isSsr}}
|
|
7
11
|
import { csrfProtection } from './middleware/csrf.{{ext}}'
|
|
8
12
|
{{/if}}
|
|
9
13
|
{{#if hasAuth}}
|
|
10
14
|
import { authRoutes } from './auth/index.{{ext}}'
|
|
11
15
|
{{/if}}
|
|
16
|
+
{{#each middlewares}}
|
|
17
|
+
{{#if (eq scope "global")}}
|
|
18
|
+
{{#if (eq fn.kind "default")}}
|
|
19
|
+
import {{importAlias}} from '{{importPath fnSource ../ext}}'
|
|
20
|
+
{{else}}
|
|
21
|
+
import { {{importName fn}} as {{importAlias}} } from '{{importPath fnSource ../ext}}'
|
|
22
|
+
{{/if}}
|
|
23
|
+
{{/if}}
|
|
24
|
+
{{/each}}
|
|
12
25
|
{{#each queries}}
|
|
13
26
|
import { {{camelCase name}}Route } from './routes/queries/{{camelCase name}}.{{../ext}}'
|
|
14
27
|
{{/each}}
|
|
15
28
|
{{#each actions}}
|
|
16
29
|
import { {{camelCase name}}Route } from './routes/actions/{{camelCase name}}.{{../ext}}'
|
|
17
30
|
{{/each}}
|
|
31
|
+
{{#each apis}}
|
|
32
|
+
import { {{camelCase name}}ApiRoute } from './routes/api/{{camelCase name}}.{{../ext}}'
|
|
33
|
+
{{/each}}
|
|
18
34
|
{{#each cruds}}
|
|
19
35
|
import { {{camelCase entity}}CrudRoutes } from './routes/crud/{{camelCase entity}}.{{../ext}}'
|
|
20
36
|
{{/each}}
|
|
@@ -27,7 +43,29 @@ import { {{camelCase name}}ScheduleRoute } from './routes/jobs/{{camelCase name}
|
|
|
27
43
|
|
|
28
44
|
const PORT = Number(process.env.PORT) || {{backendPort}}
|
|
29
45
|
|
|
46
|
+
const REQUIRED_ENV_VARS = [{{#each requiredEnvVars}}'{{this}}'{{#unless @last}}, {{/unless}}{{/each}}]
|
|
47
|
+
const missingEnvVars = REQUIRED_ENV_VARS.filter((name) => {
|
|
48
|
+
const value = process.env[name]
|
|
49
|
+
return typeof value !== 'string' || value.trim() === ''
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (missingEnvVars.length > 0) {
|
|
53
|
+
console.error('❌ Missing required environment variables:')
|
|
54
|
+
for (const key of missingEnvVars) {
|
|
55
|
+
console.error(` - ${key}`)
|
|
56
|
+
}
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
59
|
+
|
|
30
60
|
const app = new Elysia()
|
|
61
|
+
.use(swagger({
|
|
62
|
+
path: '/api/docs',
|
|
63
|
+
documentation: {
|
|
64
|
+
info: { title: '{{appName}} API', version: '1.0.0' },
|
|
65
|
+
},
|
|
66
|
+
}))
|
|
67
|
+
.use(logger())
|
|
68
|
+
.use(errorHandler())
|
|
31
69
|
.use(cors({
|
|
32
70
|
origin: process.env.CORS_ORIGIN || 'http://localhost:{{frontendPort}}',
|
|
33
71
|
credentials: true,
|
|
@@ -37,15 +75,24 @@ const app = new Elysia()
|
|
|
37
75
|
.use(csrfProtection())
|
|
38
76
|
{{/if}}
|
|
39
77
|
.get('/api/health', () => ({ status: 'ok', version: '{{vaspVersion}}' }))
|
|
78
|
+
.use(vaspDiagnosticRoutes)
|
|
40
79
|
{{#if hasAuth}}
|
|
41
80
|
.use(authRoutes)
|
|
42
81
|
{{/if}}
|
|
82
|
+
{{#each middlewares}}
|
|
83
|
+
{{#if (eq scope "global")}}
|
|
84
|
+
.use({{importAlias}})
|
|
85
|
+
{{/if}}
|
|
86
|
+
{{/each}}
|
|
43
87
|
{{#each queries}}
|
|
44
88
|
.use({{camelCase name}}Route)
|
|
45
89
|
{{/each}}
|
|
46
90
|
{{#each actions}}
|
|
47
91
|
.use({{camelCase name}}Route)
|
|
48
92
|
{{/each}}
|
|
93
|
+
{{#each apis}}
|
|
94
|
+
.use({{camelCase name}}ApiRoute)
|
|
95
|
+
{{/each}}
|
|
49
96
|
{{#each cruds}}
|
|
50
97
|
.use({{camelCase entity}}CrudRoutes)
|
|
51
98
|
{{/each}}
|