vasp-cli 0.3.1 → 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 +80 -31
- package/package.json +2 -2
- package/starters/minimal.vasp +1 -1
- package/starters/recipe.vasp +70 -0
- package/starters/todo-auth-ssr.vasp +33 -20
- package/starters/todo.vasp +15 -8
- package/templates/shared/.gitignore.hbs +1 -0
- package/templates/shared/README.md.hbs +53 -0
- package/templates/shared/auth/client/Login.vue.hbs +1 -1
- package/templates/shared/auth/client/Register.vue.hbs +1 -1
- package/templates/shared/auth/server/index.hbs +4 -8
- package/templates/shared/auth/server/middleware.hbs +33 -15
- package/templates/shared/auth/server/plugin.hbs +7 -0
- 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 +39 -9
- package/templates/shared/jobs/_job.hbs +12 -2
- package/templates/shared/package.json.hbs +14 -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 +48 -0
- package/templates/shared/server/middleware/errorHandler.hbs +75 -0
- package/templates/shared/server/middleware/logger.hbs +74 -0
- package/templates/{templates/shared → 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 +103 -11
- package/templates/shared/server/routes/queries/_query.hbs +5 -1
- package/templates/shared/server/routes/realtime/_channel.hbs +58 -10
- 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/js/plugins/vasp.client.js.hbs +11 -1
- package/templates/ssr/ts/error.vue.hbs +26 -0
- package/templates/ssr/ts/nuxt.config.ts.hbs +1 -0
- package/templates/ssr/ts/plugins/vasp.client.ts.hbs +11 -1
- 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/.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 -51
- 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 -69
- package/templates/templates/shared/bunfig.toml.hbs +0 -2
- 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 -35
- package/templates/templates/shared/server/db/client.hbs +0 -12
- package/templates/templates/shared/server/index.hbs +0 -60
- 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/.env.example.hbs → shared/.env.hbs} +0 -0
- /package/templates/{templates/shared → shared}/drizzle/drizzle.config.hbs +0 -0
- /package/templates/{templates/shared → shared}/server/middleware/csrf.hbs +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vasp-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
app RecipeApp {
|
|
2
|
+
title: "Recipe App"
|
|
3
|
+
db: Drizzle
|
|
4
|
+
ssr: false
|
|
5
|
+
typescript: false
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
auth RecipeAuth {
|
|
9
|
+
userEntity: User
|
|
10
|
+
methods: [usernameAndPassword]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
entity Recipe {
|
|
14
|
+
id: Int @id
|
|
15
|
+
title: String
|
|
16
|
+
description: String
|
|
17
|
+
ingredients: String
|
|
18
|
+
instructions: String
|
|
19
|
+
author: String
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
route HomeRoute {
|
|
23
|
+
path: "/"
|
|
24
|
+
to: HomePage
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
route RecipesRoute {
|
|
28
|
+
path: "/recipes"
|
|
29
|
+
to: RecipesPage
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
route AddRecipeRoute {
|
|
33
|
+
path: "/recipes/new"
|
|
34
|
+
to: AddRecipePage
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
page HomePage {
|
|
38
|
+
component: import HomePage from "@src/pages/HomePage.vue"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
page RecipesPage {
|
|
42
|
+
component: import RecipesPage from "@src/pages/RecipesPage.vue"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
page AddRecipePage {
|
|
46
|
+
component: import AddRecipePage from "@src/pages/AddRecipePage.vue"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
query getRecipes {
|
|
50
|
+
fn: import { getRecipes } from "@src/queries.js"
|
|
51
|
+
entities: [Recipe]
|
|
52
|
+
auth: true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
action createRecipe {
|
|
56
|
+
fn: import { createRecipe } from "@src/actions.js"
|
|
57
|
+
entities: [Recipe]
|
|
58
|
+
auth: true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
action deleteRecipe {
|
|
62
|
+
fn: import { deleteRecipe } from "@src/actions.js"
|
|
63
|
+
entities: [Recipe]
|
|
64
|
+
auth: true
|
|
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
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# {{appTitle}}
|
|
2
|
+
|
|
3
|
+
A full-stack app built with [Vasp](https://github.com/AliBeigi/Vasp).
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install dependencies
|
|
9
|
+
bun install
|
|
10
|
+
|
|
11
|
+
# Set up your database
|
|
12
|
+
# Make sure PostgreSQL is running, then push the schema:
|
|
13
|
+
bun run db:push
|
|
14
|
+
|
|
15
|
+
# Start the dev server (backend + frontend)
|
|
16
|
+
vasp start
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Project Structure
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
main.vasp # Vasp declarative config
|
|
23
|
+
server/ # Elysia backend
|
|
24
|
+
routes/ # API routes (queries, actions, CRUD)
|
|
25
|
+
db/ # Drizzle DB client
|
|
26
|
+
middleware/ # Rate limiting{{#if hasAuth}}, auth{{/if}}
|
|
27
|
+
{{#if hasJobs}} jobs/ # Background jobs (PgBoss)
|
|
28
|
+
{{/if}}src/ # Frontend source
|
|
29
|
+
pages/ # Vue page components
|
|
30
|
+
components/ # Shared components
|
|
31
|
+
drizzle/ # Database schema & migrations
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Scripts
|
|
35
|
+
|
|
36
|
+
| Command | Description |
|
|
37
|
+
|---------|-------------|
|
|
38
|
+
| `vasp start` | Start dev server (backend + frontend) |
|
|
39
|
+
| `vasp build` | Production build |
|
|
40
|
+
| `bun run db:push` | Push schema to database |
|
|
41
|
+
| `bun run db:generate` | Generate a migration |
|
|
42
|
+
| `bun run db:migrate` | Run migrations |
|
|
43
|
+
| `bun run db:studio` | Open Drizzle Studio |
|
|
44
|
+
|
|
45
|
+
## Environment Variables
|
|
46
|
+
|
|
47
|
+
Copy `.env.example` to `.env` and update the values:
|
|
48
|
+
|
|
49
|
+
- `DATABASE_URL` — PostgreSQL connection string
|
|
50
|
+
- `PORT` — Backend server port (default: {{backendPort}})
|
|
51
|
+
- `VITE_API_URL` — Frontend API base URL
|
|
52
|
+
{{#if hasAuth}}- `JWT_SECRET` — Secret for JWT token signing
|
|
53
|
+
{{/if}}
|
|
@@ -38,7 +38,7 @@ async function handleLogin() {
|
|
|
38
38
|
await login(username.value, password.value)
|
|
39
39
|
router.push('/')
|
|
40
40
|
} catch (err) {
|
|
41
|
-
error.value = err?.data?.error || 'Login failed'
|
|
41
|
+
error.value = err?.data?.error || err?.message || 'Login failed'
|
|
42
42
|
} finally {
|
|
43
43
|
loading.value = false
|
|
44
44
|
}
|
|
@@ -34,7 +34,7 @@ async function handleRegister() {
|
|
|
34
34
|
await register(username.value, password.value, email.value || undefined)
|
|
35
35
|
router.push('/')
|
|
36
36
|
} catch (err) {
|
|
37
|
-
error.value = err?.data?.error || 'Registration failed'
|
|
37
|
+
error.value = err?.data?.error || err?.message || 'Registration failed'
|
|
38
38
|
} finally {
|
|
39
39
|
loading.value = false
|
|
40
40
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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,17 +29,39 @@ export type NewUser = InferInsertModel<typeof users>
|
|
|
21
29
|
{{/if}}
|
|
22
30
|
|
|
23
31
|
{{/if}}
|
|
24
|
-
{{#each
|
|
25
|
-
// {{
|
|
26
|
-
export const {{camelCase
|
|
27
|
-
|
|
28
|
-
|
|
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}}' }),
|
|
38
|
+
{{else}}
|
|
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}}
|
|
29
46
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
30
47
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
31
48
|
})
|
|
32
49
|
{{#if ../isTypeScript}}
|
|
33
|
-
export type {{pascalCase
|
|
34
|
-
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>
|
|
35
52
|
{{/if}}
|
|
36
53
|
|
|
37
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}}
|
|
@@ -12,13 +12,23 @@ export async function register{{pascalCase name}}Worker() {
|
|
|
12
12
|
await boss.work(JOB_NAME, async (job) => {
|
|
13
13
|
await {{namedExport}}(job.data)
|
|
14
14
|
})
|
|
15
|
+
{{#if hasSchedule}}
|
|
16
|
+
|
|
17
|
+
// Register cron schedule: {{schedule}}
|
|
18
|
+
await boss.schedule(JOB_NAME, '{{schedule}}', {})
|
|
19
|
+
{{/if}}
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
/**
|
|
18
|
-
* Schedule a '{{name}}' job.
|
|
23
|
+
* Schedule a '{{name}}' job on demand.
|
|
19
24
|
* @param {unknown} data - Data to pass to the job handler
|
|
20
25
|
*/
|
|
21
26
|
export async function schedule{{pascalCase name}}(data) {
|
|
22
27
|
const boss = await getBoss()
|
|
23
|
-
return boss.send(JOB_NAME, data
|
|
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
|
+
})
|
|
24
34
|
}
|
|
@@ -6,14 +6,23 @@
|
|
|
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
|
-
"dev:client": "{{#if isSpa}}vite{{else}}nuxt dev{{/if}}"
|
|
11
|
+
"dev:client": "{{#if isSpa}}vite{{else}}nuxt dev{{/if}}",
|
|
12
|
+
"db:generate": "bunx drizzle-kit generate",
|
|
13
|
+
"db:migrate": "bunx drizzle-kit migrate",
|
|
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}}
|
|
11
17
|
},
|
|
12
18
|
"dependencies": {
|
|
13
|
-
"@vasp-framework/runtime": "^0.
|
|
19
|
+
"@vasp-framework/runtime": "^0.4.0",
|
|
14
20
|
"elysia": "^1.1.0",
|
|
15
21
|
"@elysiajs/cors": "^1.1.0",
|
|
16
|
-
"@elysiajs/static": "^1.1.0",
|
|
22
|
+
"@elysiajs/static": "^1.1.0",{{#if auth}}
|
|
23
|
+
"@elysiajs/jwt": "^1.1.0",
|
|
24
|
+
"jose": "^5.0.0",{{/if}}
|
|
25
|
+
"valibot": "^1.0.0",
|
|
17
26
|
"drizzle-orm": "^0.36.0",
|
|
18
27
|
"postgres": "^3.4.0",
|
|
19
28
|
"ofetch": "^1.3.4",
|
|
@@ -23,7 +32,8 @@
|
|
|
23
32
|
"pg-boss": "^10.0.0"{{/if}}
|
|
24
33
|
},
|
|
25
34
|
"devDependencies": {
|
|
26
|
-
"drizzle-kit": "^0.28.0"
|
|
35
|
+
"drizzle-kit": "^0.28.0",
|
|
36
|
+
"vitest": "^2.1.9"{{#if isSpa}},
|
|
27
37
|
"@vitejs/plugin-vue": "^5.2.0",
|
|
28
38
|
"vite": "^6.0.0"{{/if}}{{#if isTypeScript}},
|
|
29
39
|
"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
|
+
})
|