@kevinmarrec/create-app 0.7.0 → 0.8.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 (63) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +1 -3
  3. package/package.json +21 -17
  4. package/template/.github/workflows/ci.yml +3 -3
  5. package/template/.scripts/dev.ts +8 -0
  6. package/template/.vscode/settings.json +1 -1
  7. package/template/client/.env +1 -0
  8. package/template/{frontend → client}/package.json +11 -9
  9. package/template/{frontend → client}/src/App.vue +9 -2
  10. package/template/client/src/composables/auth.ts +39 -0
  11. package/template/client/src/composables/content.ts +12 -0
  12. package/template/{frontend → client}/src/composables/index.ts +1 -0
  13. package/template/{frontend → client}/src/lib/orpc.ts +2 -2
  14. package/template/compose.yaml +5 -5
  15. package/template/knip.config.ts +11 -6
  16. package/template/package.json +10 -10
  17. package/template/server/.env.development +4 -0
  18. package/template/{backend → server}/package.json +7 -7
  19. package/template/{backend → server}/src/auth/index.ts +2 -0
  20. package/template/{backend → server}/src/database/index.ts +1 -1
  21. package/template/{backend → server}/src/env.d.ts +2 -2
  22. package/template/{backend → server}/src/main.ts +20 -16
  23. package/template/server/src/orpc/context.ts +10 -0
  24. package/template/server/src/orpc/handler.ts +34 -0
  25. package/template/server/src/orpc/index.ts +18 -0
  26. package/template/server/src/orpc/middlewares/auth.ts +23 -0
  27. package/template/server/src/orpc/router/index.ts +19 -0
  28. package/template/server/src/utils/cors.ts +26 -0
  29. package/template/tsconfig.json +2 -2
  30. package/template/backend/.env.development +0 -4
  31. package/template/backend/src/orpc/index.ts +0 -65
  32. package/template/backend/src/orpc/middlewares/auth.ts +0 -33
  33. package/template/backend/src/orpc/router/auth.ts +0 -54
  34. package/template/backend/src/orpc/router/index.ts +0 -9
  35. package/template/frontend/.env.development +0 -1
  36. package/template/frontend/src/composables/auth.ts +0 -31
  37. package/template/scripts/dev.ts +0 -8
  38. /package/template/{gitignore → .gitignore} +0 -0
  39. /package/template/{npmrc → .npmrc} +0 -0
  40. /package/template/{frontend → client}/index.html +0 -0
  41. /package/template/{frontend → client}/public/favicon.svg +0 -0
  42. /package/template/{frontend → client}/public/robots.txt +0 -0
  43. /package/template/{frontend → client}/src/components/.gitkeep +0 -0
  44. /package/template/{frontend → client}/src/env.d.ts +0 -0
  45. /package/template/{frontend → client}/src/locales/en.yml +0 -0
  46. /package/template/{frontend → client}/src/locales/fr.yml +0 -0
  47. /package/template/{frontend → client}/src/main.ts +0 -0
  48. /package/template/{frontend → client}/tsconfig.json +0 -0
  49. /package/template/{frontend → client}/uno.config.ts +0 -0
  50. /package/template/{frontend → client}/vite.config.ts +0 -0
  51. /package/template/{frontend → client}/wrangler.json +0 -0
  52. /package/template/{backend → server}/src/database/drizzle/config.ts +0 -0
  53. /package/template/{backend → server}/src/database/migrations/0000_init.sql +0 -0
  54. /package/template/{backend → server}/src/database/migrations/meta/0000_snapshot.json +0 -0
  55. /package/template/{backend → server}/src/database/migrations/meta/_journal.json +0 -0
  56. /package/template/{backend → server}/src/database/schema/accounts.ts +0 -0
  57. /package/template/{backend → server}/src/database/schema/index.ts +0 -0
  58. /package/template/{backend → server}/src/database/schema/sessions.ts +0 -0
  59. /package/template/{backend → server}/src/database/schema/users.ts +0 -0
  60. /package/template/{backend → server}/src/database/schema/verifications.ts +0 -0
  61. /package/template/{backend → server}/src/orpc/middlewares/index.ts +0 -0
  62. /package/template/{backend → server}/src/utils/logger.ts +0 -0
  63. /package/template/{backend → server}/tsconfig.json +0 -0
