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.
Files changed (136) hide show
  1. package/dist/vasp +82 -34
  2. package/package.json +2 -2
  3. package/starters/minimal.vasp +1 -1
  4. package/starters/recipe.vasp +11 -20
  5. package/starters/todo-auth-ssr.vasp +33 -20
  6. package/starters/todo.vasp +15 -8
  7. package/templates/shared/.gitignore.hbs +1 -0
  8. package/templates/shared/auth/server/index.hbs +4 -8
  9. package/templates/shared/auth/server/middleware.hbs +33 -15
  10. package/templates/{templates/shared → shared}/auth/server/plugin.hbs +0 -2
  11. package/templates/shared/auth/server/providers/github.hbs +1 -1
  12. package/templates/shared/auth/server/providers/google.hbs +1 -1
  13. package/templates/shared/auth/server/providers/usernameAndPassword.hbs +3 -6
  14. package/templates/shared/bunfig.toml.hbs +3 -0
  15. package/templates/shared/drizzle/schema.hbs +38 -19
  16. package/templates/shared/package.json.hbs +11 -4
  17. package/templates/shared/server/db/client.hbs +19 -1
  18. package/templates/shared/server/db/seed.hbs +16 -0
  19. package/templates/shared/server/index.hbs +40 -0
  20. package/templates/shared/server/middleware/errorHandler.hbs +75 -0
  21. package/templates/shared/server/middleware/logger.hbs +74 -0
  22. package/templates/shared/server/middleware/rateLimit.hbs +2 -2
  23. package/templates/shared/server/routes/_vasp.hbs +37 -0
  24. package/templates/shared/server/routes/actions/_action.hbs +5 -1
  25. package/templates/shared/server/routes/api/_api.hbs +24 -0
  26. package/templates/shared/server/routes/crud/_crud.hbs +58 -10
  27. package/templates/shared/server/routes/queries/_query.hbs +5 -1
  28. package/templates/shared/shared/types.hbs +58 -0
  29. package/templates/shared/shared/validation.hbs +20 -0
  30. package/templates/shared/tests/actions/_action.test.js.hbs +7 -0
  31. package/templates/shared/tests/actions/_action.test.ts.hbs +7 -0
  32. package/templates/shared/tests/auth/login.test.js.hbs +7 -0
  33. package/templates/shared/tests/auth/login.test.ts.hbs +7 -0
  34. package/templates/shared/tests/crud/_entity.test.js.hbs +7 -0
  35. package/templates/shared/tests/crud/_entity.test.ts.hbs +7 -0
  36. package/templates/shared/tests/queries/_query.test.js.hbs +7 -0
  37. package/templates/shared/tests/queries/_query.test.ts.hbs +7 -0
  38. package/templates/shared/tests/setup.js.hbs +5 -0
  39. package/templates/shared/tests/setup.ts.hbs +5 -0
  40. package/templates/shared/tests/vitest.config.js.hbs +8 -0
  41. package/templates/shared/tests/vitest.config.ts.hbs +8 -0
  42. package/templates/shared/tsconfig.json.hbs +2 -1
  43. package/templates/spa/js/src/App.vue.hbs +9 -1
  44. package/templates/spa/js/src/components/VaspErrorBoundary.vue.hbs +33 -0
  45. package/templates/spa/js/src/components/VaspNotifications.vue.hbs +60 -0
  46. package/templates/spa/js/src/vasp/auth.js.hbs +31 -15
  47. package/templates/spa/js/src/vasp/client/actions.js.hbs +7 -1
  48. package/templates/spa/js/src/vasp/client/crud.js.hbs +94 -5
  49. package/templates/spa/js/src/vasp/useVaspNotifications.js.hbs +35 -0
  50. package/templates/spa/js/vite.config.js.hbs +1 -0
  51. package/templates/spa/ts/src/App.vue.hbs +9 -1
  52. package/templates/spa/ts/src/components/VaspErrorBoundary.vue.hbs +33 -0
  53. package/templates/spa/ts/src/components/VaspNotifications.vue.hbs +60 -0
  54. package/templates/spa/ts/src/vasp/auth.ts.hbs +31 -15
  55. package/templates/spa/ts/src/vasp/client/actions.ts.hbs +7 -1
  56. package/templates/spa/ts/src/vasp/client/crud.ts.hbs +96 -10
  57. package/templates/spa/ts/src/vasp/client/types.ts.hbs +14 -28
  58. package/templates/spa/ts/src/vasp/useVaspNotifications.ts.hbs +41 -0
  59. package/templates/spa/ts/vite.config.ts.hbs +1 -0
  60. package/templates/ssr/js/error.vue.hbs +23 -0
  61. package/templates/ssr/js/nuxt.config.js.hbs +1 -0
  62. package/templates/ssr/ts/error.vue.hbs +26 -0
  63. package/templates/ssr/ts/nuxt.config.ts.hbs +1 -0
  64. package/templates/starters/minimal.vasp +15 -0
  65. package/templates/starters/recipe.vasp +70 -0
  66. package/templates/starters/todo-auth-ssr.vasp +65 -0
  67. package/templates/starters/todo.vasp +42 -0
  68. package/templates/templates/shared/.env.example.hbs +0 -14
  69. package/templates/templates/shared/.gitignore.hbs +0 -8
  70. package/templates/templates/shared/auth/client/Login.vue.hbs +0 -46
  71. package/templates/templates/shared/auth/client/Register.vue.hbs +0 -42
  72. package/templates/templates/shared/auth/server/index.hbs +0 -46
  73. package/templates/templates/shared/auth/server/middleware.hbs +0 -33
  74. package/templates/templates/shared/auth/server/providers/github.hbs +0 -48
  75. package/templates/templates/shared/auth/server/providers/google.hbs +0 -53
  76. package/templates/templates/shared/auth/server/providers/usernameAndPassword.hbs +0 -66
  77. package/templates/templates/shared/bunfig.toml.hbs +0 -5
  78. package/templates/templates/shared/drizzle/drizzle.config.hbs +0 -10
  79. package/templates/templates/shared/drizzle/schema.hbs +0 -48
  80. package/templates/templates/shared/jobs/_job.hbs +0 -34
  81. package/templates/templates/shared/jobs/boss.hbs +0 -15
  82. package/templates/templates/shared/package.json.hbs +0 -38
  83. package/templates/templates/shared/server/db/client.hbs +0 -12
  84. package/templates/templates/shared/server/index.hbs +0 -73
  85. package/templates/templates/shared/server/middleware/csrf.hbs +0 -34
  86. package/templates/templates/shared/server/middleware/rateLimit.hbs +0 -44
  87. package/templates/templates/shared/server/routes/actions/_action.hbs +0 -20
  88. package/templates/templates/shared/server/routes/crud/_crud.hbs +0 -86
  89. package/templates/templates/shared/server/routes/jobs/_schedule.hbs +0 -12
  90. package/templates/templates/shared/server/routes/queries/_query.hbs +0 -20
  91. package/templates/templates/shared/server/routes/realtime/_channel.hbs +0 -78
  92. package/templates/templates/shared/server/routes/realtime/index.hbs +0 -9
  93. package/templates/templates/shared/tsconfig.json.hbs +0 -21
  94. package/templates/templates/spa/js/index.html.hbs +0 -12
  95. package/templates/templates/spa/js/src/App.vue.hbs +0 -3
  96. package/templates/templates/spa/js/src/main.js.hbs +0 -9
  97. package/templates/templates/spa/js/src/router/index.js.hbs +0 -41
  98. package/templates/templates/spa/js/src/vasp/auth.js.hbs +0 -45
  99. package/templates/templates/spa/js/src/vasp/client/actions.js.hbs +0 -15
  100. package/templates/templates/spa/js/src/vasp/client/crud.js.hbs +0 -30
  101. package/templates/templates/spa/js/src/vasp/client/index.js.hbs +0 -16
  102. package/templates/templates/spa/js/src/vasp/client/queries.js.hbs +0 -15
  103. package/templates/templates/spa/js/src/vasp/client/realtime.js.hbs +0 -51
  104. package/templates/templates/spa/js/src/vasp/plugin.js.hbs +0 -11
  105. package/templates/templates/spa/js/vite.config.js.hbs +0 -26
  106. package/templates/templates/spa/ts/index.html.hbs +0 -12
  107. package/templates/templates/spa/ts/src/App.vue.hbs +0 -3
  108. package/templates/templates/spa/ts/src/main.ts.hbs +0 -9
  109. package/templates/templates/spa/ts/src/router/index.ts.hbs +0 -41
  110. package/templates/templates/spa/ts/src/vasp/auth.ts.hbs +0 -53
  111. package/templates/templates/spa/ts/src/vasp/client/actions.ts.hbs +0 -19
  112. package/templates/templates/spa/ts/src/vasp/client/crud.ts.hbs +0 -37
  113. package/templates/templates/spa/ts/src/vasp/client/index.ts.hbs +0 -17
  114. package/templates/templates/spa/ts/src/vasp/client/queries.ts.hbs +0 -19
  115. package/templates/templates/spa/ts/src/vasp/client/realtime.ts.hbs +0 -56
  116. package/templates/templates/spa/ts/src/vasp/client/types.ts.hbs +0 -33
  117. package/templates/templates/spa/ts/src/vasp/plugin.ts.hbs +0 -12
  118. package/templates/templates/spa/ts/vite.config.ts.hbs +0 -26
  119. package/templates/templates/ssr/js/_page.vue.hbs +0 -10
  120. package/templates/templates/ssr/js/app.vue.hbs +0 -3
  121. package/templates/templates/ssr/js/composables/useAuth.js.hbs +0 -52
  122. package/templates/templates/ssr/js/composables/useVasp.js.hbs +0 -6
  123. package/templates/templates/ssr/js/middleware/auth.js.hbs +0 -8
  124. package/templates/templates/ssr/js/nuxt.config.js.hbs +0 -15
  125. package/templates/templates/ssr/js/plugins/vasp.client.js.hbs +0 -27
  126. package/templates/templates/ssr/js/plugins/vasp.server.js.hbs +0 -33
  127. package/templates/templates/ssr/ts/_page.vue.hbs +0 -10
  128. package/templates/templates/ssr/ts/app.vue.hbs +0 -3
  129. package/templates/templates/ssr/ts/composables/useAuth.ts.hbs +0 -56
  130. package/templates/templates/ssr/ts/composables/useVasp.ts.hbs +0 -10
  131. package/templates/templates/ssr/ts/middleware/auth.ts.hbs +0 -8
  132. package/templates/templates/ssr/ts/nuxt.config.ts.hbs +0 -19
  133. package/templates/templates/ssr/ts/plugins/vasp.client.ts.hbs +0 -27
  134. package/templates/templates/ssr/ts/plugins/vasp.server.ts.hbs +0 -33
  135. /package/templates/{templates/shared → shared}/.env.hbs +0 -0
  136. /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>
@@ -5,6 +5,7 @@ export default defineNuxtConfig({
5
5
  devtools: { enabled: true },
6
6
  alias: {
7
7
  '@src': '~/src',
8
+ '@shared': '~/shared',
8
9
  },
9
10
  runtimeConfig: {
10
11
  backendUrl: process.env.BACKEND_URL || 'http://localhost:{{backendPort}}',
@@ -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>
@@ -9,6 +9,7 @@ export default defineNuxtConfig({
9
9
  },
10
10
  alias: {
11
11
  '@src': '~/src',
12
+ '@shared': '~/shared',
12
13
  },
13
14
  runtimeConfig: {
14
15
  backendUrl: process.env.BACKEND_URL || 'http://localhost:{{backendPort}}',
@@ -0,0 +1,15 @@
1
+ app MinimalApp {
2
+ title: "Minimal App"
3
+ db: Drizzle
4
+ ssr: false
5
+ typescript: false
6
+ }
7
+
8
+ route HomeRoute {
9
+ path: "/"
10
+ to: HomePage
11
+ }
12
+
13
+ page HomePage {
14
+ component: import Home from "@src/pages/Home.vue"
15
+ }
@@ -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,8 +0,0 @@
1
- node_modules
2
- dist
3
- .output
4
- .nuxt
5
- .vasp-gen
6
- .env
7
- .env.local
8
- *.log
@@ -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
- )