create-better-t-stack 0.1.0 → 1.0.1
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/README.md +59 -49
- package/dist/index.js +128 -305
- package/package.json +19 -11
- package/template/base/_gitignore +2 -0
- package/template/base/package.json +18 -0
- package/template/base/packages/client/_gitignore +23 -0
- package/template/base/packages/client/components.json +21 -0
- package/template/base/packages/client/index.html +12 -0
- package/template/base/packages/client/package.json +49 -0
- package/template/base/packages/client/src/components/header.tsx +31 -0
- package/template/base/packages/client/src/components/loader.tsx +9 -0
- package/template/base/packages/client/src/components/mode-toggle.tsx +37 -0
- package/template/base/packages/client/src/components/theme-provider.tsx +73 -0
- package/template/base/packages/client/src/components/ui/button.tsx +57 -0
- package/template/base/packages/client/src/components/ui/card.tsx +92 -0
- package/template/base/packages/client/src/components/ui/checkbox.tsx +30 -0
- package/template/base/packages/client/src/components/ui/dropdown-menu.tsx +199 -0
- package/template/base/packages/client/src/components/ui/input.tsx +22 -0
- package/template/base/packages/client/src/components/ui/label.tsx +24 -0
- package/template/base/packages/client/src/components/ui/skeleton.tsx +15 -0
- package/template/base/packages/client/src/components/ui/sonner.tsx +29 -0
- package/template/base/packages/client/src/index.css +119 -0
- package/template/base/packages/client/src/lib/utils.ts +6 -0
- package/template/base/packages/client/src/main.tsx +72 -0
- package/template/base/packages/client/src/routes/__root.tsx +58 -0
- package/template/base/packages/client/src/routes/index.tsx +97 -0
- package/template/base/packages/client/src/utils/trpc.ts +4 -0
- package/template/base/packages/client/tsconfig.json +18 -0
- package/template/base/packages/client/vite.config.ts +14 -0
- package/template/base/packages/server/_gitignore +36 -0
- package/template/base/packages/server/package.json +27 -0
- package/template/base/packages/server/src/index.ts +41 -0
- package/template/base/packages/server/src/lib/context.ts +13 -0
- package/template/base/packages/server/src/lib/trpc.ts +8 -0
- package/template/base/packages/server/src/routers/index.ts +11 -0
- package/template/base/packages/server/tsconfig.json +18 -0
- package/template/base/turbo.json +27 -0
- package/template/examples/todo/packages/client/src/routes/todos.tsx +128 -0
- package/template/examples/todo/packages/server/src/routers/with-drizzle-todo.ts +44 -0
- package/template/examples/todo/packages/server/src/routers/with-prisma-todo.ts +55 -0
- package/template/with-auth/packages/client/src/components/auth-forms.tsx +13 -0
- package/template/with-auth/packages/client/src/components/header.tsx +34 -0
- package/template/with-auth/packages/client/src/components/sign-in-form.tsx +139 -0
- package/template/with-auth/packages/client/src/components/sign-up-form.tsx +164 -0
- package/template/with-auth/packages/client/src/components/user-menu.tsx +62 -0
- package/template/with-auth/packages/client/src/lib/auth-client.ts +5 -0
- package/template/with-auth/packages/client/src/main.tsx +78 -0
- package/template/with-auth/packages/client/src/routes/dashboard.tsx +36 -0
- package/template/with-auth/packages/client/src/routes/index.tsx +97 -0
- package/template/with-auth/packages/client/src/routes/login.tsx +11 -0
- package/template/with-auth/packages/server/src/index.ts +46 -0
- package/template/with-auth/packages/server/src/lib/trpc.ts +24 -0
- package/template/with-auth/packages/server/src/routers/index.ts +19 -0
- package/template/with-biome/biome.json +42 -0
- package/template/with-drizzle-postgres/packages/server/drizzle.config.ts +10 -0
- package/template/with-drizzle-postgres/packages/server/src/db/index.ts +5 -0
- package/template/with-drizzle-postgres/packages/server/src/db/schema/auth.ts +47 -0
- package/template/with-drizzle-postgres/packages/server/src/db/schema/todo.ts +7 -0
- package/template/with-drizzle-postgres/packages/server/src/routers/todo.ts +44 -0
- package/template/with-drizzle-postgres/packages/server/src/with-auth-lib/auth.ts +15 -0
- package/template/with-drizzle-postgres/packages/server/src/with-auth-lib/context.ts +18 -0
- package/template/with-drizzle-postgres/packages/server/src/with-auth-lib/trpc.ts +24 -0
- package/template/with-drizzle-sqlite/packages/server/drizzle.config.ts +11 -0
- package/template/with-drizzle-sqlite/packages/server/src/db/index.ts +9 -0
- package/template/with-drizzle-sqlite/packages/server/src/db/schema/auth.ts +61 -0
- package/template/with-drizzle-sqlite/packages/server/src/db/schema/todo.ts +7 -0
- package/template/with-drizzle-sqlite/packages/server/src/with-auth-lib/auth.ts +15 -0
- package/template/with-drizzle-sqlite/packages/server/src/with-auth-lib/context.ts +18 -0
- package/template/with-drizzle-sqlite/packages/server/src/with-auth-lib/trpc.ts +24 -0
- package/template/with-husky/.husky/pre-commit +1 -0
- package/template/with-prisma-postgres/packages/server/prisma/index.ts +5 -0
- package/template/with-prisma-postgres/packages/server/prisma/schema/auth.prisma +59 -0
- package/template/with-prisma-postgres/packages/server/prisma/schema/schema.prisma +9 -0
- package/template/with-prisma-postgres/packages/server/prisma/schema/todo.prisma +7 -0
- package/template/with-prisma-postgres/packages/server/src/with-auth-lib/auth.ts +17 -0
- package/template/with-prisma-postgres/packages/server/src/with-auth-lib/context.ts +18 -0
- package/template/with-prisma-postgres/packages/server/src/with-auth-lib/trpc.ts +24 -0
- package/template/with-prisma-sqlite/packages/server/prisma/index.ts +5 -0
- package/template/with-prisma-sqlite/packages/server/prisma/schema/auth.prisma +59 -0
- package/template/with-prisma-sqlite/packages/server/prisma/schema/schema.prisma +8 -0
- package/template/with-prisma-sqlite/packages/server/prisma/schema/todo.prisma +7 -0
- package/template/with-prisma-sqlite/packages/server/src/with-auth-lib/auth.ts +17 -0
- package/template/with-prisma-sqlite/packages/server/src/with-auth-lib/context.ts +18 -0
- package/template/with-prisma-sqlite/packages/server/src/with-auth-lib/trpc.ts +24 -0
- package/template/with-pwa/packages/client/public/logo.png +0 -0
- package/template/with-pwa/packages/client/pwa-assets.config.ts +12 -0
- package/template/with-pwa/packages/client/vite.config.ts +35 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
QueryCache,
|
|
3
|
+
QueryClient,
|
|
4
|
+
QueryClientProvider,
|
|
5
|
+
} from "@tanstack/react-query";
|
|
6
|
+
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
|
7
|
+
import { httpBatchLink } from "@trpc/client";
|
|
8
|
+
import { createTRPCQueryUtils } from "@trpc/react-query";
|
|
9
|
+
import ReactDOM from "react-dom/client";
|
|
10
|
+
import { toast } from "sonner";
|
|
11
|
+
import Loader from "./components/loader";
|
|
12
|
+
import { routeTree } from "./routeTree.gen";
|
|
13
|
+
import { trpc } from "./utils/trpc";
|
|
14
|
+
|
|
15
|
+
const queryClient = new QueryClient({
|
|
16
|
+
queryCache: new QueryCache({
|
|
17
|
+
onError: (error) => {
|
|
18
|
+
toast.error(error.message, {
|
|
19
|
+
action: {
|
|
20
|
+
label: "retry",
|
|
21
|
+
onClick: () => {
|
|
22
|
+
queryClient.invalidateQueries();
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const trpcClient = trpc.createClient({
|
|
31
|
+
links: [
|
|
32
|
+
httpBatchLink({
|
|
33
|
+
url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
|
|
34
|
+
fetch(url, options) {
|
|
35
|
+
return fetch(url, {
|
|
36
|
+
...options,
|
|
37
|
+
credentials: "include",
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const trpcQueryUtils = createTRPCQueryUtils({
|
|
45
|
+
queryClient,
|
|
46
|
+
client: trpcClient,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const router = createRouter({
|
|
50
|
+
routeTree,
|
|
51
|
+
defaultPreload: "intent",
|
|
52
|
+
context: { trpcQueryUtils },
|
|
53
|
+
defaultPendingComponent: () => <Loader />,
|
|
54
|
+
Wrap: function WrapComponent({ children }) {
|
|
55
|
+
return (
|
|
56
|
+
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
|
57
|
+
<QueryClientProvider client={queryClient}>
|
|
58
|
+
{children}
|
|
59
|
+
</QueryClientProvider>
|
|
60
|
+
</trpc.Provider>
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Register things for typesafety
|
|
66
|
+
declare module "@tanstack/react-router" {
|
|
67
|
+
interface Register {
|
|
68
|
+
router: typeof router;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const rootElement = document.getElementById("app");
|
|
73
|
+
if (!rootElement) throw new Error("Root element not found");
|
|
74
|
+
|
|
75
|
+
if (!rootElement.innerHTML) {
|
|
76
|
+
const root = ReactDOM.createRoot(rootElement);
|
|
77
|
+
root.render(<RouterProvider router={router} />);
|
|
78
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { authClient } from "@/lib/auth-client";
|
|
2
|
+
import { trpc } from "@/utils/trpc";
|
|
3
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
4
|
+
import { useEffect } from "react";
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute("/dashboard")({
|
|
7
|
+
component: RouteComponent,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function RouteComponent() {
|
|
11
|
+
const { data: session, isPending } = authClient.useSession();
|
|
12
|
+
|
|
13
|
+
const navigate = Route.useNavigate();
|
|
14
|
+
|
|
15
|
+
const privateData = trpc.privateData.useQuery();
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!session && !isPending) {
|
|
19
|
+
navigate({
|
|
20
|
+
to: "/login",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}, [session, isPending]);
|
|
24
|
+
|
|
25
|
+
if (isPending) {
|
|
26
|
+
return <div>Loading...</div>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div>
|
|
31
|
+
<h1>Dashboard</h1>
|
|
32
|
+
<p>Welcome {session?.user.name}</p>
|
|
33
|
+
<p>privateData: {privateData.data?.message}</p>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Button } from "@/components/ui/button";
|
|
2
|
+
import { trpc } from "@/utils/trpc";
|
|
3
|
+
import { Link, createFileRoute } from "@tanstack/react-router";
|
|
4
|
+
import { ArrowRight } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute("/")({
|
|
7
|
+
component: HomeComponent,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const TITLE_TEXT = `
|
|
11
|
+
██████╗ ███████╗████████╗████████╗███████╗██████╗
|
|
12
|
+
██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗
|
|
13
|
+
██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝
|
|
14
|
+
██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗
|
|
15
|
+
██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║
|
|
16
|
+
╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
|
|
17
|
+
|
|
18
|
+
████████╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗
|
|
19
|
+
╚══██╔══╝ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
|
|
20
|
+
██║ ███████╗ ██║ ███████║██║ █████╔╝
|
|
21
|
+
██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗
|
|
22
|
+
██║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗
|
|
23
|
+
╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
function HomeComponent() {
|
|
27
|
+
const healthCheck = trpc.healthCheck.useQuery();
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="container mx-auto max-w-3xl px-4 py-2">
|
|
31
|
+
<pre className="overflow-x-auto font-mono text-sm">{TITLE_TEXT}</pre>
|
|
32
|
+
<div className="grid gap-6">
|
|
33
|
+
<section className="rounded-lg border p-4">
|
|
34
|
+
<h2 className="mb-2 font-medium">API Status</h2>
|
|
35
|
+
<div className="flex items-center gap-2">
|
|
36
|
+
<div
|
|
37
|
+
className={`h-2 w-2 rounded-full ${healthCheck.data ? "bg-green-500" : "bg-red-500"}`}
|
|
38
|
+
/>
|
|
39
|
+
<span className="text-sm text-muted-foreground">
|
|
40
|
+
{healthCheck.isLoading
|
|
41
|
+
? "Checking..."
|
|
42
|
+
: healthCheck.data
|
|
43
|
+
? "Connected"
|
|
44
|
+
: "Disconnected"}
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
</section>
|
|
48
|
+
|
|
49
|
+
<section>
|
|
50
|
+
<h2 className="mb-3 font-medium">Core Features</h2>
|
|
51
|
+
<ul className="grid grid-cols-2 gap-3">
|
|
52
|
+
<FeatureItem
|
|
53
|
+
title="Type-Safe API"
|
|
54
|
+
description="End-to-end type safety with tRPC"
|
|
55
|
+
/>
|
|
56
|
+
<FeatureItem
|
|
57
|
+
title="Modern React"
|
|
58
|
+
description="TanStack Router + TanStack Query"
|
|
59
|
+
/>
|
|
60
|
+
<FeatureItem
|
|
61
|
+
title="Fast Backend"
|
|
62
|
+
description="Lightweight Hono server"
|
|
63
|
+
/>
|
|
64
|
+
<FeatureItem
|
|
65
|
+
title="Beautiful UI"
|
|
66
|
+
description="TailwindCSS + shadcn/ui components"
|
|
67
|
+
/>
|
|
68
|
+
</ul>
|
|
69
|
+
</section>
|
|
70
|
+
|
|
71
|
+
<div className="mt-4 flex flex-col gap-4 sm:flex-row sm:items-center">
|
|
72
|
+
<Button asChild>
|
|
73
|
+
<Link to="/todos" className="flex items-center">
|
|
74
|
+
View Todo Demo
|
|
75
|
+
<ArrowRight className="ml-1 h-4 w-4" />
|
|
76
|
+
</Link>
|
|
77
|
+
</Button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function FeatureItem({
|
|
85
|
+
title,
|
|
86
|
+
description,
|
|
87
|
+
}: {
|
|
88
|
+
title: string;
|
|
89
|
+
description: string;
|
|
90
|
+
}) {
|
|
91
|
+
return (
|
|
92
|
+
<li className="border-l-2 border-primary py-1 pl-3">
|
|
93
|
+
<h3 className="font-medium">{title}</h3>
|
|
94
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
95
|
+
</li>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import AuthForms from "@/components/auth-forms";
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute("/login")({
|
|
5
|
+
component: RouteComponent,
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
function RouteComponent() {
|
|
9
|
+
return (
|
|
10
|
+
<AuthForms />);
|
|
11
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { serve } from "@hono/node-server";
|
|
2
|
+
import { trpcServer } from "@hono/trpc-server";
|
|
3
|
+
import "dotenv/config";
|
|
4
|
+
import { Hono } from "hono";
|
|
5
|
+
import { cors } from "hono/cors";
|
|
6
|
+
import { logger } from "hono/logger";
|
|
7
|
+
import { auth } from "./lib/auth";
|
|
8
|
+
import { createContext } from "./lib/context";
|
|
9
|
+
import { appRouter } from "./routers/index";
|
|
10
|
+
|
|
11
|
+
const app = new Hono();
|
|
12
|
+
|
|
13
|
+
app.use(logger());
|
|
14
|
+
|
|
15
|
+
app.use(
|
|
16
|
+
"/*",
|
|
17
|
+
cors({
|
|
18
|
+
origin: process.env.CORS_ORIGIN || "",
|
|
19
|
+
allowMethods: ["GET", "POST", "OPTIONS"],
|
|
20
|
+
allowHeaders: ["Content-Type", "Authorization"],
|
|
21
|
+
credentials: true,
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
app.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw));
|
|
26
|
+
|
|
27
|
+
app.use(
|
|
28
|
+
"/trpc/*",
|
|
29
|
+
trpcServer({
|
|
30
|
+
router: appRouter,
|
|
31
|
+
createContext: (_opts, hono) => {
|
|
32
|
+
return createContext({ hono });
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
app.get("/", (c) => {
|
|
38
|
+
return c.text("OK");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
serve({
|
|
42
|
+
fetch: app.fetch,
|
|
43
|
+
port: 3000,
|
|
44
|
+
}, (info) => {
|
|
45
|
+
console.log(`Server is running on http://localhost:${info.port}`)
|
|
46
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { initTRPC, TRPCError } from "@trpc/server";
|
|
2
|
+
import type { Context } from "./context";
|
|
3
|
+
|
|
4
|
+
export const t = initTRPC.context<Context>().create();
|
|
5
|
+
|
|
6
|
+
export const router = t.router;
|
|
7
|
+
|
|
8
|
+
export const publicProcedure = t.procedure;
|
|
9
|
+
|
|
10
|
+
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
|
|
11
|
+
if (!ctx.session) {
|
|
12
|
+
throw new TRPCError({
|
|
13
|
+
code: "UNAUTHORIZED",
|
|
14
|
+
message: "Authentication required",
|
|
15
|
+
cause: "No session",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return next({
|
|
19
|
+
ctx: {
|
|
20
|
+
...ctx,
|
|
21
|
+
session: ctx.session,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
import { router, publicProcedure, protectedProcedure } from "../lib/trpc";
|
|
3
|
+
import { todoRouter } from "./todo";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export const appRouter = router({
|
|
7
|
+
healthCheck: publicProcedure.query(() => {
|
|
8
|
+
return "OK";
|
|
9
|
+
}),
|
|
10
|
+
privateData: protectedProcedure.query(({ ctx }) => {
|
|
11
|
+
return {
|
|
12
|
+
message: "This is private",
|
|
13
|
+
user: ctx.session.user,
|
|
14
|
+
};
|
|
15
|
+
}),
|
|
16
|
+
todo: todoRouter,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export type AppRouter = typeof appRouter;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": false,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": false
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": false,
|
|
10
|
+
"ignore": [
|
|
11
|
+
".next",
|
|
12
|
+
"dist",
|
|
13
|
+
".turbo",
|
|
14
|
+
"dev-dist",
|
|
15
|
+
".zed",
|
|
16
|
+
".vscode",
|
|
17
|
+
"routeTree.gen.ts",
|
|
18
|
+
"src-tauri"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"formatter": {
|
|
22
|
+
"enabled": true,
|
|
23
|
+
"indentStyle": "tab"
|
|
24
|
+
},
|
|
25
|
+
"organizeImports": {
|
|
26
|
+
"enabled": true
|
|
27
|
+
},
|
|
28
|
+
"linter": {
|
|
29
|
+
"enabled": true,
|
|
30
|
+
"rules": {
|
|
31
|
+
"recommended": true,
|
|
32
|
+
"correctness": {
|
|
33
|
+
"useExhaustiveDependencies": "info"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"javascript": {
|
|
38
|
+
"formatter": {
|
|
39
|
+
"quoteStyle": "double"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { pgTable, text, timestamp, boolean, serial } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
export const user = pgTable("user", {
|
|
4
|
+
id: text("id").primaryKey(),
|
|
5
|
+
name: text('name').notNull(),
|
|
6
|
+
email: text('email').notNull().unique(),
|
|
7
|
+
emailVerified: boolean('email_verified').notNull(),
|
|
8
|
+
image: text('image'),
|
|
9
|
+
createdAt: timestamp('created_at').notNull(),
|
|
10
|
+
updatedAt: timestamp('updated_at').notNull()
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const session = pgTable("session", {
|
|
14
|
+
id: text("id").primaryKey(),
|
|
15
|
+
expiresAt: timestamp('expires_at').notNull(),
|
|
16
|
+
token: text('token').notNull().unique(),
|
|
17
|
+
createdAt: timestamp('created_at').notNull(),
|
|
18
|
+
updatedAt: timestamp('updated_at').notNull(),
|
|
19
|
+
ipAddress: text('ip_address'),
|
|
20
|
+
userAgent: text('user_agent'),
|
|
21
|
+
userId: text('user_id').notNull().references(()=> user.id, { onDelete: 'cascade' })
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const account = pgTable("account", {
|
|
25
|
+
id: text("id").primaryKey(),
|
|
26
|
+
accountId: text('account_id').notNull(),
|
|
27
|
+
providerId: text('provider_id').notNull(),
|
|
28
|
+
userId: text('user_id').notNull().references(()=> user.id, { onDelete: 'cascade' }),
|
|
29
|
+
accessToken: text('access_token'),
|
|
30
|
+
refreshToken: text('refresh_token'),
|
|
31
|
+
idToken: text('id_token'),
|
|
32
|
+
accessTokenExpiresAt: timestamp('access_token_expires_at'),
|
|
33
|
+
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
|
|
34
|
+
scope: text('scope'),
|
|
35
|
+
password: text('password'),
|
|
36
|
+
createdAt: timestamp('created_at').notNull(),
|
|
37
|
+
updatedAt: timestamp('updated_at').notNull()
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const verification = pgTable("verification", {
|
|
41
|
+
id: text("id").primaryKey(),
|
|
42
|
+
identifier: text('identifier').notNull(),
|
|
43
|
+
value: text('value').notNull(),
|
|
44
|
+
expiresAt: timestamp('expires_at').notNull(),
|
|
45
|
+
createdAt: timestamp('created_at'),
|
|
46
|
+
updatedAt: timestamp('updated_at')
|
|
47
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { router, publicProcedure } from "../lib/trpc";
|
|
3
|
+
import { todo } from "../db/schema";
|
|
4
|
+
import { eq } from "drizzle-orm";
|
|
5
|
+
import { db } from "../db";
|
|
6
|
+
|
|
7
|
+
export const todoRouter = router({
|
|
8
|
+
getAll: publicProcedure.query(async () => {
|
|
9
|
+
return await db.select().from(todo).all();
|
|
10
|
+
}),
|
|
11
|
+
|
|
12
|
+
create: publicProcedure
|
|
13
|
+
.input(z.object({ text: z.string().min(1) }))
|
|
14
|
+
.mutation(async ({ input }) => {
|
|
15
|
+
return await db
|
|
16
|
+
.insert(todo)
|
|
17
|
+
.values({
|
|
18
|
+
text: input.text,
|
|
19
|
+
})
|
|
20
|
+
.returning()
|
|
21
|
+
.get();
|
|
22
|
+
}),
|
|
23
|
+
|
|
24
|
+
toggle: publicProcedure
|
|
25
|
+
.input(z.object({ id: z.number(), completed: z.boolean() }))
|
|
26
|
+
.mutation(async ({ input }) => {
|
|
27
|
+
return await db
|
|
28
|
+
.update(todo)
|
|
29
|
+
.set({ completed: input.completed })
|
|
30
|
+
.where(eq(todo.id, input.id))
|
|
31
|
+
.returning()
|
|
32
|
+
.get();
|
|
33
|
+
}),
|
|
34
|
+
|
|
35
|
+
delete: publicProcedure
|
|
36
|
+
.input(z.object({ id: z.number() }))
|
|
37
|
+
.mutation(async ({ input }) => {
|
|
38
|
+
return await db
|
|
39
|
+
.delete(todo)
|
|
40
|
+
.where(eq(todo.id, input.id))
|
|
41
|
+
.returning()
|
|
42
|
+
.get();
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { betterAuth } from "better-auth";
|
|
2
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
3
|
+
import { db } from "../db";
|
|
4
|
+
import * as schema from "../db/schema/auth";
|
|
5
|
+
|
|
6
|
+
export const auth = betterAuth({
|
|
7
|
+
database: drizzleAdapter(db, {
|
|
8
|
+
provider: "pg",
|
|
9
|
+
schema: schema,
|
|
10
|
+
}),
|
|
11
|
+
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
|
12
|
+
emailAndPassword: {
|
|
13
|
+
enabled: true,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Context as HonoContext } from "hono";
|
|
2
|
+
import { auth } from "./auth";
|
|
3
|
+
|
|
4
|
+
export type CreateContextOptions = {
|
|
5
|
+
hono: HonoContext;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export async function createContext({ hono }: CreateContextOptions) {
|
|
9
|
+
const session = await auth.api.getSession({
|
|
10
|
+
headers: hono.req.raw.headers,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
session,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type Context = Awaited<ReturnType<typeof createContext>>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { initTRPC, TRPCError } from "@trpc/server";
|
|
2
|
+
import type { Context } from "./context";
|
|
3
|
+
|
|
4
|
+
export const t = initTRPC.context<Context>().create();
|
|
5
|
+
|
|
6
|
+
export const router = t.router;
|
|
7
|
+
|
|
8
|
+
export const publicProcedure = t.procedure;
|
|
9
|
+
|
|
10
|
+
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
|
|
11
|
+
if (!ctx.session) {
|
|
12
|
+
throw new TRPCError({
|
|
13
|
+
code: "UNAUTHORIZED",
|
|
14
|
+
message: "Authentication required",
|
|
15
|
+
cause: "No session",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return next({
|
|
19
|
+
ctx: {
|
|
20
|
+
...ctx,
|
|
21
|
+
session: ctx.session,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineConfig } from "drizzle-kit";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
schema: "./src/db/schema",
|
|
5
|
+
out: "./src/db/migrations",
|
|
6
|
+
dialect: "turso",
|
|
7
|
+
dbCredentials: {
|
|
8
|
+
url: process.env.TURSO_CONNECTION_URL || "",
|
|
9
|
+
authToken: process.env.TURSO_AUTH_TOKEN,
|
|
10
|
+
},
|
|
11
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
2
|
+
|
|
3
|
+
export const todo = sqliteTable("todo", {
|
|
4
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
5
|
+
text: text("text").notNull(),
|
|
6
|
+
completed: integer("completed", { mode: "boolean" }).default(false).notNull()
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const user = sqliteTable("user", {
|
|
10
|
+
id: text("id").primaryKey(),
|
|
11
|
+
name: text("name").notNull(),
|
|
12
|
+
email: text("email").notNull().unique(),
|
|
13
|
+
emailVerified: integer("email_verified", { mode: "boolean" }).notNull(),
|
|
14
|
+
image: text("image"),
|
|
15
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
16
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const session = sqliteTable("session", {
|
|
20
|
+
id: text("id").primaryKey(),
|
|
21
|
+
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
|
22
|
+
token: text("token").notNull().unique(),
|
|
23
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
24
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
25
|
+
ipAddress: text("ip_address"),
|
|
26
|
+
userAgent: text("user_agent"),
|
|
27
|
+
userId: text("user_id")
|
|
28
|
+
.notNull()
|
|
29
|
+
.references(() => user.id),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const account = sqliteTable("account", {
|
|
33
|
+
id: text("id").primaryKey(),
|
|
34
|
+
accountId: text("account_id").notNull(),
|
|
35
|
+
providerId: text("provider_id").notNull(),
|
|
36
|
+
userId: text("user_id")
|
|
37
|
+
.notNull()
|
|
38
|
+
.references(() => user.id),
|
|
39
|
+
accessToken: text("access_token"),
|
|
40
|
+
refreshToken: text("refresh_token"),
|
|
41
|
+
idToken: text("id_token"),
|
|
42
|
+
accessTokenExpiresAt: integer("access_token_expires_at", {
|
|
43
|
+
mode: "timestamp",
|
|
44
|
+
}),
|
|
45
|
+
refreshTokenExpiresAt: integer("refresh_token_expires_at", {
|
|
46
|
+
mode: "timestamp",
|
|
47
|
+
}),
|
|
48
|
+
scope: text("scope"),
|
|
49
|
+
password: text("password"),
|
|
50
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
51
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const verification = sqliteTable("verification", {
|
|
55
|
+
id: text("id").primaryKey(),
|
|
56
|
+
identifier: text("identifier").notNull(),
|
|
57
|
+
value: text("value").notNull(),
|
|
58
|
+
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
|
59
|
+
createdAt: integer("created_at", { mode: "timestamp" }),
|
|
60
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }),
|
|
61
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
2
|
+
|
|
3
|
+
export const todo = sqliteTable("todo", {
|
|
4
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
5
|
+
text: text("text").notNull(),
|
|
6
|
+
completed: integer("completed", { mode: "boolean" }).default(false).notNull()
|
|
7
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { betterAuth } from "better-auth";
|
|
2
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
3
|
+
import { db } from "../db";
|
|
4
|
+
import * as schema from "../db/schema/auth";
|
|
5
|
+
|
|
6
|
+
export const auth = betterAuth({
|
|
7
|
+
database: drizzleAdapter(db, {
|
|
8
|
+
provider: "sqlite",
|
|
9
|
+
schema: schema,
|
|
10
|
+
}),
|
|
11
|
+
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
|
12
|
+
emailAndPassword: {
|
|
13
|
+
enabled: true,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Context as HonoContext } from "hono";
|
|
2
|
+
import { auth } from "./auth";
|
|
3
|
+
|
|
4
|
+
export type CreateContextOptions = {
|
|
5
|
+
hono: HonoContext;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export async function createContext({ hono }: CreateContextOptions) {
|
|
9
|
+
const session = await auth.api.getSession({
|
|
10
|
+
headers: hono.req.raw.headers,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
session,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type Context = Awaited<ReturnType<typeof createContext>>;
|