package/README.md CHANGED
@@ -12,7 +12,7 @@ CLI that scaffolds an opinionated [Bun](https://bun.sh) & [Vue](https://vuejs.or
12
12
 
13
13
  ## Usage
14
14
 
15
- > Requires [Bun](https://bun.sh) v1.2 _or later_.
15
+ > Requires [Bun](https://bun.sh) v1.3 _or later_.
16
16
 
17
17
  ```sh
18
18
  bun create @kevinmarrec/app
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { x } from "tinyexec";
8
8
  import fs from "node:fs/promises";
9
9
 
10
10
  //#region package.json
11
- var version = "0.7.0";
11
+ var version = "0.8.0";
12
12
 
13
13
  //#endregion
14
14
  //#region src/utils/fs.ts
@@ -35,8 +35,6 @@ var fs_default = {
35
35
  async function scaffold(root) {
36
36
  await fs_default.exists(root) ? await fs_default.empty(root) : await fs_default.mkdir(root);
37
37
  await fs_default.cp(join(import.meta.dirname, "../template"), root, { recursive: true });
38
- await fs_default.rename(join(root, "gitignore"), join(root, ".gitignore"));
39
- await fs_default.rename(join(root, "npmrc"), join(root, ".npmrc"));
40
38
  }
41
39
 
42
40
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kevinmarrec/create-app",
3
3
  "type": "module",
4
- "version": "0.7.0",
4
+ "version": "0.8.0",
5
5
  "packageManager": "bun@1.3.1",
6
6
  "description": "CLI that scaffolds an opinionated Bun & Vue fullstack application.",
7
7
  "author": "Kevin Marrec <kevin@marrec.io>",
@@ -18,19 +18,21 @@
18
18
  ],
19
19
  "workspaces": [
20
20
  "template",
21
- "template/backend",
22
- "template/frontend"
21
+ "template/client",
22
+ "template/server"
23
23
  ],
24
24
  "bin": {
25
25
  "create-app": "dist/index.js"
26
26
  },
27
27
  "files": [
28
28
  "dist",
29
- "template"
29
+ "template",
30
+ "template/.gitignore",
31
+ "template/.npmrc"
30
32
  ],
31
33
  "scripts": {
32
- "build": "tsdown",
33
- "check": "bun run check:unused && bun run check:eslint && bun run check:stylelint && bun run check:types",
34
+ "build": "tsdown --no-fixed-extension",
35
+ "check": "bun run check:eslint && bun run check:stylelint && bun run check:unused && bun run check:types",
34
36
  "check:eslint": "eslint . --cache",
35
37
  "check:stylelint": "stylelint '**/*.{css,scss,vue}' --ignorePath .gitignore --cache",
36
38
  "check:types": "vue-tsc --noEmit",
@@ -47,22 +49,24 @@
47
49
  "@clack/prompts": "^0.11.0",
48
50
  "ansis": "^4.2.0",
49
51
  "pathe": "^2.0.3",
50
- "tinyexec": "^1.0.1"
52
+ "tinyexec": "^1.0.2"
51
53
  },
52
54
  "devDependencies": {
53
55
  "@faker-js/faker": "^10.1.0",
54
- "@kevinmarrec/eslint-config": "^1.5.3",
55
- "@kevinmarrec/stylelint-config": "^1.2.2",
56
- "@kevinmarrec/tsconfig": "^1.1.0",
57
- "@types/bun": "^1.3.0",
58
- "@vitest/coverage-v8": "^4.0.1",
56
+ "@kevinmarrec/eslint-config": "^1.5.4",
57
+ "@kevinmarrec/stylelint-config": "^1.5.4",
58
+ "@kevinmarrec/tsconfig": "^1.5.4",
59
+ "@types/bun": "^1.3.1",
60
+ "@vitest/coverage-v8": "^4.0.7",
59
61
  "bumpp": "^10.3.1",
60
- "eslint": "^9.38.0",
61
- "knip": "^5.66.0",
62
+ "eslint": "^9.39.1",
63
+ "filesize": "^11.0.13",
64
+ "knip": "^5.67.1",
62
65
  "stylelint": "^16.25.0",
63
- "tsdown": "^0.15.7",
66
+ "tinyglobby": "^0.2.15",
67
+ "tsdown": "^0.16.0",
64
68
  "typescript": "^5.9.3",
65
- "vitest": "^4.0.1",
66
- "vue-tsc": "^3.1.1"
69
+ "vitest": "^4.0.7",
70
+ "vue-tsc": "^3.1.3"
67
71
  }
68
72
  }
@@ -16,11 +16,11 @@ jobs:
16
16
  - name: Setup Bun
17
17
  uses: kevinmarrec/workflows/setup-bun@v1
18
18
 
19
+ - name: Lint
20
+ run: bun run lint
21
+
19
22
  - name: Check unused files, dependencies, and exports
20
23
  run: bun run check:unused
21
24
 
22
- - name: Lint
23
- run: bun run check:eslint && bun run check:stylelint
24
-
25
25
  - name: Type check
26
26
  run: bun run check:types
@@ -0,0 +1,8 @@
1
+ import concurrently, { type ConcurrentlyCommandInput } from 'concurrently'
2
+
3
+ const commandInputs: ConcurrentlyCommandInput[] = [
4
+ { name: 'server', command: `bun --cwd server dev | pino-pretty`, prefixColor: 'blue' },
5
+ { name: 'client', command: `bun --cwd client dev`, prefixColor: 'green' },
6
+ ]
7
+
8
+ concurrently(commandInputs)
@@ -67,6 +67,6 @@
67
67
  "i18n-ally.enabledFrameworks": ["vue"],
68
68
  "i18n-ally.keystyle": "nested",
69
69
  "i18n-ally.localesPaths": [
70
- "frontend/src/locales"
70
+ "client/src/locales"
71
71
  ]
72
72
  }
@@ -0,0 +1 @@
1
+ VITE_API_URL=http://localhost:4000
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "frontend",
2
+ "name": "client",
3
3
  "type": "module",
4
4
  "private": true,
5
5
  "scripts": {
@@ -9,17 +9,19 @@
9
9
  "preview": "vite preview --open"
10
10
  },
11
11
  "dependencies": {
12
- "@kevinmarrec/vue-i18n": "^1.1.2",
13
- "@orpc/client": "^1.9.4",
14
- "@orpc/tanstack-query": "1.9.4",
15
- "@tanstack/query-core": "^5.90.5",
16
- "@tanstack/vue-query": "^5.90.5",
12
+ "@kevinmarrec/vue-i18n": "^1.1.3",
13
+ "@orpc/client": "^1.10.4",
14
+ "@orpc/tanstack-query": "1.10.3",
15
+ "@tanstack/query-core": "^5.90.7",
16
+ "@tanstack/vue-query": "^5.90.7",
17
17
  "@unhead/vue": "^2.0.19",
18
+ "@vueuse/core": "^14.0.0",
19
+ "better-auth": "^1.3.34",
18
20
  "unocss": "^66.5.4",
19
21
  "vue": "^3.5.22"
20
22
  },
21
23
  "devDependencies": {
22
- "@kevinmarrec/unocss-config": "^1.3.3",
24
+ "@kevinmarrec/unocss-config": "^1.5.4",
23
25
  "@kevinmarrec/vite-plugin-dark-mode": "^1.1.2",
24
26
  "@modyfi/vite-plugin-yaml": "^1.1.1",
25
27
  "@unhead/addons": "^2.0.19",
@@ -27,9 +29,9 @@
27
29
  "@vitejs/plugin-vue": "^6.0.1",
28
30
  "beasties": "^0.3.5",
29
31
  "rollup-plugin-visualizer": "^6.0.5",
30
- "vite": "^7.1.10",
32
+ "vite": "^7.2.0",
31
33
  "vite-plugin-vue-devtools": "^8.0.3",
32
- "vite-ssg": "^28.2.1",
34
+ "vite-ssg": "^28.2.2",
33
35
  "vite-tsconfig-paths": "^5.1.4"
34
36
  }
35
37
  }
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { useAuth, useHead, useI18n } from '@frontend/composables'
2
+ import { useAuth, useContent, useHead, useI18n } from '@client/composables'
3
3
  import { ref } from 'vue'
4
4
 
5
5
  const { t } = useI18n()
@@ -12,7 +12,8 @@ useHead({
12
12
  }],
13
13
  })
14
14
 
15
- const { user, signUp, signIn, signOut } = useAuth()
15
+ const { user, signIn, signUp, signOut } = useAuth()
16
+ const { publicContent, privateContent } = useContent()
16
17
 
17
18
  const email = ref('')
18
19
  const password = ref('')
@@ -99,6 +100,12 @@ const password = ref('')
99
100
  <p class="text-gray-600 mt-2 dark:text-gray-400">
100
101
  {{ user.email }}
101
102
  </p>
103
+ <p class="text-gray-600 mt-2 dark:text-gray-400">
104
+ {{ publicContent }}
105
+ </p>
106
+ <p class="text-gray-600 mt-2 dark:text-gray-400">
107
+ {{ privateContent }}
108
+ </p>
102
109
  </div>
103
110
 
104
111
  <button
@@ -0,0 +1,39 @@
1
+ import { useMutation } from '@tanstack/vue-query'
2
+ import { createAuthClient } from 'better-auth/vue'
3
+ import { computed } from 'vue'
4
+
5
+ const authClient = createAuthClient({
6
+ baseURL: `${import.meta.env.VITE_API_URL}/auth`,
7
+ fetchOptions: {
8
+ credentials: 'include',
9
+ },
10
+ })
11
+
12
+ const session = authClient.useSession()
13
+
14
+ function createAuthMutation<T extends (...args: any[]) => any>(fn: T) {
15
+ return useMutation({
16
+ mutationFn: async (input: Parameters<T>[0]) => {
17
+ return fn(input, {
18
+ onError: ({ error }: any) => {
19
+ throw error
20
+ },
21
+ })
22
+ },
23
+ })
24
+ }
25
+
26
+ export function useAuth() {
27
+ const signIn = createAuthMutation(authClient.signIn.email)
28
+ const signUp = createAuthMutation(authClient.signUp.email)
29
+ const signOut = createAuthMutation(authClient.signOut)
30
+
31
+ const user = computed(() => session.value.data?.user)
32
+
33
+ return {
34
+ signIn,
35
+ signUp,
36
+ signOut,
37
+ user,
38
+ }
39
+ }
@@ -0,0 +1,12 @@
1
+ import { orpc } from '@client/lib/orpc'
2
+ import { useQuery } from '@tanstack/vue-query'
3
+
4
+ export function useContent() {
5
+ const publicContent = useQuery(orpc.public.queryOptions({})).data
6
+ const privateContent = useQuery(orpc.private.queryOptions({})).data
7
+
8
+ return {
9
+ publicContent,
10
+ privateContent,
11
+ }
12
+ }
@@ -1,3 +1,4 @@
1
1
  export { useAuth } from './auth'
2
+ export { useContent } from './content'
2
3
  export { useI18n } from '@kevinmarrec/vue-i18n'
3
4
  export { useHead } from '@unhead/vue'
@@ -1,10 +1,10 @@
1
- import type { Router, RouterClient } from '@backend/orpc/router'
2
1
  import { createORPCClient } from '@orpc/client'
3
2
  import { RPCLink } from '@orpc/client/fetch'
4
3
  import { createTanstackQueryUtils } from '@orpc/tanstack-query'
4
+ import type { Router, RouterClient } from '@server/orpc/router'
5
5
 
6
6
  const link = new RPCLink({
7
- url: import.meta.env.VITE_API_URL,
7
+ url: `${import.meta.env.VITE_API_URL}/rpc`,
8
8
  fetch: (request, init) =>
9
9
  globalThis.fetch(request, {
10
10
  ...init,
@@ -5,18 +5,18 @@ x-common: &common
5
5
  user: 1000:1000
6
6
 
7
7
  services:
8
- backend:
8
+ server:
9
9
  <<: *common
10
10
  image: oven/bun:1-alpine
11
- command: [bun, --cwd, backend, dev]
11
+ command: [bun, --cwd, server, dev]
12
12
  ports:
13
13
  - 4000:4000
14
14
 
15
- frontend:
15
+ client:
16
16
  <<: *common
17
17
  depends_on:
18
- - backend
18
+ - server
19
19
  image: imbios/bun-node:22-alpine
20
- command: [bun, --cwd, frontend, dev, --host, 0.0.0.0]
20
+ command: [bun, --cwd, client, dev, --host, 0.0.0.0]
21
21
  ports:
22
22
  - 5173:5173
@@ -10,15 +10,20 @@ export default {
10
10
  '.': {
11
11
  entry: ['*.config.ts'],
12
12
  },
13
- 'backend': {
13
+ 'client': {
14
+ entry: ['src/main.ts'],
15
+ ignoreDependencies: [
16
+ '@vueuse/core',
17
+ 'uno.css',
18
+ ],
19
+ },
20
+ 'server': {
14
21
  drizzle: {
15
22
  config: 'src/database/drizzle/config.ts',
16
23
  },
17
- ignoreDependencies: ['pino-pretty'],
18
- },
19
- 'frontend': {
20
- entry: ['src/main.ts'],
21
- ignoreDependencies: ['uno.css'],
24
+ ignoreDependencies: [
25
+ 'pino-pretty',
26
+ ],
22
27
  },
23
28
  },
24
29
  } satisfies KnipConfig
@@ -7,29 +7,29 @@
7
7
  "node": "lts/*"
8
8
  },
9
9
  "workspaces": [
10
- "backend",
11
- "frontend"
10
+ "client",
11
+ "server"
12
12
  ],
13
13
  "scripts": {
14
- "check": "bun run check:unused && bun run check:eslint && bun run check:stylelint && bun run check:types",
14
+ "check": "bun run check:eslint && bun run check:stylelint && bun run check:unused && bun run check:types",
15
15
  "check:eslint": "eslint . --cache",
16
16
  "check:stylelint": "stylelint '**/*.{css,scss,vue}' --ignorePath .gitignore --cache",
17
17
  "check:types": "vue-tsc --noEmit",
18
18
  "check:unused": "knip -n",
19
- "dev": "bun scripts/dev.ts",
19
+ "dev": "bun .scripts/dev.ts",
20
20
  "lint": "bun run check:eslint && bun run check:stylelint",
21
21
  "lint:inspect": "bunx @eslint/config-inspector",
22
22
  "update": "bunx taze -I -rwi"
23
23
  },
24
24
  "devDependencies": {
25
- "@kevinmarrec/eslint-config": "^1.5.3",
26
- "@kevinmarrec/stylelint-config": "^1.2.2",
27
- "@kevinmarrec/tsconfig": "^1.1.0",
25
+ "@kevinmarrec/eslint-config": "^1.5.4",
26
+ "@kevinmarrec/stylelint-config": "^1.5.4",
27
+ "@kevinmarrec/tsconfig": "^1.5.4",
28
28
  "concurrently": "^9.2.1",
29
- "eslint": "^9.38.0",
30
- "knip": "^5.66.0",
29
+ "eslint": "^9.39.1",
30
+ "knip": "^5.67.1",
31
31
  "stylelint": "^16.25.0",
32
32
  "typescript": "~5.9.3",
33
- "vue-tsc": "^3.1.1"
33
+ "vue-tsc": "^3.1.3"
34
34
  }
35
35
  }
@@ -0,0 +1,4 @@
1
+ ALLOWED_ORIGINS=http://localhost:5173,http://localhost:5174
2
+ AUTH_SECRET=foo
3
+ DATABASE_URL=.db
4
+ NODE_ENV=development
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "backend",
2
+ "name": "server",
3
3
  "type": "module",
4
4
  "private": true,
5
5
  "scripts": {
@@ -9,16 +9,16 @@
9
9
  "db:migrate": "bun --bun run drizzle-kit migrate --config src/database/drizzle/config.ts"
10
10
  },
11
11
  "dependencies": {
12
- "@orpc/server": "^1.9.4",
13
- "better-auth": "^1.3.27",
14
- "drizzle-orm": "^0.44.6",
12
+ "@orpc/server": "^1.10.4",
13
+ "better-auth": "^1.3.34",
14
+ "drizzle-orm": "^0.44.7",
15
15
  "pino": "^10.1.0",
16
16
  "valibot": "^1.1.0"
17
17
  },
18
18
  "devDependencies": {
19
- "@electric-sql/pglite": "^0.3.11",
20
- "@types/bun": "^1.3.0",
21
- "drizzle-kit": "^0.31.5",
19
+ "@electric-sql/pglite": "^0.3.13",
20
+ "@types/bun": "^1.3.1",
21
+ "drizzle-kit": "^0.31.6",
22
22
  "pino-pretty": "^13.1.2"
23
23
  }
24
24
  }
@@ -4,6 +4,8 @@ import type { BaseLogger } from 'pino'
4
4
 
5
5
  export function createBetterAuth({ db, logger }: { db: DB, logger: BaseLogger }) {
6
6
  return betterAuth({
7
+ basePath: '/auth',
8
+ trustedOrigins: import.meta.env.ALLOWED_ORIGINS.split(','),
7
9
  database: drizzleAdapter(db, {
8
10
  provider: 'pg',
9
11
  usePlural: true,
@@ -1,5 +1,5 @@
1
- import { logger } from '@backend/utils/logger'
2
1
  import { PGlite } from '@electric-sql/pglite'
2
+ import { logger } from '@server/utils/logger'
3
3
  import { drizzle } from 'drizzle-orm/pglite'
4
4
 
5
5
  import * as schema from './schema'
@@ -1,6 +1,6 @@
1
1
  interface ImportMetaEnv {
2
- readonly BETTER_AUTH_SECRET: string
3
- readonly BETTER_AUTH_URL: string
2
+ readonly ALLOWED_ORIGINS: string
3
+ readonly AUTH_SECRET: string
4
4
  readonly DATABASE_URL: string
5
5
  readonly LOG_LEVEL: string
6
6
  readonly NODE_ENV: string
@@ -1,32 +1,36 @@
1
1
  import process from 'node:process'
2
2
 
3
- import { createBetterAuth } from '@backend/auth'
4
- import { db } from '@backend/database'
5
- import { createRpcHandler } from '@backend/orpc'
6
- import { router } from '@backend/orpc/router'
7
- import { logger } from '@backend/utils/logger'
3
+ import { createBetterAuth } from '@server/auth'
4
+ import { db } from '@server/database'
5
+ import { createRpcHandler } from '@server/orpc'
6
+ import { router } from '@server/orpc/router'
7
+ import { cors } from '@server/utils/cors'
8
+ import { logger } from '@server/utils/logger'
8
9
 
9
10
  const auth = createBetterAuth({ db, logger })
10
11
  const rpcHandler = createRpcHandler(router)
11
12
 
12
- const server = Bun.serve({
13
- hostname: import.meta.env.HOST ?? '0.0.0.0',
14
- port: import.meta.env.PORT ?? 4000,
15
- async fetch(request) {
16
- const { matched, response } = await rpcHandler.handle(request, {
13
+ const routes = {
14
+ '/auth/*': cors(async (req) => {
15
+ return await auth.handler(req)
16
+ }),
17
+ '/rpc/*': cors(async (req) => {
18
+ const { matched, response } = await rpcHandler.handle(req, {
17
19
  prefix: '/rpc',
18
- context: {
19
- auth,
20
- db,
21
- logger,
22
- },
20
+ context: { auth, db, logger },
23
21
  })
24
22
 
25
23
  if (matched)
26
24
  return response
27
25
 
28
26
  return new Response('Not found', { status: 404 })
29
- },
27
+ }),
28
+ }
29
+
30
+ const server = Bun.serve({
31
+ hostname: import.meta.env.HOST ?? '0.0.0.0',
32
+ port: import.meta.env.PORT ?? 4000,
33
+ routes,
30
34
  error(error) {
31
35
  logger.error(error)
32
36
  return new Response('Internal Server Error', { status: 500 })
@@ -0,0 +1,10 @@
1
+ import type { RequestHeadersPluginContext, ResponseHeadersPluginContext } from '@orpc/server/plugins'
2
+ import type { Auth } from '@server/auth'
3
+ import type { Database } from '@server/database'
4
+ import type { Logger } from '@server/utils/logger'
5
+
6
+ export interface Context extends RequestHeadersPluginContext, ResponseHeadersPluginContext {
7
+ auth: Auth
8
+ db: Database
9
+ logger: Logger
10
+ }
@@ -0,0 +1,34 @@
1
+ import { onError, ORPCError, type Router } from '@orpc/server'
2
+ import { RPCHandler } from '@orpc/server/fetch'
3
+ import {
4
+ RequestHeadersPlugin,
5
+ ResponseHeadersPlugin,
6
+ } from '@orpc/server/plugins'
7
+ import { APIError } from 'better-auth/api'
8
+
9
+ import type { Context } from './context'
10
+
11
+ export function createRpcHandler<T extends Context>(router: Router<any, T>) {
12
+ return new RPCHandler<T>(router, {
13
+ plugins: [
14
+ new RequestHeadersPlugin(),
15
+ new ResponseHeadersPlugin(),
16
+ ],
17
+ clientInterceptors: [
18
+ onError((error, { context }) => {
19
+ if (error instanceof APIError) {
20
+ throw new ORPCError(error.body?.code ?? 'INTERNAL_SERVER_ERROR', {
21
+ status: error.statusCode,
22
+ message: error.body?.message,
23
+ })
24
+ }
25
+
26
+ if (error instanceof ORPCError) {
27
+ throw error
28
+ }
29
+
30
+ context.logger.error(error)
31
+ }),
32
+ ],
33
+ })
34
+ }
@@ -0,0 +1,18 @@
1
+ import { os } from '@orpc/server'
2
+ import { authMiddleware } from '@server/orpc/middlewares'
3
+
4
+ import type { Context } from './context'
5
+
6
+ export { createRpcHandler } from './handler'
7
+
8
+ export type { Context }
9
+
10
+ export const pub = os
11
+ .$context<Context>()
12
+ .errors({
13
+ UNAUTHORIZED: { status: 401 },
14
+ })
15
+
16
+ /** @beta */
17
+ export const authed = pub
18
+ .use(authMiddleware)
@@ -0,0 +1,23 @@
1
+ import { ORPCError, os } from '@orpc/server'
2
+ import type { Context } from '@server/orpc'
3
+
4
+ export const authMiddleware = os
5
+ .$context<Context>()
6
+ .middleware(async ({ context, next }) => {
7
+ const { response: session, headers } = await context.auth.api.getSession({
8
+ headers: context.reqHeaders,
9
+ returnHeaders: true,
10
+ })
11
+
12
+ headers.forEach((v, k) => context.resHeaders?.append(k, v))
13
+
14
+ if (!session) {
15
+ throw new ORPCError('UNAUTHORIZED')
16
+ }
17
+
18
+ return next({
19
+ context: {
20
+ user: session.user,
21
+ },
22
+ })
23
+ })
@@ -0,0 +1,19 @@
1
+ import { authed, pub } from '@server/orpc'
2
+ import * as v from 'valibot'
3
+
4
+ export type { RouterClient } from '@orpc/server'
5
+
6
+ export const router = {
7
+ public: pub
8
+ .input(v.any())
9
+ .handler(async () => {
10
+ return 'public'
11
+ }),
12
+ private: authed
13
+ .input(v.any())
14
+ .handler(async () => {
15
+ return 'private'
16
+ }),
17
+ }
18
+
19
+ export type Router = typeof router
@@ -0,0 +1,26 @@
1
+ export function cors(handler: (req: Request) => Promise<Response>) {
2
+ const allowedOrigins = import.meta.env.ALLOWED_ORIGINS.split(',')
3
+
4
+ return async (req: Request) => {
5
+ const origin = req.headers.get('origin') ?? ''
6
+
7
+ if (!origin || !allowedOrigins.includes(origin)) {
8
+ return new Response('Origin not allowed', { status: 403 })
9
+ }
10
+
11
+ const response = req.method === 'OPTIONS'
12
+ ? new Response(undefined, { status: 204 })
13
+ : await handler(req)
14
+
15
+ if (req.method === 'OPTIONS') {
16
+ response.headers.append('Access-Control-Allow-Headers', 'Content-Type')
17
+ response.headers.append('Access-Control-Allow-Methods', 'GET, HEAD, PUT, POST, DELETE, PATCH')
18
+ response.headers.append('Access-Control-Max-Age', '7200') // 2 hours https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Max-Age
19
+ }
20
+
21
+ response.headers.append('Access-Control-Allow-Credentials', 'true')
22
+ response.headers.append('Access-Control-Allow-Origin', origin)
23
+
24
+ return response
25
+ }
26
+ }
@@ -2,8 +2,8 @@
2
2
  "extends": "@kevinmarrec/tsconfig",
3
3
  "compilerOptions": {
4
4
  "paths": {
5
- "@backend/*": ["./backend/src/*"],
6
- "@frontend/*": ["./frontend/src/*"]
5
+ "@client/*": ["./client/src/*"],
6
+ "@server/*": ["./server/src/*"]
7
7
  }
8
8
  },
9
9
  "exclude": ["**/dist/**"]
@@ -1,4 +0,0 @@
1
- BETTER_AUTH_SECRET=foo
2
- BETTER_AUTH_URL=http://localhost:5137
3
- DATABASE_URL=.db
4
- NODE_ENV=development
@@ -1,65 +0,0 @@
1
- import type { Auth } from '@backend/auth'
2
- import type { Database } from '@backend/database'
3
- import { requiredAuthMiddleware } from '@backend/orpc/middlewares'
4
- import type { Logger } from '@backend/utils/logger'
5
- import { onError, ORPCError, os, type Router } from '@orpc/server'
6
- import { RPCHandler } from '@orpc/server/fetch'
7
- import {
8
- CORSPlugin,
9
- RequestHeadersPlugin,
10
- type RequestHeadersPluginContext,
11
- ResponseHeadersPlugin,
12
- type ResponseHeadersPluginContext,
13
- } from '@orpc/server/plugins'
14
- import { APIError } from 'better-auth/api'
15
-
16
- /* Context */
17
-
18
- export interface Context extends RequestHeadersPluginContext, ResponseHeadersPluginContext {
19
- auth: Auth
20
- db: Database
21
- logger: Logger
22
- }
23
-
24
- /* RPC Handler */
25
-
26
- export function createRpcHandler<T extends Context>(router: Router<any, T>) {
27
- return new RPCHandler<T>(router, {
28
- plugins: [
29
- new CORSPlugin({
30
- credentials: true,
31
- maxAge: 7200, // 2 hours https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Max-Age
32
- }),
33
- new RequestHeadersPlugin(),
34
- new ResponseHeadersPlugin(),
35
- ],
36
- clientInterceptors: [
37
- onError((error, { context }) => {
38
- if (error instanceof APIError) {
39
- throw new ORPCError(error.body?.code ?? 'INTERNAL_SERVER_ERROR', {
40
- status: error.statusCode,
41
- message: error.body?.message,
42
- })
43
- }
44
-
45
- if (error instanceof ORPCError) {
46
- throw error
47
- }
48
-
49
- context.logger.error(error)
50
- }),
51
- ],
52
- })
53
- }
54
-
55
- /* Builder */
56
-
57
- export const pub = os
58
- .$context<Context>()
59
- .errors({
60
- UNAUTHORIZED: { status: 401 },
61
- })
62
-
63
- /** @beta */
64
- export const authed = pub
65
- .use(requiredAuthMiddleware)
@@ -1,33 +0,0 @@
1
- import type { Context } from '@backend/orpc'
2
- import { ORPCError, os } from '@orpc/server'
3
-
4
- export const authMiddleware = os
5
- .$context<Context>()
6
- .middleware(async ({ context, next }) => {
7
- const { headers, response: session } = await context.auth.api.getSession({
8
- headers: context.reqHeaders ?? new Headers(),
9
- returnHeaders: true,
10
- })
11
-
12
- headers.forEach((v, k) => context.resHeaders?.append(k, v))
13
-
14
- return next({
15
- context: {
16
- user: session ? session.user : null,
17
- },
18
- })
19
- })
20
-
21
- export const requiredAuthMiddleware = authMiddleware
22
- .concat(async ({ context, next }) => {
23
- if (!context.user) {
24
- throw new ORPCError('UNAUTHORIZED')
25
- }
26
-
27
- return next({
28
- context: {
29
- user: context.user,
30
- },
31
- })
32
- },
33
- )
@@ -1,54 +0,0 @@
1
- import { pub } from '@backend/orpc'
2
- import { authMiddleware } from '@backend/orpc/middlewares'
3
- import * as v from 'valibot'
4
-
5
- export const getCurrentUser = pub
6
- .use(authMiddleware)
7
- .handler(async ({ context }) => context.user)
8
-
9
- export const signUp = pub
10
- .input(v.object({
11
- name: v.string(),
12
- email: v.pipe(v.string(), v.email()),
13
- password: v.string(),
14
- rememberMe: v.optional(v.boolean(), true),
15
- }))
16
- .handler(async ({ input, context: { resHeaders, auth } }) => {
17
- const { headers, response } = await auth.api.signUpEmail({
18
- body: input,
19
- returnHeaders: true,
20
- })
21
-
22
- headers.forEach((v, k) => resHeaders?.append(k, v))
23
-
24
- return response.user
25
- })
26
-
27
- export const signIn = pub
28
- .input(v.object({
29
- email: v.pipe(v.string(), v.email()),
30
- password: v.string(),
31
- rememberMe: v.optional(v.boolean(), true),
32
- }))
33
- .handler(async ({ input, context: { resHeaders, auth } }) => {
34
- const { headers, response } = await auth.api.signInEmail({
35
- body: input,
36
- returnHeaders: true,
37
- })
38
-
39
- headers.forEach((v, k) => resHeaders?.append(k, v))
40
-
41
- return response.user
42
- })
43
-
44
- export const signOut = pub
45
- .handler(async ({ context: { auth, reqHeaders, resHeaders } }) => {
46
- const { headers, response } = await auth.api.signOut({
47
- headers: reqHeaders ?? new Headers(),
48
- returnHeaders: true,
49
- })
50
-
51
- headers.forEach((v, k) => resHeaders?.append(k, v))
52
-
53
- return response
54
- })
@@ -1,9 +0,0 @@
1
- import * as auth from './auth'
2
-
3
- export type { RouterClient } from '@orpc/server'
4
-
5
- export const router = {
6
- auth,
7
- }
8
-
9
- export type Router = typeof router
@@ -1 +0,0 @@
1
- VITE_API_URL=http://localhost:4000/rpc
@@ -1,31 +0,0 @@
1
- import { orpc } from '@frontend/lib/orpc'
2
- import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
3
-
4
- export function useAuth() {
5
- const qc = useQueryClient()
6
-
7
- const { data: user } = useQuery(orpc.auth.getCurrentUser.queryOptions({
8
- retry: false,
9
- }))
10
-
11
- const signUp = useMutation(orpc.auth.signUp.mutationOptions({
12
- onSuccess: () => qc.invalidateQueries({ queryKey: orpc.auth.getCurrentUser.queryKey() }),
13
- }))
14
-
15
- const signIn = useMutation(orpc.auth.signIn.mutationOptions({
16
- onSuccess: () => qc.invalidateQueries({ queryKey: orpc.auth.getCurrentUser.queryKey() }),
17
- }))
18
-
19
- const signOut = useMutation(orpc.auth.signOut.mutationOptions({
20
- onSuccess: () => {
21
- qc.setQueryData<null>(orpc.auth.getCurrentUser.queryKey(), null)
22
- },
23
- }))
24
-
25
- return {
26
- user,
27
- signUp,
28
- signIn,
29
- signOut,
30
- }
31
- }
@@ -1,8 +0,0 @@
1
- import concurrently, { type ConcurrentlyCommandInput } from 'concurrently'
2
-
3
- const commandInputs: ConcurrentlyCommandInput[] = [
4
- { name: 'backend', command: `bun --cwd backend dev | pino-pretty`, prefixColor: 'blue' },
5
- { name: 'frontend', command: `bun --cwd frontend dev`, prefixColor: 'green' },
6
- ]
7
-
8
- concurrently(commandInputs)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes