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
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type VaspNotification = {
|
|
4
|
+
id: number
|
|
5
|
+
type: 'error' | 'success' | 'info'
|
|
6
|
+
message: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const notifications = ref<VaspNotification[]>([])
|
|
10
|
+
|
|
11
|
+
export function pushNotification(type: VaspNotification['type'], message: string, timeoutMs = 3500): number {
|
|
12
|
+
const id = Date.now() + Math.floor(Math.random() * 1000)
|
|
13
|
+
notifications.value.push({ id, type, message })
|
|
14
|
+
|
|
15
|
+
if (timeoutMs > 0) {
|
|
16
|
+
setTimeout(() => removeNotification(id), timeoutMs)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return id
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function removeNotification(id: number): void {
|
|
23
|
+
notifications.value = notifications.value.filter((item) => item.id !== id)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function notifyError(error: unknown): void {
|
|
27
|
+
if (error instanceof Error && error.message) {
|
|
28
|
+
pushNotification('error', error.message)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
pushNotification('error', 'Request failed')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useVaspNotifications() {
|
|
35
|
+
return {
|
|
36
|
+
notifications,
|
|
37
|
+
pushNotification,
|
|
38
|
+
removeNotification,
|
|
39
|
+
notifyError,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -7,6 +7,7 @@ export default defineConfig({
|
|
|
7
7
|
resolve: {
|
|
8
8
|
alias: {
|
|
9
9
|
'@src': fileURLToPath(new URL('./src', import.meta.url)),
|
|
10
|
+
'@shared': fileURLToPath(new URL('./shared', import.meta.url)),
|
|
10
11
|
'@vasp-framework/client': fileURLToPath(new URL('./src/vasp/client', import.meta.url)),
|
|
11
12
|
},
|
|
12
13
|
},
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
error: {
|
|
6
|
+
type: Object,
|
|
7
|
+
required: true,
|
|
8
|
+
},
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const messageText = computed(() => props.error.statusMessage || props.error.message || 'Unexpected server error')
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div style="padding:24px; max-width:720px; margin:0 auto;">
|
|
16
|
+
<h1 v-if="props.error.statusCode === 404">Page not found</h1>
|
|
17
|
+
<h1 v-else-if="props.error.statusCode === 401">Authentication required</h1>
|
|
18
|
+
<h1 v-else>Something went wrong</h1>
|
|
19
|
+
|
|
20
|
+
<p>{{ messageText }}</p>
|
|
21
|
+
<NuxtLink to="/">Go back home</NuxtLink>
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
error: {
|
|
6
|
+
statusCode?: number
|
|
7
|
+
statusMessage?: string
|
|
8
|
+
message?: string
|
|
9
|
+
}
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
const messageText = computed(() => {
|
|
13
|
+
return props.error.statusMessage ?? props.error.message ?? 'Unexpected server error'
|
|
14
|
+
})
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<div style="padding:24px; max-width:720px; margin:0 auto;">
|
|
19
|
+
<h1 v-if="props.error.statusCode === 404">Page not found</h1>
|
|
20
|
+
<h1 v-else-if="props.error.statusCode === 401">Authentication required</h1>
|
|
21
|
+
<h1 v-else>Something went wrong</h1>
|
|
22
|
+
|
|
23
|
+
<p>{{ messageText }}</p>
|
|
24
|
+
<NuxtLink to="/">Go back home</NuxtLink>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
app TodoAuthSsrApp {
|
|
2
|
+
title: "Todo App"
|
|
3
|
+
db: Drizzle
|
|
4
|
+
ssr: true
|
|
5
|
+
typescript: false
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
auth TodoAuth {
|
|
9
|
+
userEntity: User
|
|
10
|
+
methods: [usernameAndPassword]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
entity User {
|
|
14
|
+
id: Int @id
|
|
15
|
+
username: String @unique
|
|
16
|
+
email: String @unique
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
entity Todo {
|
|
20
|
+
id: Int @id
|
|
21
|
+
title: String
|
|
22
|
+
done: Boolean
|
|
23
|
+
createdAt: DateTime @default(now)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
route HomeRoute {
|
|
27
|
+
path: "/"
|
|
28
|
+
to: HomePage
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
route TodoRoute {
|
|
32
|
+
path: "/todos"
|
|
33
|
+
to: TodoPage
|
|
34
|
+
}
|
|
35
|
+
|
|
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"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
query getTodos {
|
|
45
|
+
fn: import { getTodos } from "@src/queries.js"
|
|
46
|
+
entities: [Todo]
|
|
47
|
+
auth: true
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
action createTodo {
|
|
51
|
+
fn: import { createTodo } from "@src/actions.js"
|
|
52
|
+
entities: [Todo]
|
|
53
|
+
auth: true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
action deleteTodo {
|
|
57
|
+
fn: import { deleteTodo } from "@src/actions.js"
|
|
58
|
+
entities: [Todo]
|
|
59
|
+
auth: true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
crud Todo {
|
|
63
|
+
entity: Todo
|
|
64
|
+
operations: [list, create, update, delete]
|
|
65
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
app TodoApp {
|
|
2
|
+
title: "Todo App"
|
|
3
|
+
db: Drizzle
|
|
4
|
+
ssr: false
|
|
5
|
+
typescript: false
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
entity Todo {
|
|
9
|
+
id: Int @id
|
|
10
|
+
title: String
|
|
11
|
+
done: Boolean
|
|
12
|
+
createdAt: DateTime @default(now)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
route HomeRoute {
|
|
16
|
+
path: "/"
|
|
17
|
+
to: HomePage
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
page HomePage {
|
|
21
|
+
component: import Home from "@src/pages/Home.vue"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
query getTodos {
|
|
25
|
+
fn: import { getTodos } from "@src/queries.js"
|
|
26
|
+
entities: [Todo]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
action createTodo {
|
|
30
|
+
fn: import { createTodo } from "@src/actions.js"
|
|
31
|
+
entities: [Todo]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
action deleteTodo {
|
|
35
|
+
fn: import { deleteTodo } from "@src/actions.js"
|
|
36
|
+
entities: [Todo]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
crud Todo {
|
|
40
|
+
entity: Todo
|
|
41
|
+
operations: [list, create, update, delete]
|
|
42
|
+
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
DATABASE_URL=postgres://user:password@localhost:5432/{{kebabCase appName}}
|
|
2
|
-
PORT={{backendPort}}
|
|
3
|
-
VITE_API_URL=http://localhost:{{backendPort}}/api
|
|
4
|
-
{{#if hasAuth}}
|
|
5
|
-
JWT_SECRET=change-me-in-production
|
|
6
|
-
{{#if (includes authMethods "google")}}
|
|
7
|
-
GOOGLE_CLIENT_ID=
|
|
8
|
-
GOOGLE_CLIENT_SECRET=
|
|
9
|
-
{{/if}}
|
|
10
|
-
{{#if (includes authMethods "github")}}
|
|
11
|
-
GITHUB_CLIENT_ID=
|
|
12
|
-
GITHUB_CLIENT_SECRET=
|
|
13
|
-
{{/if}}
|
|
14
|
-
{{/if}}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="auth-form">
|
|
3
|
-
<h2>Login</h2>
|
|
4
|
-
<form @submit.prevent="handleLogin">
|
|
5
|
-
<input v-model="username" type="text" placeholder="Username" required />
|
|
6
|
-
<input v-model="password" type="password" placeholder="Password" required />
|
|
7
|
-
<button type="submit" :disabled="loading">
|
|
8
|
-
\{{ loading ? 'Logging in...' : 'Login' }}
|
|
9
|
-
</button>
|
|
10
|
-
<p v-if="error" class="error">\{{ error }}</p>
|
|
11
|
-
</form>
|
|
12
|
-
<p>Don't have an account? <RouterLink to="/register">Register</RouterLink></p>
|
|
13
|
-
{{#if (includes authMethods "google")}}
|
|
14
|
-
<a href="/auth/google" class="oauth-btn">Continue with Google</a>
|
|
15
|
-
{{/if}}
|
|
16
|
-
{{#if (includes authMethods "github")}}
|
|
17
|
-
<a href="/auth/github" class="oauth-btn">Continue with GitHub</a>
|
|
18
|
-
{{/if}}
|
|
19
|
-
</div>
|
|
20
|
-
</template>
|
|
21
|
-
|
|
22
|
-
<script setup>
|
|
23
|
-
import { ref } from 'vue'
|
|
24
|
-
import { useRouter } from 'vue-router'
|
|
25
|
-
import { useAuth } from '../vasp/auth.js'
|
|
26
|
-
|
|
27
|
-
const router = useRouter()
|
|
28
|
-
const { login } = useAuth()
|
|
29
|
-
const username = ref('')
|
|
30
|
-
const password = ref('')
|
|
31
|
-
const loading = ref(false)
|
|
32
|
-
const error = ref('')
|
|
33
|
-
|
|
34
|
-
async function handleLogin() {
|
|
35
|
-
loading.value = true
|
|
36
|
-
error.value = ''
|
|
37
|
-
try {
|
|
38
|
-
await login(username.value, password.value)
|
|
39
|
-
router.push('/')
|
|
40
|
-
} catch (err) {
|
|
41
|
-
error.value = err?.data?.error || err?.message || 'Login failed'
|
|
42
|
-
} finally {
|
|
43
|
-
loading.value = false
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
</script>
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="auth-form">
|
|
3
|
-
<h2>Create Account</h2>
|
|
4
|
-
<form @submit.prevent="handleRegister">
|
|
5
|
-
<input v-model="username" type="text" placeholder="Username" required />
|
|
6
|
-
<input v-model="email" type="email" placeholder="Email (optional)" />
|
|
7
|
-
<input v-model="password" type="password" placeholder="Password (min 8 chars)" required />
|
|
8
|
-
<button type="submit" :disabled="loading">
|
|
9
|
-
\{{ loading ? 'Creating account...' : 'Register' }}
|
|
10
|
-
</button>
|
|
11
|
-
<p v-if="error" class="error">\{{ error }}</p>
|
|
12
|
-
</form>
|
|
13
|
-
<p>Already have an account? <RouterLink to="/login">Login</RouterLink></p>
|
|
14
|
-
</div>
|
|
15
|
-
</template>
|
|
16
|
-
|
|
17
|
-
<script setup>
|
|
18
|
-
import { ref } from 'vue'
|
|
19
|
-
import { useRouter } from 'vue-router'
|
|
20
|
-
import { useAuth } from '../vasp/auth.js'
|
|
21
|
-
|
|
22
|
-
const router = useRouter()
|
|
23
|
-
const { register } = useAuth()
|
|
24
|
-
const username = ref('')
|
|
25
|
-
const email = ref('')
|
|
26
|
-
const password = ref('')
|
|
27
|
-
const loading = ref(false)
|
|
28
|
-
const error = ref('')
|
|
29
|
-
|
|
30
|
-
async function handleRegister() {
|
|
31
|
-
loading.value = true
|
|
32
|
-
error.value = ''
|
|
33
|
-
try {
|
|
34
|
-
await register(username.value, password.value, email.value || undefined)
|
|
35
|
-
router.push('/')
|
|
36
|
-
} catch (err) {
|
|
37
|
-
error.value = err?.data?.error || err?.message || 'Registration failed'
|
|
38
|
-
} finally {
|
|
39
|
-
loading.value = false
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
</script>
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { Elysia } from 'elysia'
|
|
2
|
-
import { db } from '../db/client.{{ext}}'
|
|
3
|
-
import { users } from '../../drizzle/schema.{{ext}}'
|
|
4
|
-
import { eq } from 'drizzle-orm'
|
|
5
|
-
import { authPlugin } from './plugin.{{ext}}'
|
|
6
|
-
{{#if (includes authMethods "usernameAndPassword")}}
|
|
7
|
-
import { usernameAndPasswordRoutes } from './providers/usernameAndPassword.{{ext}}'
|
|
8
|
-
{{/if}}
|
|
9
|
-
{{#if (includes authMethods "google")}}
|
|
10
|
-
import { googleRoutes } from './providers/google.{{ext}}'
|
|
11
|
-
{{/if}}
|
|
12
|
-
{{#if (includes authMethods "github")}}
|
|
13
|
-
import { githubRoutes } from './providers/github.{{ext}}'
|
|
14
|
-
{{/if}}
|
|
15
|
-
|
|
16
|
-
export { authPlugin } from './plugin.{{ext}}'
|
|
17
|
-
|
|
18
|
-
export const authRoutes = new Elysia({ prefix: '/api/auth' })
|
|
19
|
-
.use(authPlugin)
|
|
20
|
-
{{#if (includes authMethods "usernameAndPassword")}}
|
|
21
|
-
.use(usernameAndPasswordRoutes)
|
|
22
|
-
{{/if}}
|
|
23
|
-
{{#if (includes authMethods "google")}}
|
|
24
|
-
.use(googleRoutes)
|
|
25
|
-
{{/if}}
|
|
26
|
-
{{#if (includes authMethods "github")}}
|
|
27
|
-
.use(githubRoutes)
|
|
28
|
-
{{/if}}
|
|
29
|
-
.get('/me', async ({ jwt, cookie: { token }, set }) => {
|
|
30
|
-
const payload = await jwt.verify(token?.value ?? '')
|
|
31
|
-
if (!payload || typeof payload.userId !== 'number') {
|
|
32
|
-
set.status = 401
|
|
33
|
-
return { error: 'Unauthorized' }
|
|
34
|
-
}
|
|
35
|
-
const [user] = await db.select().from(users).where(eq(users.id, payload.userId)).limit(1)
|
|
36
|
-
if (!user) {
|
|
37
|
-
set.status = 401
|
|
38
|
-
return { error: 'User not found' }
|
|
39
|
-
}
|
|
40
|
-
const { passwordHash: _ph, ...safeUser } = user
|
|
41
|
-
return safeUser
|
|
42
|
-
})
|
|
43
|
-
.post('/logout', ({ cookie: { token }, set }) => {
|
|
44
|
-
token?.remove()
|
|
45
|
-
return { ok: true }
|
|
46
|
-
})
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { Elysia } from 'elysia'
|
|
2
|
-
import { jwt } from '@elysiajs/jwt'
|
|
3
|
-
import { cookie } from '@elysiajs/cookie'
|
|
4
|
-
import { db } from '../db/client.{{ext}}'
|
|
5
|
-
import { users } from '../../drizzle/schema.{{ext}}'
|
|
6
|
-
import { eq } from 'drizzle-orm'
|
|
7
|
-
|
|
8
|
-
const JWT_SECRET = process.env.JWT_SECRET || 'change-me-in-production'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* requireAuth — Elysia plugin that verifies the JWT cookie and injects `user` into the context.
|
|
12
|
-
* Use on any route that requires authentication.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* new Elysia().use(requireAuth).get('/protected', ({ user }) => user)
|
|
16
|
-
*/
|
|
17
|
-
export const requireAuth = new Elysia({ name: 'require-auth' })
|
|
18
|
-
.use(jwt({ name: 'jwt', secret: JWT_SECRET }))
|
|
19
|
-
.use(cookie())
|
|
20
|
-
.derive(async ({ jwt, cookie: { token }, set }) => {
|
|
21
|
-
const payload = await jwt.verify(token?.value ?? '')
|
|
22
|
-
if (!payload || typeof payload.userId !== 'number') {
|
|
23
|
-
set.status = 401
|
|
24
|
-
throw new Error('Unauthorized')
|
|
25
|
-
}
|
|
26
|
-
const [user] = await db.select().from(users).where(eq(users.id, payload.userId)).limit(1)
|
|
27
|
-
if (!user) {
|
|
28
|
-
set.status = 401
|
|
29
|
-
throw new Error('User not found')
|
|
30
|
-
}
|
|
31
|
-
const { passwordHash: _ph, ...safeUser } = user
|
|
32
|
-
return { user: safeUser }
|
|
33
|
-
})
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { Elysia } from 'elysia'
|
|
2
|
-
import { db } from '../../db/client.{{ext}}'
|
|
3
|
-
import { users } from '../../../drizzle/schema.{{ext}}'
|
|
4
|
-
import { eq } from 'drizzle-orm'
|
|
5
|
-
import { authPlugin } from '../plugin.{{ext}}'
|
|
6
|
-
|
|
7
|
-
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || ''
|
|
8
|
-
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET || ''
|
|
9
|
-
const REDIRECT_URI = process.env.GITHUB_REDIRECT_URI || 'http://localhost:{{backendPort}}/auth/github/callback'
|
|
10
|
-
|
|
11
|
-
export const githubRoutes = new Elysia()
|
|
12
|
-
.use(authPlugin)
|
|
13
|
-
.get('/github', ({ set }) => {
|
|
14
|
-
const params = new URLSearchParams({
|
|
15
|
-
client_id: GITHUB_CLIENT_ID,
|
|
16
|
-
redirect_uri: REDIRECT_URI,
|
|
17
|
-
scope: 'read:user user:email',
|
|
18
|
-
})
|
|
19
|
-
set.redirect = `https://github.com/login/oauth/authorize?${params}`
|
|
20
|
-
})
|
|
21
|
-
.get('/github/callback', async ({ query, jwt, cookie: { token }, set }) => {
|
|
22
|
-
const { code } = query
|
|
23
|
-
if (!code) { set.status = 400; return { error: 'Missing code' } }
|
|
24
|
-
|
|
25
|
-
const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
|
|
26
|
-
method: 'POST',
|
|
27
|
-
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
28
|
-
body: JSON.stringify({ client_id: GITHUB_CLIENT_ID, client_secret: GITHUB_CLIENT_SECRET, code }),
|
|
29
|
-
})
|
|
30
|
-
const { access_token } = await tokenRes.json()
|
|
31
|
-
|
|
32
|
-
const userRes = await fetch('https://api.github.com/user', {
|
|
33
|
-
headers: { Authorization: `Bearer ${access_token}`, Accept: 'application/json' },
|
|
34
|
-
})
|
|
35
|
-
const ghUser = await userRes.json()
|
|
36
|
-
const githubId = String(ghUser.id)
|
|
37
|
-
const email = ghUser.email || `${ghUser.login}@github.local`
|
|
38
|
-
|
|
39
|
-
let [user] = await db.select().from(users).where(eq(users.githubId, githubId)).limit(1)
|
|
40
|
-
if (!user) {
|
|
41
|
-
;[user] = await db.insert(users).values({ username: ghUser.login, email, githubId }).returning()
|
|
42
|
-
}
|
|
43
|
-
if (!user) { set.status = 500; return { error: 'Failed to create user' } }
|
|
44
|
-
|
|
45
|
-
const tokenValue = await jwt.sign({ userId: user.id })
|
|
46
|
-
token.set({ value: tokenValue, httpOnly: true, sameSite: 'lax', path: '/' })
|
|
47
|
-
set.redirect = '/'
|
|
48
|
-
})
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { Elysia } from 'elysia'
|
|
2
|
-
import { db } from '../../db/client.{{ext}}'
|
|
3
|
-
import { users } from '../../../drizzle/schema.{{ext}}'
|
|
4
|
-
import { eq } from 'drizzle-orm'
|
|
5
|
-
import { authPlugin } from '../plugin.{{ext}}'
|
|
6
|
-
|
|
7
|
-
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || ''
|
|
8
|
-
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || ''
|
|
9
|
-
const REDIRECT_URI = process.env.GOOGLE_REDIRECT_URI || 'http://localhost:{{backendPort}}/auth/google/callback'
|
|
10
|
-
|
|
11
|
-
export const googleRoutes = new Elysia()
|
|
12
|
-
.use(authPlugin)
|
|
13
|
-
.get('/google', ({ set }) => {
|
|
14
|
-
const params = new URLSearchParams({
|
|
15
|
-
client_id: GOOGLE_CLIENT_ID,
|
|
16
|
-
redirect_uri: REDIRECT_URI,
|
|
17
|
-
response_type: 'code',
|
|
18
|
-
scope: 'openid email profile',
|
|
19
|
-
})
|
|
20
|
-
set.redirect = `https://accounts.google.com/o/oauth2/v2/auth?${params}`
|
|
21
|
-
})
|
|
22
|
-
.get('/google/callback', async ({ query, jwt, cookie: { token }, set }) => {
|
|
23
|
-
const { code } = query
|
|
24
|
-
if (!code) { set.status = 400; return { error: 'Missing code' } }
|
|
25
|
-
|
|
26
|
-
// Exchange code for tokens
|
|
27
|
-
const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
|
|
28
|
-
method: 'POST',
|
|
29
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
30
|
-
body: new URLSearchParams({
|
|
31
|
-
code,
|
|
32
|
-
client_id: GOOGLE_CLIENT_ID,
|
|
33
|
-
client_secret: GOOGLE_CLIENT_SECRET,
|
|
34
|
-
redirect_uri: REDIRECT_URI,
|
|
35
|
-
grant_type: 'authorization_code',
|
|
36
|
-
}),
|
|
37
|
-
})
|
|
38
|
-
const tokenData = await tokenRes.json()
|
|
39
|
-
const idToken = tokenData.id_token
|
|
40
|
-
const payload = JSON.parse(Buffer.from(idToken.split('.')[1], 'base64').toString())
|
|
41
|
-
const googleId = payload.sub
|
|
42
|
-
const email = payload.email
|
|
43
|
-
|
|
44
|
-
let [user] = await db.select().from(users).where(eq(users.googleId, googleId)).limit(1)
|
|
45
|
-
if (!user) {
|
|
46
|
-
;[user] = await db.insert(users).values({ username: email, email, googleId }).returning()
|
|
47
|
-
}
|
|
48
|
-
if (!user) { set.status = 500; return { error: 'Failed to create user' } }
|
|
49
|
-
|
|
50
|
-
const tokenValue = await jwt.sign({ userId: user.id })
|
|
51
|
-
token.set({ value: tokenValue, httpOnly: true, sameSite: 'lax', path: '/' })
|
|
52
|
-
set.redirect = '/'
|
|
53
|
-
})
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { Elysia, t } from 'elysia'
|
|
2
|
-
import { db } from '../../db/client.{{ext}}'
|
|
3
|
-
import { users } from '../../../drizzle/schema.{{ext}}'
|
|
4
|
-
import { eq } from 'drizzle-orm'
|
|
5
|
-
import { authPlugin } from '../plugin.{{ext}}'
|
|
6
|
-
|
|
7
|
-
async function hashPassword(password{{#if isTypeScript}}: string{{/if}}) {
|
|
8
|
-
return Bun.password.hash(password, 'argon2id')
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
async function verifyPassword(password{{#if isTypeScript}}: string{{/if}}, hash{{#if isTypeScript}}: string{{/if}}) {
|
|
12
|
-
return Bun.password.verify(password, hash)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const usernameAndPasswordRoutes = new Elysia()
|
|
16
|
-
.use(authPlugin)
|
|
17
|
-
.post(
|
|
18
|
-
'/register',
|
|
19
|
-
async ({ body, jwt, cookie: { token }, set }) => {
|
|
20
|
-
const existing = await db.select().from(users).where(eq(users.username, body.username)).limit(1)
|
|
21
|
-
if (existing.length > 0) {
|
|
22
|
-
set.status = 400
|
|
23
|
-
return { error: 'Username already taken' }
|
|
24
|
-
}
|
|
25
|
-
const passwordHash = await hashPassword(body.password)
|
|
26
|
-
const [user] = await db
|
|
27
|
-
.insert(users)
|
|
28
|
-
.values({ username: body.username, email: body.email ?? null, passwordHash })
|
|
29
|
-
.returning()
|
|
30
|
-
if (!user) {
|
|
31
|
-
set.status = 500
|
|
32
|
-
return { error: 'Failed to create user' }
|
|
33
|
-
}
|
|
34
|
-
const tokenValue = await jwt.sign({ userId: user.id })
|
|
35
|
-
token.set({ value: tokenValue, httpOnly: true, sameSite: 'lax', path: '/' })
|
|
36
|
-
const { passwordHash: _ph, ...safeUser } = user
|
|
37
|
-
return safeUser
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
body: t.Object({
|
|
41
|
-
username: t.String({ minLength: 3 }),
|
|
42
|
-
password: t.String({ minLength: 8 }),
|
|
43
|
-
email: t.Optional(t.String({ format: 'email' })),
|
|
44
|
-
}),
|
|
45
|
-
},
|
|
46
|
-
)
|
|
47
|
-
.post(
|
|
48
|
-
'/login',
|
|
49
|
-
async ({ body, jwt, cookie: { token }, set }) => {
|
|
50
|
-
const [user] = await db.select().from(users).where(eq(users.username, body.username)).limit(1)
|
|
51
|
-
if (!user || !user.passwordHash || !(await verifyPassword(body.password, user.passwordHash))) {
|
|
52
|
-
set.status = 401
|
|
53
|
-
return { error: 'Invalid username or password' }
|
|
54
|
-
}
|
|
55
|
-
const tokenValue = await jwt.sign({ userId: user.id })
|
|
56
|
-
token.set({ value: tokenValue, httpOnly: true, sameSite: 'lax', path: '/' })
|
|
57
|
-
const { passwordHash: _ph, ...safeUser } = user
|
|
58
|
-
return safeUser
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
body: t.Object({
|
|
62
|
-
username: t.String(),
|
|
63
|
-
password: t.String(),
|
|
64
|
-
}),
|
|
65
|
-
},
|
|
66
|
-
)
|