@kevinmarrec/create-app 0.3.0 → 0.5.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/index.js +1 -1
- package/package.json +11 -11
- package/template/backend/.env.development +3 -1
- package/template/backend/package.json +9 -8
- package/template/backend/src/auth/index.ts +23 -0
- package/template/backend/src/core/builder.ts +14 -0
- package/template/backend/src/core/context.ts +10 -0
- package/template/backend/src/core/index.ts +33 -0
- package/template/backend/src/core/middlewares/auth.ts +37 -0
- package/template/backend/src/core/router/auth.ts +55 -0
- package/template/backend/src/{router → core/router}/index.ts +2 -2
- package/template/backend/src/database/drizzle.config.ts +9 -0
- package/template/backend/src/database/index.ts +4 -4
- package/template/backend/src/database/migrations/0000_violet_enchantress.sql +49 -0
- package/template/backend/src/database/migrations/meta/0000_snapshot.json +292 -7
- package/template/backend/src/database/migrations/meta/_journal.json +2 -2
- package/template/backend/src/database/schema/accounts.ts +20 -0
- package/template/backend/src/database/schema/index.ts +3 -0
- package/template/backend/src/database/schema/sessions.ts +15 -0
- package/template/backend/src/database/schema/users.ts +7 -3
- package/template/backend/src/database/schema/verifications.ts +11 -0
- package/template/backend/{env.d.ts → src/env.d.ts} +2 -0
- package/template/backend/src/{server.ts → main.ts} +10 -18
- package/template/backend/src/utils/headers.ts +7 -0
- package/template/backend/src/{logger.ts → utils/logger.ts} +1 -3
- package/template/frontend/package.json +12 -13
- package/template/frontend/src/App.vue +96 -10
- package/template/frontend/src/composables/auth.ts +31 -0
- package/template/frontend/src/composables/index.ts +1 -0
- package/template/frontend/src/lib/orpc.ts +6 -1
- package/template/knip.config.ts +1 -1
- package/template/package.json +9 -9
- package/template/backend/src/config/database.ts +0 -11
- package/template/backend/src/config/drizzle.ts +0 -10
- package/template/backend/src/config/logger.ts +0 -19
- package/template/backend/src/config/server.ts +0 -40
- package/template/backend/src/database/migrations/0000_white_bishop.sql +0 -6
- package/template/backend/src/lib/orpc.ts +0 -10
- package/template/backend/src/router/welcome.ts +0 -11
- package/template/frontend/src/queries/index.ts +0 -1
- package/template/frontend/src/queries/welcome.ts +0 -10
- package/template/frontend/src/utils.ts +0 -1
- /package/template/frontend/{env.d.ts → src/env.d.ts} +0 -0
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kevinmarrec/create-app",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"description": "CLI that scaffolds an opinionated Bun & Vue fullstack application.",
|
|
6
6
|
"author": "Kevin Marrec <kevin@marrec.io>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -49,19 +49,19 @@
|
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@faker-js/faker": "^10.0.0",
|
|
52
|
-
"@kevinmarrec/eslint-config": "^1.
|
|
53
|
-
"@kevinmarrec/stylelint-config": "^1.
|
|
54
|
-
"@kevinmarrec/tsconfig": "^1.
|
|
55
|
-
"@types/bun": "^1.2.
|
|
52
|
+
"@kevinmarrec/eslint-config": "^1.4.0",
|
|
53
|
+
"@kevinmarrec/stylelint-config": "^1.2.0",
|
|
54
|
+
"@kevinmarrec/tsconfig": "^1.1.0",
|
|
55
|
+
"@types/bun": "^1.2.22",
|
|
56
56
|
"@vitest/coverage-v8": "^3.2.4",
|
|
57
57
|
"bumpp": "^10.2.3",
|
|
58
|
-
"eslint": "^9.
|
|
59
|
-
"knip": "^5.63.
|
|
60
|
-
"stylelint": "^16.
|
|
61
|
-
"taze": "^19.
|
|
62
|
-
"tsdown": "^0.
|
|
58
|
+
"eslint": "^9.36.0",
|
|
59
|
+
"knip": "^5.63.1",
|
|
60
|
+
"stylelint": "^16.24.0",
|
|
61
|
+
"taze": "^19.7.0",
|
|
62
|
+
"tsdown": "^0.15.2",
|
|
63
63
|
"typescript": "^5.9.2",
|
|
64
64
|
"vitest": "^3.2.4",
|
|
65
|
-
"vue-tsc": "^3.0.
|
|
65
|
+
"vue-tsc": "^3.0.7"
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -3,20 +3,21 @@
|
|
|
3
3
|
"type": "module",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
|
-
"dev": "bun --watch --no-clear-screen src/
|
|
7
|
-
"build": "bun build src/
|
|
8
|
-
"db:generate": "bun --bun run drizzle-kit generate --config src/
|
|
9
|
-
"db:migrate": "bun --bun run drizzle-kit migrate --config src/
|
|
6
|
+
"dev": "bun --watch --no-clear-screen src/main.ts",
|
|
7
|
+
"build": "bun build src/main.ts --compile --minify --sourcemap --outfile dist/server",
|
|
8
|
+
"db:generate": "bun --bun run drizzle-kit generate --config src/database/drizzle.config.ts",
|
|
9
|
+
"db:migrate": "bun --bun run drizzle-kit migrate --config src/database/drizzle.config.ts"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@orpc/server": "^1.8.
|
|
12
|
+
"@orpc/server": "^1.8.9",
|
|
13
|
+
"better-auth": "^1.3.12",
|
|
13
14
|
"drizzle-orm": "^0.44.5",
|
|
14
|
-
"pino": "^9.
|
|
15
|
+
"pino": "^9.10.0",
|
|
15
16
|
"valibot": "^1.1.0"
|
|
16
17
|
},
|
|
17
18
|
"devDependencies": {
|
|
18
|
-
"@libsql/client": "^0.15.
|
|
19
|
-
"@types/bun": "^1.2.
|
|
19
|
+
"@libsql/client": "^0.15.15",
|
|
20
|
+
"@types/bun": "^1.2.22",
|
|
20
21
|
"drizzle-kit": "^0.31.4",
|
|
21
22
|
"pino-pretty": "^13.1.1"
|
|
22
23
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { betterAuth } from 'better-auth'
|
|
2
|
+
import { type DB, drizzleAdapter } from 'better-auth/adapters/drizzle'
|
|
3
|
+
import type { BaseLogger } from 'pino'
|
|
4
|
+
|
|
5
|
+
export function createBetterAuth({ db, logger }: { db: DB, logger: BaseLogger }) {
|
|
6
|
+
return betterAuth({
|
|
7
|
+
database: drizzleAdapter(db, {
|
|
8
|
+
provider: 'sqlite',
|
|
9
|
+
usePlural: true,
|
|
10
|
+
}),
|
|
11
|
+
logger: {
|
|
12
|
+
log: (level, message) => {
|
|
13
|
+
logger[level](message)
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
emailAndPassword: {
|
|
17
|
+
enabled: true,
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type Auth = ReturnType<typeof createBetterAuth>
|
|
23
|
+
export type Session = Auth['$Infer']['Session']
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { os } from '@orpc/server'
|
|
2
|
+
|
|
3
|
+
import type { Context } from './context'
|
|
4
|
+
import { requiredAuthMiddleware } from './middlewares/auth'
|
|
5
|
+
|
|
6
|
+
export const pub = os
|
|
7
|
+
.$context<Context>()
|
|
8
|
+
.errors({
|
|
9
|
+
UNAUTHORIZED: { status: 401 },
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
/** @beta */
|
|
13
|
+
export const authed = pub
|
|
14
|
+
.use(requiredAuthMiddleware)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Auth } from '@backend/auth'
|
|
2
|
+
import type { Database } from '@backend/database'
|
|
3
|
+
import type { Logger } from '@backend/utils/logger'
|
|
4
|
+
import type { RequestHeadersPluginContext, ResponseHeadersPluginContext } from '@orpc/server/plugins'
|
|
5
|
+
|
|
6
|
+
export interface Context extends RequestHeadersPluginContext, ResponseHeadersPluginContext {
|
|
7
|
+
auth: Auth
|
|
8
|
+
db: Database
|
|
9
|
+
logger: Logger
|
|
10
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { onError, ORPCError } from '@orpc/server'
|
|
2
|
+
import { RPCHandler } from '@orpc/server/fetch'
|
|
3
|
+
import { CORSPlugin, RequestHeadersPlugin, ResponseHeadersPlugin } from '@orpc/server/plugins'
|
|
4
|
+
import { APIError } from 'better-auth/api'
|
|
5
|
+
|
|
6
|
+
import { router } from './router'
|
|
7
|
+
|
|
8
|
+
export const rpcHandler = new RPCHandler(router, {
|
|
9
|
+
plugins: [
|
|
10
|
+
new CORSPlugin({
|
|
11
|
+
credentials: true,
|
|
12
|
+
maxAge: 7200, // 2 hours https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Max-Age
|
|
13
|
+
}),
|
|
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
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Session } from '@backend/auth'
|
|
2
|
+
import type { Context } from '@backend/core/context'
|
|
3
|
+
import { copyHeaders } from '@backend/utils/headers'
|
|
4
|
+
import { ORPCError, os } from '@orpc/server'
|
|
5
|
+
|
|
6
|
+
export const authMiddleware = os
|
|
7
|
+
.$context<Context>()
|
|
8
|
+
.middleware(async ({ context, next }) => {
|
|
9
|
+
const response = await context.auth.api.getSession({
|
|
10
|
+
headers: context.reqHeaders ?? new Headers(),
|
|
11
|
+
asResponse: true,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const session = await response.json() as Session | null
|
|
15
|
+
|
|
16
|
+
copyHeaders(response.headers, context.resHeaders)
|
|
17
|
+
|
|
18
|
+
return next({
|
|
19
|
+
context: {
|
|
20
|
+
user: session ? session.user : null,
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export const requiredAuthMiddleware = authMiddleware
|
|
26
|
+
.concat(async ({ context, next }) => {
|
|
27
|
+
if (!context.user) {
|
|
28
|
+
throw new ORPCError('UNAUTHORIZED')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return next({
|
|
32
|
+
context: {
|
|
33
|
+
user: context.user,
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
},
|
|
37
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { pub } from '@backend/core/builder'
|
|
2
|
+
import { authMiddleware } from '@backend/core/middlewares/auth'
|
|
3
|
+
import { copyHeaders } from '@backend/utils/headers'
|
|
4
|
+
import * as v from 'valibot'
|
|
5
|
+
|
|
6
|
+
export const getCurrentUser = pub
|
|
7
|
+
.use(authMiddleware)
|
|
8
|
+
.handler(async ({ context }) => context.user)
|
|
9
|
+
|
|
10
|
+
export const signUp = pub
|
|
11
|
+
.input(v.object({
|
|
12
|
+
name: v.string(),
|
|
13
|
+
email: v.pipe(v.string(), v.email()),
|
|
14
|
+
password: v.string(),
|
|
15
|
+
rememberMe: v.optional(v.boolean(), true),
|
|
16
|
+
}))
|
|
17
|
+
.handler(async ({ input, context: { resHeaders, auth } }) => {
|
|
18
|
+
const { headers, response } = await auth.api.signUpEmail({
|
|
19
|
+
body: input,
|
|
20
|
+
returnHeaders: true,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
copyHeaders(headers, resHeaders)
|
|
24
|
+
|
|
25
|
+
return response.user
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const signIn = pub
|
|
29
|
+
.input(v.object({
|
|
30
|
+
email: v.pipe(v.string(), v.email()),
|
|
31
|
+
password: v.string(),
|
|
32
|
+
rememberMe: v.optional(v.boolean(), true),
|
|
33
|
+
}))
|
|
34
|
+
.handler(async ({ input, context: { resHeaders, auth } }) => {
|
|
35
|
+
const { headers, response } = await auth.api.signInEmail({
|
|
36
|
+
body: input,
|
|
37
|
+
returnHeaders: true,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
copyHeaders(headers, resHeaders)
|
|
41
|
+
|
|
42
|
+
return response.user
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
export const signOut = pub
|
|
46
|
+
.handler(async ({ context: { auth, reqHeaders, resHeaders } }) => {
|
|
47
|
+
const { headers, response } = await auth.api.signOut({
|
|
48
|
+
headers: reqHeaders ?? new Headers(),
|
|
49
|
+
returnHeaders: true,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
copyHeaders(headers, resHeaders)
|
|
53
|
+
|
|
54
|
+
return response
|
|
55
|
+
})
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { logger } from '@backend/logger'
|
|
1
|
+
import { logger } from '@backend/utils/logger'
|
|
3
2
|
import { drizzle } from 'drizzle-orm/bun-sqlite'
|
|
4
3
|
|
|
5
4
|
import * as schema from './schema'
|
|
6
5
|
|
|
7
|
-
export const db = drizzle(
|
|
6
|
+
export const db = drizzle(import.meta.env.DATABASE_URL, {
|
|
7
|
+
casing: 'snake_case',
|
|
8
8
|
schema,
|
|
9
9
|
logger: {
|
|
10
10
|
logQuery: (query, params) => {
|
|
11
11
|
let msg = `[SQL] ${query}`
|
|
12
12
|
msg += params.length ? ` [${params}]` : ''
|
|
13
|
-
logger.
|
|
13
|
+
logger.debug(msg)
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
16
|
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
CREATE TABLE `accounts` (
|
|
2
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
3
|
+
`user_id` text NOT NULL,
|
|
4
|
+
`account_id` text NOT NULL,
|
|
5
|
+
`provider_id` text NOT NULL,
|
|
6
|
+
`id_token` text,
|
|
7
|
+
`access_token` text,
|
|
8
|
+
`access_token_expires_at` integer,
|
|
9
|
+
`refresh_token` text,
|
|
10
|
+
`refresh_token_expires_at` integer,
|
|
11
|
+
`scope` text,
|
|
12
|
+
`password` text,
|
|
13
|
+
`created_at` integer NOT NULL,
|
|
14
|
+
`updated_at` integer NOT NULL,
|
|
15
|
+
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
|
|
16
|
+
);
|
|
17
|
+
--> statement-breakpoint
|
|
18
|
+
CREATE TABLE `sessions` (
|
|
19
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
20
|
+
`user_id` text NOT NULL,
|
|
21
|
+
`token` text NOT NULL,
|
|
22
|
+
`expires_at` integer NOT NULL,
|
|
23
|
+
`ip_address` text,
|
|
24
|
+
`user_agent` text,
|
|
25
|
+
`created_at` integer NOT NULL,
|
|
26
|
+
`updated_at` integer NOT NULL,
|
|
27
|
+
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
|
|
28
|
+
);
|
|
29
|
+
--> statement-breakpoint
|
|
30
|
+
CREATE UNIQUE INDEX `sessions_token_unique` ON `sessions` (`token`);--> statement-breakpoint
|
|
31
|
+
CREATE TABLE `users` (
|
|
32
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
33
|
+
`name` text NOT NULL,
|
|
34
|
+
`email` text NOT NULL,
|
|
35
|
+
`email_verified` integer DEFAULT false NOT NULL,
|
|
36
|
+
`image` text,
|
|
37
|
+
`created_at` integer NOT NULL,
|
|
38
|
+
`updated_at` integer NOT NULL
|
|
39
|
+
);
|
|
40
|
+
--> statement-breakpoint
|
|
41
|
+
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint
|
|
42
|
+
CREATE TABLE `verifications` (
|
|
43
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
44
|
+
`identifier` text NOT NULL,
|
|
45
|
+
`value` text NOT NULL,
|
|
46
|
+
`expires_at` integer NOT NULL,
|
|
47
|
+
`created_at` integer NOT NULL,
|
|
48
|
+
`updated_at` integer NOT NULL
|
|
49
|
+
);
|
|
@@ -1,18 +1,221 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "6",
|
|
3
3
|
"dialect": "sqlite",
|
|
4
|
-
"id": "
|
|
4
|
+
"id": "28ff26ce-b417-459e-b30f-4ec14ab62876",
|
|
5
5
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
6
6
|
"tables": {
|
|
7
|
+
"accounts": {
|
|
8
|
+
"name": "accounts",
|
|
9
|
+
"columns": {
|
|
10
|
+
"id": {
|
|
11
|
+
"name": "id",
|
|
12
|
+
"type": "text",
|
|
13
|
+
"primaryKey": true,
|
|
14
|
+
"notNull": true,
|
|
15
|
+
"autoincrement": false
|
|
16
|
+
},
|
|
17
|
+
"user_id": {
|
|
18
|
+
"name": "user_id",
|
|
19
|
+
"type": "text",
|
|
20
|
+
"primaryKey": false,
|
|
21
|
+
"notNull": true,
|
|
22
|
+
"autoincrement": false
|
|
23
|
+
},
|
|
24
|
+
"account_id": {
|
|
25
|
+
"name": "account_id",
|
|
26
|
+
"type": "text",
|
|
27
|
+
"primaryKey": false,
|
|
28
|
+
"notNull": true,
|
|
29
|
+
"autoincrement": false
|
|
30
|
+
},
|
|
31
|
+
"provider_id": {
|
|
32
|
+
"name": "provider_id",
|
|
33
|
+
"type": "text",
|
|
34
|
+
"primaryKey": false,
|
|
35
|
+
"notNull": true,
|
|
36
|
+
"autoincrement": false
|
|
37
|
+
},
|
|
38
|
+
"id_token": {
|
|
39
|
+
"name": "id_token",
|
|
40
|
+
"type": "text",
|
|
41
|
+
"primaryKey": false,
|
|
42
|
+
"notNull": false,
|
|
43
|
+
"autoincrement": false
|
|
44
|
+
},
|
|
45
|
+
"access_token": {
|
|
46
|
+
"name": "access_token",
|
|
47
|
+
"type": "text",
|
|
48
|
+
"primaryKey": false,
|
|
49
|
+
"notNull": false,
|
|
50
|
+
"autoincrement": false
|
|
51
|
+
},
|
|
52
|
+
"access_token_expires_at": {
|
|
53
|
+
"name": "access_token_expires_at",
|
|
54
|
+
"type": "integer",
|
|
55
|
+
"primaryKey": false,
|
|
56
|
+
"notNull": false,
|
|
57
|
+
"autoincrement": false
|
|
58
|
+
},
|
|
59
|
+
"refresh_token": {
|
|
60
|
+
"name": "refresh_token",
|
|
61
|
+
"type": "text",
|
|
62
|
+
"primaryKey": false,
|
|
63
|
+
"notNull": false,
|
|
64
|
+
"autoincrement": false
|
|
65
|
+
},
|
|
66
|
+
"refresh_token_expires_at": {
|
|
67
|
+
"name": "refresh_token_expires_at",
|
|
68
|
+
"type": "integer",
|
|
69
|
+
"primaryKey": false,
|
|
70
|
+
"notNull": false,
|
|
71
|
+
"autoincrement": false
|
|
72
|
+
},
|
|
73
|
+
"scope": {
|
|
74
|
+
"name": "scope",
|
|
75
|
+
"type": "text",
|
|
76
|
+
"primaryKey": false,
|
|
77
|
+
"notNull": false,
|
|
78
|
+
"autoincrement": false
|
|
79
|
+
},
|
|
80
|
+
"password": {
|
|
81
|
+
"name": "password",
|
|
82
|
+
"type": "text",
|
|
83
|
+
"primaryKey": false,
|
|
84
|
+
"notNull": false,
|
|
85
|
+
"autoincrement": false
|
|
86
|
+
},
|
|
87
|
+
"created_at": {
|
|
88
|
+
"name": "created_at",
|
|
89
|
+
"type": "integer",
|
|
90
|
+
"primaryKey": false,
|
|
91
|
+
"notNull": true,
|
|
92
|
+
"autoincrement": false
|
|
93
|
+
},
|
|
94
|
+
"updated_at": {
|
|
95
|
+
"name": "updated_at",
|
|
96
|
+
"type": "integer",
|
|
97
|
+
"primaryKey": false,
|
|
98
|
+
"notNull": true,
|
|
99
|
+
"autoincrement": false
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"indexes": {},
|
|
103
|
+
"foreignKeys": {
|
|
104
|
+
"accounts_user_id_users_id_fk": {
|
|
105
|
+
"name": "accounts_user_id_users_id_fk",
|
|
106
|
+
"tableFrom": "accounts",
|
|
107
|
+
"tableTo": "users",
|
|
108
|
+
"columnsFrom": [
|
|
109
|
+
"user_id"
|
|
110
|
+
],
|
|
111
|
+
"columnsTo": [
|
|
112
|
+
"id"
|
|
113
|
+
],
|
|
114
|
+
"onDelete": "cascade",
|
|
115
|
+
"onUpdate": "no action"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"compositePrimaryKeys": {},
|
|
119
|
+
"uniqueConstraints": {},
|
|
120
|
+
"checkConstraints": {}
|
|
121
|
+
},
|
|
122
|
+
"sessions": {
|
|
123
|
+
"name": "sessions",
|
|
124
|
+
"columns": {
|
|
125
|
+
"id": {
|
|
126
|
+
"name": "id",
|
|
127
|
+
"type": "text",
|
|
128
|
+
"primaryKey": true,
|
|
129
|
+
"notNull": true,
|
|
130
|
+
"autoincrement": false
|
|
131
|
+
},
|
|
132
|
+
"user_id": {
|
|
133
|
+
"name": "user_id",
|
|
134
|
+
"type": "text",
|
|
135
|
+
"primaryKey": false,
|
|
136
|
+
"notNull": true,
|
|
137
|
+
"autoincrement": false
|
|
138
|
+
},
|
|
139
|
+
"token": {
|
|
140
|
+
"name": "token",
|
|
141
|
+
"type": "text",
|
|
142
|
+
"primaryKey": false,
|
|
143
|
+
"notNull": true,
|
|
144
|
+
"autoincrement": false
|
|
145
|
+
},
|
|
146
|
+
"expires_at": {
|
|
147
|
+
"name": "expires_at",
|
|
148
|
+
"type": "integer",
|
|
149
|
+
"primaryKey": false,
|
|
150
|
+
"notNull": true,
|
|
151
|
+
"autoincrement": false
|
|
152
|
+
},
|
|
153
|
+
"ip_address": {
|
|
154
|
+
"name": "ip_address",
|
|
155
|
+
"type": "text",
|
|
156
|
+
"primaryKey": false,
|
|
157
|
+
"notNull": false,
|
|
158
|
+
"autoincrement": false
|
|
159
|
+
},
|
|
160
|
+
"user_agent": {
|
|
161
|
+
"name": "user_agent",
|
|
162
|
+
"type": "text",
|
|
163
|
+
"primaryKey": false,
|
|
164
|
+
"notNull": false,
|
|
165
|
+
"autoincrement": false
|
|
166
|
+
},
|
|
167
|
+
"created_at": {
|
|
168
|
+
"name": "created_at",
|
|
169
|
+
"type": "integer",
|
|
170
|
+
"primaryKey": false,
|
|
171
|
+
"notNull": true,
|
|
172
|
+
"autoincrement": false
|
|
173
|
+
},
|
|
174
|
+
"updated_at": {
|
|
175
|
+
"name": "updated_at",
|
|
176
|
+
"type": "integer",
|
|
177
|
+
"primaryKey": false,
|
|
178
|
+
"notNull": true,
|
|
179
|
+
"autoincrement": false
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"indexes": {
|
|
183
|
+
"sessions_token_unique": {
|
|
184
|
+
"name": "sessions_token_unique",
|
|
185
|
+
"columns": [
|
|
186
|
+
"token"
|
|
187
|
+
],
|
|
188
|
+
"isUnique": true
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
"foreignKeys": {
|
|
192
|
+
"sessions_user_id_users_id_fk": {
|
|
193
|
+
"name": "sessions_user_id_users_id_fk",
|
|
194
|
+
"tableFrom": "sessions",
|
|
195
|
+
"tableTo": "users",
|
|
196
|
+
"columnsFrom": [
|
|
197
|
+
"user_id"
|
|
198
|
+
],
|
|
199
|
+
"columnsTo": [
|
|
200
|
+
"id"
|
|
201
|
+
],
|
|
202
|
+
"onDelete": "cascade",
|
|
203
|
+
"onUpdate": "no action"
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
"compositePrimaryKeys": {},
|
|
207
|
+
"uniqueConstraints": {},
|
|
208
|
+
"checkConstraints": {}
|
|
209
|
+
},
|
|
7
210
|
"users": {
|
|
8
211
|
"name": "users",
|
|
9
212
|
"columns": {
|
|
10
213
|
"id": {
|
|
11
214
|
"name": "id",
|
|
12
|
-
"type": "
|
|
215
|
+
"type": "text",
|
|
13
216
|
"primaryKey": true,
|
|
14
217
|
"notNull": true,
|
|
15
|
-
"autoincrement":
|
|
218
|
+
"autoincrement": false
|
|
16
219
|
},
|
|
17
220
|
"name": {
|
|
18
221
|
"name": "name",
|
|
@@ -21,15 +224,97 @@
|
|
|
21
224
|
"notNull": true,
|
|
22
225
|
"autoincrement": false
|
|
23
226
|
},
|
|
24
|
-
"
|
|
25
|
-
"name": "
|
|
227
|
+
"email": {
|
|
228
|
+
"name": "email",
|
|
229
|
+
"type": "text",
|
|
230
|
+
"primaryKey": false,
|
|
231
|
+
"notNull": true,
|
|
232
|
+
"autoincrement": false
|
|
233
|
+
},
|
|
234
|
+
"email_verified": {
|
|
235
|
+
"name": "email_verified",
|
|
236
|
+
"type": "integer",
|
|
237
|
+
"primaryKey": false,
|
|
238
|
+
"notNull": true,
|
|
239
|
+
"autoincrement": false,
|
|
240
|
+
"default": false
|
|
241
|
+
},
|
|
242
|
+
"image": {
|
|
243
|
+
"name": "image",
|
|
244
|
+
"type": "text",
|
|
245
|
+
"primaryKey": false,
|
|
246
|
+
"notNull": false,
|
|
247
|
+
"autoincrement": false
|
|
248
|
+
},
|
|
249
|
+
"created_at": {
|
|
250
|
+
"name": "created_at",
|
|
251
|
+
"type": "integer",
|
|
252
|
+
"primaryKey": false,
|
|
253
|
+
"notNull": true,
|
|
254
|
+
"autoincrement": false
|
|
255
|
+
},
|
|
256
|
+
"updated_at": {
|
|
257
|
+
"name": "updated_at",
|
|
258
|
+
"type": "integer",
|
|
259
|
+
"primaryKey": false,
|
|
260
|
+
"notNull": true,
|
|
261
|
+
"autoincrement": false
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
"indexes": {
|
|
265
|
+
"users_email_unique": {
|
|
266
|
+
"name": "users_email_unique",
|
|
267
|
+
"columns": [
|
|
268
|
+
"email"
|
|
269
|
+
],
|
|
270
|
+
"isUnique": true
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
"foreignKeys": {},
|
|
274
|
+
"compositePrimaryKeys": {},
|
|
275
|
+
"uniqueConstraints": {},
|
|
276
|
+
"checkConstraints": {}
|
|
277
|
+
},
|
|
278
|
+
"verifications": {
|
|
279
|
+
"name": "verifications",
|
|
280
|
+
"columns": {
|
|
281
|
+
"id": {
|
|
282
|
+
"name": "id",
|
|
283
|
+
"type": "text",
|
|
284
|
+
"primaryKey": true,
|
|
285
|
+
"notNull": true,
|
|
286
|
+
"autoincrement": false
|
|
287
|
+
},
|
|
288
|
+
"identifier": {
|
|
289
|
+
"name": "identifier",
|
|
290
|
+
"type": "text",
|
|
291
|
+
"primaryKey": false,
|
|
292
|
+
"notNull": true,
|
|
293
|
+
"autoincrement": false
|
|
294
|
+
},
|
|
295
|
+
"value": {
|
|
296
|
+
"name": "value",
|
|
297
|
+
"type": "text",
|
|
298
|
+
"primaryKey": false,
|
|
299
|
+
"notNull": true,
|
|
300
|
+
"autoincrement": false
|
|
301
|
+
},
|
|
302
|
+
"expires_at": {
|
|
303
|
+
"name": "expires_at",
|
|
304
|
+
"type": "integer",
|
|
305
|
+
"primaryKey": false,
|
|
306
|
+
"notNull": true,
|
|
307
|
+
"autoincrement": false
|
|
308
|
+
},
|
|
309
|
+
"created_at": {
|
|
310
|
+
"name": "created_at",
|
|
26
311
|
"type": "integer",
|
|
27
312
|
"primaryKey": false,
|
|
28
313
|
"notNull": true,
|
|
29
314
|
"autoincrement": false
|
|
30
315
|
},
|
|
31
|
-
"
|
|
32
|
-
"name": "
|
|
316
|
+
"updated_at": {
|
|
317
|
+
"name": "updated_at",
|
|
33
318
|
"type": "integer",
|
|
34
319
|
"primaryKey": false,
|
|
35
320
|
"notNull": true,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { sqliteTable } from 'drizzle-orm/sqlite-core'
|
|
2
|
+
|
|
3
|
+
import { users } from './users'
|
|
4
|
+
|
|
5
|
+
// https://www.better-auth.com/docs/concepts/database#account
|
|
6
|
+
export const accounts = sqliteTable('accounts', t => ({
|
|
7
|
+
id: t.text().primaryKey(),
|
|
8
|
+
userId: t.text().notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
9
|
+
accountId: t.text().notNull(),
|
|
10
|
+
providerId: t.text().notNull(),
|
|
11
|
+
idToken: t.text(),
|
|
12
|
+
accessToken: t.text(),
|
|
13
|
+
accessTokenExpiresAt: t.integer({ mode: 'timestamp' }),
|
|
14
|
+
refreshToken: t.text(),
|
|
15
|
+
refreshTokenExpiresAt: t.integer({ mode: 'timestamp' }),
|
|
16
|
+
scope: t.text(),
|
|
17
|
+
password: t.text(),
|
|
18
|
+
createdAt: t.integer({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
|
19
|
+
updatedAt: t.integer({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()).$onUpdate(() => new Date()),
|
|
20
|
+
}))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { sqliteTable } from 'drizzle-orm/sqlite-core'
|
|
2
|
+
|
|
3
|
+
import { users } from './users'
|
|
4
|
+
|
|
5
|
+
// https://www.better-auth.com/docs/concepts/database#session
|
|
6
|
+
export const sessions = sqliteTable('sessions', t => ({
|
|
7
|
+
id: t.text().primaryKey(),
|
|
8
|
+
userId: t.text().notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
9
|
+
token: t.text().notNull().unique(),
|
|
10
|
+
expiresAt: t.integer({ mode: 'timestamp' }).notNull(),
|
|
11
|
+
ipAddress: t.text(),
|
|
12
|
+
userAgent: t.text(),
|
|
13
|
+
createdAt: t.integer({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
|
14
|
+
updatedAt: t.integer({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()).$onUpdate(() => new Date()),
|
|
15
|
+
}))
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { sqliteTable } from 'drizzle-orm/sqlite-core'
|
|
2
2
|
|
|
3
|
+
// https://www.better-auth.com/docs/concepts/database#user
|
|
3
4
|
export const users = sqliteTable('users', t => ({
|
|
4
|
-
id: t.
|
|
5
|
+
id: t.text().primaryKey(),
|
|
5
6
|
name: t.text().notNull(),
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
email: t.text().notNull().unique(),
|
|
8
|
+
emailVerified: t.integer({ mode: 'boolean' }).notNull().default(false),
|
|
9
|
+
image: t.text(),
|
|
10
|
+
createdAt: t.integer({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
|
11
|
+
updatedAt: t.integer({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()).$onUpdate(() => new Date()),
|
|
8
12
|
}))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { sqliteTable } from 'drizzle-orm/sqlite-core'
|
|
2
|
+
|
|
3
|
+
// https://www.better-auth.com/docs/concepts/database#verification
|
|
4
|
+
export const verifications = sqliteTable('verifications', t => ({
|
|
5
|
+
id: t.text().primaryKey(),
|
|
6
|
+
identifier: t.text().notNull(),
|
|
7
|
+
value: t.text().notNull(),
|
|
8
|
+
expiresAt: t.integer({ mode: 'timestamp' }).notNull(),
|
|
9
|
+
createdAt: t.integer({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
|
10
|
+
updatedAt: t.integer({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()).$onUpdate(() => new Date()),
|
|
11
|
+
}))
|
|
@@ -1,30 +1,20 @@
|
|
|
1
1
|
import process from 'node:process'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { CORSPlugin } from '@orpc/server/plugins'
|
|
6
|
-
|
|
7
|
-
import { cors, hostname, port } from './config/server'
|
|
3
|
+
import { createBetterAuth } from './auth'
|
|
4
|
+
import { rpcHandler } from './core'
|
|
8
5
|
import { db } from './database'
|
|
9
|
-
import { logger } from './logger'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const rpcHandler = new RPCHandler(router, {
|
|
13
|
-
plugins: [new CORSPlugin(cors)],
|
|
14
|
-
interceptors: [
|
|
15
|
-
onError((error) => {
|
|
16
|
-
logger.error(error)
|
|
17
|
-
}),
|
|
18
|
-
],
|
|
19
|
-
})
|
|
6
|
+
import { logger } from './utils/logger'
|
|
7
|
+
|
|
8
|
+
const auth = createBetterAuth({ db, logger })
|
|
20
9
|
|
|
21
10
|
const server = Bun.serve({
|
|
22
|
-
hostname,
|
|
23
|
-
port,
|
|
11
|
+
hostname: import.meta.env.HOST ?? '0.0.0.0',
|
|
12
|
+
port: import.meta.env.PORT ?? 4000,
|
|
24
13
|
async fetch(request) {
|
|
25
14
|
const { matched, response } = await rpcHandler.handle(request, {
|
|
26
15
|
prefix: '/rpc',
|
|
27
16
|
context: {
|
|
17
|
+
auth,
|
|
28
18
|
db,
|
|
29
19
|
logger,
|
|
30
20
|
},
|
|
@@ -43,6 +33,8 @@ const server = Bun.serve({
|
|
|
43
33
|
|
|
44
34
|
logger.info(`Listening on ${server.url}`)
|
|
45
35
|
|
|
36
|
+
// Graceful Shutdown
|
|
37
|
+
|
|
46
38
|
async function gracefulShutdown() {
|
|
47
39
|
logger.info('Gracefully shutting down...')
|
|
48
40
|
await server.stop()
|
|
@@ -9,26 +9,25 @@
|
|
|
9
9
|
"preview": "vite preview --open"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@kevinmarrec/vue-i18n": "^1.
|
|
13
|
-
"@orpc/client": "^1.8.
|
|
12
|
+
"@kevinmarrec/vue-i18n": "^1.1.0",
|
|
13
|
+
"@orpc/client": "^1.8.9",
|
|
14
14
|
"@orpc/tanstack-query": "1.6.4",
|
|
15
|
-
"@tanstack/vue-query": "^5.
|
|
16
|
-
"@unhead/vue": "^2.0.
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"vue": "^3.5.20"
|
|
15
|
+
"@tanstack/vue-query": "^5.89.0",
|
|
16
|
+
"@unhead/vue": "^2.0.17",
|
|
17
|
+
"unocss": "^66.5.1",
|
|
18
|
+
"vue": "^3.5.21"
|
|
20
19
|
},
|
|
21
20
|
"devDependencies": {
|
|
22
|
-
"@kevinmarrec/unocss-config": "^1.
|
|
23
|
-
"@kevinmarrec/vite-plugin-dark-mode": "^1.
|
|
21
|
+
"@kevinmarrec/unocss-config": "^1.3.0",
|
|
22
|
+
"@kevinmarrec/vite-plugin-dark-mode": "^1.1.0",
|
|
24
23
|
"@modyfi/vite-plugin-yaml": "^1.1.1",
|
|
25
|
-
"@unhead/addons": "^2.0.
|
|
26
|
-
"@unocss/vite": "^66.
|
|
24
|
+
"@unhead/addons": "^2.0.17",
|
|
25
|
+
"@unocss/vite": "^66.5.1",
|
|
27
26
|
"@vitejs/plugin-vue": "^6.0.1",
|
|
28
27
|
"beasties": "^0.3.5",
|
|
29
28
|
"rollup-plugin-visualizer": "^6.0.3",
|
|
30
|
-
"vite": "^7.1.
|
|
31
|
-
"vite-plugin-vue-devtools": "^8.0.
|
|
29
|
+
"vite": "^7.1.6",
|
|
30
|
+
"vite-plugin-vue-devtools": "^8.0.2",
|
|
32
31
|
"vite-ssg": "^28.1.0",
|
|
33
32
|
"vite-tsconfig-paths": "^5.1.4"
|
|
34
33
|
}
|
|
@@ -1,25 +1,111 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { useHead, useI18n } from '@frontend/composables'
|
|
3
|
-
import {
|
|
2
|
+
import { useAuth, useHead, useI18n } from '@frontend/composables'
|
|
3
|
+
import { ref } from 'vue'
|
|
4
4
|
|
|
5
5
|
const { t } = useI18n()
|
|
6
|
-
|
|
7
6
|
useHead({
|
|
8
7
|
title: () => t('title'),
|
|
9
8
|
})
|
|
10
9
|
|
|
11
|
-
const {
|
|
10
|
+
const { user, signUp, signIn, signOut } = useAuth()
|
|
11
|
+
|
|
12
|
+
const email = ref('')
|
|
13
|
+
const password = ref('')
|
|
12
14
|
</script>
|
|
13
15
|
|
|
14
16
|
<template>
|
|
15
17
|
<div class="grid h-full place-items-center">
|
|
16
18
|
<div class="flex flex-col gap-4 items-center">
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
<!-- Login Form (when user is not logged in) -->
|
|
20
|
+
<div v-if="!user" class="mx-auto max-w-md w-full">
|
|
21
|
+
<div class="p-8 rounded-lg bg-white shadow-lg dark:bg-gray-800">
|
|
22
|
+
<h2 class="text-2xl text-gray-900 font-bold mb-6 text-center dark:text-white">
|
|
23
|
+
Welcome Back
|
|
24
|
+
</h2>
|
|
25
|
+
|
|
26
|
+
<form class="space-y-4" @submit.prevent>
|
|
27
|
+
<div>
|
|
28
|
+
<label for="email" class="text-sm text-gray-700 font-medium mb-2 block dark:text-gray-300">
|
|
29
|
+
Email
|
|
30
|
+
</label>
|
|
31
|
+
<input
|
|
32
|
+
id="email"
|
|
33
|
+
v-model="email"
|
|
34
|
+
type="email"
|
|
35
|
+
class="px-3 py-2 border border-gray-300 rounded-md w-full shadow-sm dark:text-white focus:outline-none dark:border-gray-600 focus:border-blue-500 dark:bg-gray-700 focus:ring-2 focus:ring-blue-500"
|
|
36
|
+
placeholder="Enter your email"
|
|
37
|
+
required
|
|
38
|
+
>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div>
|
|
42
|
+
<label for="password" class="text-sm text-gray-700 font-medium mb-2 block dark:text-gray-300">
|
|
43
|
+
Password
|
|
44
|
+
</label>
|
|
45
|
+
<input
|
|
46
|
+
id="password"
|
|
47
|
+
v-model="password"
|
|
48
|
+
type="password"
|
|
49
|
+
class="px-3 py-2 border border-gray-300 rounded-md w-full shadow-sm dark:text-white focus:outline-none dark:border-gray-600 focus:border-blue-500 dark:bg-gray-700 focus:ring-2 focus:ring-blue-500"
|
|
50
|
+
placeholder="Enter your password"
|
|
51
|
+
required
|
|
52
|
+
>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div v-if="signIn.error" class="text-sm text-red-600 dark:text-red-400">
|
|
56
|
+
{{ signIn.error }}
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="flex space-x-3">
|
|
60
|
+
<button
|
|
61
|
+
:disabled="signIn.isPending.value"
|
|
62
|
+
class="text-white font-medium px-4 py-2 rounded-md bg-blue-600 flex-1 transition-colors duration-200 disabled:bg-blue-400 hover:bg-blue-700"
|
|
63
|
+
@click="signIn.mutate({ email, password })"
|
|
64
|
+
>
|
|
65
|
+
<span v-if="signIn.isPending.value">Signing In...</span>
|
|
66
|
+
<span v-else>Sign In</span>
|
|
67
|
+
</button>
|
|
68
|
+
|
|
69
|
+
<button
|
|
70
|
+
:disabled="signUp.isPending.value"
|
|
71
|
+
class="text-white font-medium px-4 py-2 rounded-md bg-green-600 flex-1 transition-colors duration-200 disabled:bg-green-400 hover:bg-green-700"
|
|
72
|
+
@click="signUp.mutate({ email, password, name: 'User' })"
|
|
73
|
+
>
|
|
74
|
+
<span v-if="signUp.isPending.value">Signing Up...</span>
|
|
75
|
+
<span v-else>Sign Up</span>
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
</form>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<!-- User Dashboard (when user is logged in) -->
|
|
83
|
+
<div v-else class="mx-auto max-w-md w-full">
|
|
84
|
+
<div class="p-8 text-center rounded-lg bg-white shadow-lg dark:bg-gray-800">
|
|
85
|
+
<div class="mb-6">
|
|
86
|
+
<div class="mx-auto mb-4 rounded-full flex h-20 w-20 items-center justify-center from-blue-500 to-purple-600 bg-gradient-to-r">
|
|
87
|
+
<span class="text-2xl text-white font-bold">
|
|
88
|
+
{{ user.name?.charAt(0).toUpperCase() || 'U' }}
|
|
89
|
+
</span>
|
|
90
|
+
</div>
|
|
91
|
+
<h2 class="text-2xl text-gray-900 font-bold dark:text-white">
|
|
92
|
+
Welcome, {{ user.name || 'User' }}!
|
|
93
|
+
</h2>
|
|
94
|
+
<p class="text-gray-600 mt-2 dark:text-gray-400">
|
|
95
|
+
{{ user.email }}
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<button
|
|
100
|
+
:disabled="signOut.isPending.value"
|
|
101
|
+
class="text-white font-medium px-4 py-2 rounded-md bg-red-600 w-full transition-colors duration-200 disabled:bg-red-400 hover:bg-red-700"
|
|
102
|
+
@click="signOut.mutate({})"
|
|
103
|
+
>
|
|
104
|
+
<span v-if="signOut.isPending.value">Signing Out...</span>
|
|
105
|
+
<span v-else>Sign Out</span>
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
23
109
|
</div>
|
|
24
110
|
</div>
|
|
25
111
|
</template>
|
|
@@ -0,0 +1,31 @@
|
|
|
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,10 +1,15 @@
|
|
|
1
|
-
import type { Router, RouterClient } from '@backend/router'
|
|
1
|
+
import type { Router, RouterClient } from '@backend/core/router'
|
|
2
2
|
import { createORPCClient } from '@orpc/client'
|
|
3
3
|
import { RPCLink } from '@orpc/client/fetch'
|
|
4
4
|
import { createTanstackQueryUtils } from '@orpc/tanstack-query'
|
|
5
5
|
|
|
6
6
|
const link = new RPCLink({
|
|
7
7
|
url: import.meta.env.VITE_API_URL,
|
|
8
|
+
fetch: (request, init) =>
|
|
9
|
+
globalThis.fetch(request, {
|
|
10
|
+
...init,
|
|
11
|
+
credentials: 'include',
|
|
12
|
+
}),
|
|
8
13
|
})
|
|
9
14
|
|
|
10
15
|
const client: RouterClient<Router> = createORPCClient(link)
|
package/template/knip.config.ts
CHANGED
package/template/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "app",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"private": true,
|
|
5
|
-
"packageManager": "bun@1.2.
|
|
5
|
+
"packageManager": "bun@1.2.22",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "lts/*"
|
|
8
8
|
},
|
|
@@ -21,15 +21,15 @@
|
|
|
21
21
|
"lint:inspect": "bunx @eslint/config-inspector"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@kevinmarrec/eslint-config": "^1.
|
|
25
|
-
"@kevinmarrec/stylelint-config": "^1.
|
|
26
|
-
"@kevinmarrec/tsconfig": "^1.
|
|
24
|
+
"@kevinmarrec/eslint-config": "^1.4.0",
|
|
25
|
+
"@kevinmarrec/stylelint-config": "^1.2.0",
|
|
26
|
+
"@kevinmarrec/tsconfig": "^1.1.0",
|
|
27
27
|
"concurrently": "^9.2.1",
|
|
28
|
-
"eslint": "^9.
|
|
29
|
-
"knip": "^5.63.
|
|
30
|
-
"stylelint": "^16.
|
|
31
|
-
"taze": "^19.
|
|
28
|
+
"eslint": "^9.36.0",
|
|
29
|
+
"knip": "^5.63.1",
|
|
30
|
+
"stylelint": "^16.24.0",
|
|
31
|
+
"taze": "^19.7.0",
|
|
32
32
|
"typescript": "~5.9.2",
|
|
33
|
-
"vue-tsc": "^3.0.
|
|
33
|
+
"vue-tsc": "^3.0.7"
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import * as v from 'valibot'
|
|
2
|
-
|
|
3
|
-
const schema = v.object({
|
|
4
|
-
level: v.optional(v.union([
|
|
5
|
-
v.literal('trace'),
|
|
6
|
-
v.literal('debug'),
|
|
7
|
-
v.literal('info'),
|
|
8
|
-
v.literal('warn'),
|
|
9
|
-
v.literal('error'),
|
|
10
|
-
v.literal('fatal'),
|
|
11
|
-
v.literal('silent'),
|
|
12
|
-
]), 'info'),
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
const config = v.parse(schema, {
|
|
16
|
-
level: import.meta.env.LOG_LEVEL,
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
export const level = config.level
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import * as v from 'valibot'
|
|
2
|
-
|
|
3
|
-
const schema = v.object({
|
|
4
|
-
cors: v.object({
|
|
5
|
-
origin: v.optional(
|
|
6
|
-
v.pipe(
|
|
7
|
-
v.string(),
|
|
8
|
-
v.minLength(1),
|
|
9
|
-
v.transform(value => value.split(',')),
|
|
10
|
-
),
|
|
11
|
-
'*',
|
|
12
|
-
),
|
|
13
|
-
}),
|
|
14
|
-
hostname: v.optional(
|
|
15
|
-
v.pipe(
|
|
16
|
-
v.string(),
|
|
17
|
-
v.minLength(1),
|
|
18
|
-
),
|
|
19
|
-
'localhost',
|
|
20
|
-
),
|
|
21
|
-
port: v.optional(v.pipe(
|
|
22
|
-
v.string(),
|
|
23
|
-
v.transform(value => +value),
|
|
24
|
-
v.number(),
|
|
25
|
-
v.minValue(3000),
|
|
26
|
-
v.maxValue(65535),
|
|
27
|
-
), '4000'),
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
const config = v.parse(schema, {
|
|
31
|
-
cors: {
|
|
32
|
-
origin: import.meta.env.ALLOWED_ORIGINS,
|
|
33
|
-
},
|
|
34
|
-
hostname: import.meta.env.HOST,
|
|
35
|
-
port: import.meta.env.PORT,
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
export const cors = config.cors
|
|
39
|
-
export const hostname = config.hostname
|
|
40
|
-
export const port = config.port
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { users } from '@backend/database/schema'
|
|
2
|
-
import { pub } from '@backend/lib/orpc'
|
|
3
|
-
import * as v from 'valibot'
|
|
4
|
-
|
|
5
|
-
export const welcome = pub
|
|
6
|
-
.input(v.string())
|
|
7
|
-
.output(v.string())
|
|
8
|
-
.handler(async ({ input, context: { db } }) => {
|
|
9
|
-
const count = await db.$count(users)
|
|
10
|
-
return `Hello ${input} (${count})`
|
|
11
|
-
})
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './welcome'
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { orpc } from '@frontend/lib/orpc'
|
|
2
|
-
import { get } from '@frontend/utils'
|
|
3
|
-
import { useQuery } from '@tanstack/vue-query'
|
|
4
|
-
import { computed, type MaybeRef } from 'vue'
|
|
5
|
-
|
|
6
|
-
export function useWelcome(text: MaybeRef<string>) {
|
|
7
|
-
return useQuery(computed(() => orpc.welcome.queryOptions({
|
|
8
|
-
input: get(text),
|
|
9
|
-
})))
|
|
10
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { get } from '@vueuse/core'
|
|
File without changes
|