feat: auth works!
This commit is contained in:
parent
45ecac8a48
commit
66796503a5
61
package.json
61
package.json
|
@ -4,62 +4,61 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"db:push": "prisma db push",
|
"db:push": "drizzle-kit push:sqlite",
|
||||||
"db:studio": "prisma studio",
|
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"postinstall": "prisma generate",
|
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"start": "next start"
|
"start": "next start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/form": "^7.1.7",
|
"@mantine/form": "^7.1.7",
|
||||||
"@prisma/migrate": "^5.4.1",
|
"@prisma/migrate": "^5.5.2",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@t3-oss/env-nextjs": "^0.6.0",
|
"@t3-oss/env-nextjs": "^0.7.1",
|
||||||
"@tanstack/react-query": "^4.32.6",
|
"@tanstack/react-query": "^4.36.1",
|
||||||
"@trpc/client": "^10.37.1",
|
"@trpc/client": "^10.43.1",
|
||||||
"@trpc/next": "^10.37.1",
|
"@trpc/next": "^10.43.1",
|
||||||
"@trpc/react-query": "^10.37.1",
|
"@trpc/react-query": "^10.43.1",
|
||||||
"@trpc/server": "^10.37.1",
|
"@trpc/server": "^10.43.1",
|
||||||
"argon2": "^0.31.1",
|
"argon2": "^0.31.2",
|
||||||
"better-sqlite3": "^9.0.0",
|
"better-sqlite3": "^9.0.0",
|
||||||
"bunyan": "^1.8.15",
|
"bunyan": "^1.8.15",
|
||||||
"bunyan-format": "^0.2.1",
|
"bunyan-format": "^0.2.1",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"drizzle-orm": "^0.28.6",
|
"drizzle-orm": "^0.28.6",
|
||||||
"next": "^13.4.19",
|
"next": "^14.0.1",
|
||||||
"next-auth": "^4.23.2",
|
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"superjson": "^1.13.1",
|
"react-icons": "^4.11.0",
|
||||||
|
"sonner": "^1.2.0",
|
||||||
|
"superjson": "^2.2.0",
|
||||||
"tailwind-merge": "^2.0.0",
|
"tailwind-merge": "^2.0.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"ts-permissions": "^1.0.0",
|
"ts-permissions": "^1.0.0",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.6",
|
"@types/better-sqlite3": "^7.6.6",
|
||||||
"@types/bunyan": "^1.8.9",
|
"@types/bunyan": "^1.8.10",
|
||||||
"@types/bunyan-format": "^0.2.6",
|
"@types/bunyan-format": "^0.2.7",
|
||||||
"@types/eslint": "^8.44.2",
|
"@types/eslint": "^8.44.6",
|
||||||
"@types/node": "^18.16.0",
|
"@types/node": "^20.8.10",
|
||||||
"@types/react": "^18.2.20",
|
"@types/react": "^18.2.35",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.14",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
"@typescript-eslint/eslint-plugin": "^6.9.1",
|
||||||
"@typescript-eslint/parser": "^6.3.0",
|
"@typescript-eslint/parser": "^6.9.1",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.16",
|
||||||
"drizzle-kit": "^0.19.13",
|
"drizzle-kit": "^0.19.13",
|
||||||
"eslint": "^8.47.0",
|
"eslint": "^8.53.0",
|
||||||
"eslint-config-next": "^13.4.19",
|
"eslint-config-next": "^14.0.1",
|
||||||
"postcss": "^8.4.27",
|
"postcss": "^8.4.31",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.1",
|
"prettier-plugin-tailwindcss": "^0.5.6",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.5",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.2.2"
|
||||||
},
|
},
|
||||||
"ct3aMetadata": {
|
"ct3aMetadata": {
|
||||||
"initVersion": "7.20.3-beta.2e8399f"
|
"initVersion": "7.20.3-beta.2e8399f"
|
||||||
|
|
1452
pnpm-lock.yaml
1452
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -12,11 +12,32 @@ import {
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
import { Label } from "~/components/ui/label";
|
import { Label } from "~/components/ui/label";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
|
import { api } from "~/trpc/react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export default function LoginForm() {
|
export default function LoginForm() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [toastLoading, setToastLoading] = useState<
|
||||||
|
string | number | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
|
const login = api.auth.login.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Successfully logged in!", { id: toastLoading });
|
||||||
|
router.push("/dashboard");
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
toast.error(error.message, { id: toastLoading });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
email: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -24,35 +45,60 @@ export default function LoginForm() {
|
||||||
return (
|
return (
|
||||||
<Card className="mx-auto w-80">
|
<Card className="mx-auto w-80">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Hostforge</CardTitle>
|
<CardTitle>🔥 Hostforge</CardTitle>
|
||||||
<CardDescription>Log into your account to start.</CardDescription>
|
<CardDescription>Log into your account to start.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-2">
|
<CardContent>
|
||||||
{/* basic login */}
|
<form
|
||||||
<div className="flex flex-col gap-2">
|
className="flex flex-col gap-4"
|
||||||
<Label htmlFor="email">Username</Label>
|
id="loginform"
|
||||||
<Input id="email" type="email" placeholder="jsmith" />
|
onSubmit={(e) => {
|
||||||
</div>
|
e.preventDefault();
|
||||||
<div className="flex flex-col gap-2">
|
login.mutate(form.values);
|
||||||
<Label htmlFor="password">Password</Label>
|
setToastLoading(toast.loading("Logging you in..."));
|
||||||
<Input id="password" type="password" />
|
}}
|
||||||
</div>
|
>
|
||||||
|
{/* basic login */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="email">Username</Label>
|
||||||
|
<Input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
placeholder="jsmith"
|
||||||
|
{...form.getInputProps("username")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="password">Password</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
{...form.getInputProps("password")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
{/* or */}
|
{/* or */}
|
||||||
<div className="flex items-center gap-2">
|
{/* <div className="flex items-center gap-2">
|
||||||
<hr className="flex-grow border-gray-300" />
|
<hr className="flex-grow border-gray-300" />
|
||||||
<span className="text-gray-500">or</span>
|
<span className="text-gray-500">or</span>
|
||||||
<hr className="flex-grow border-gray-300" />
|
<hr className="flex-grow border-gray-300" />
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* social login */}
|
{/* social login */}
|
||||||
<Button variant="outline">
|
{/* <Button variant="outline"> */}
|
||||||
{/* TODO: icons */}
|
{/* TODO: icons */}
|
||||||
GitHub
|
{/* GitHub
|
||||||
</Button>
|
</Button> */}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
<Button className="w-full">Log In</Button>
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
type="submit"
|
||||||
|
form="loginform"
|
||||||
|
isLoading={login.isLoading}
|
||||||
|
>
|
||||||
|
Log In
|
||||||
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { api } from "~/trpc/react";
|
|
||||||
|
|
||||||
export function CreatePost() {
|
|
||||||
const router = useRouter();
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
|
|
||||||
const createPost = api.post.create.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
router.refresh();
|
|
||||||
setName("");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
createPost.mutate({ name });
|
|
||||||
}}
|
|
||||||
className="flex flex-col gap-2"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Title"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
className="w-full rounded-full px-4 py-2 text-black"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="rounded-full bg-white/10 px-10 py-3 font-semibold transition hover:bg-white/20"
|
|
||||||
disabled={createPost.isLoading}
|
|
||||||
>
|
|
||||||
{createPost.isLoading ? "Submitting..." : "Submit"}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -10,12 +10,12 @@ const handler = (req: NextRequest) =>
|
||||||
endpoint: "/api/trpc",
|
endpoint: "/api/trpc",
|
||||||
req,
|
req,
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
createContext: () => createTRPCContext({ req }),
|
createContext: ({ resHeaders }) => createTRPCContext({ req, resHeaders }),
|
||||||
onError:
|
onError:
|
||||||
env.NODE_ENV === "development"
|
env.NODE_ENV === "development"
|
||||||
? ({ path, error }) => {
|
? ({ path, error }) => {
|
||||||
console.error(
|
console.error(
|
||||||
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`
|
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
11
src/app/dashboard/RSC.tsx
Normal file
11
src/app/dashboard/RSC.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { api } from "~/trpc/server";
|
||||||
|
|
||||||
|
export default async function Test() {
|
||||||
|
const user = await api.auth.me.query();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Logged in as {user.username} ({user.id})
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
11
src/app/dashboard/page.tsx
Normal file
11
src/app/dashboard/page.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { api } from "~/trpc/server";
|
||||||
|
import Test from "./RSC";
|
||||||
|
|
||||||
|
export default async function DashboardHome() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
rip
|
||||||
|
<Test />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
import "~/styles/globals.css";
|
import "~/styles/globals.css";
|
||||||
|
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import { headers } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { TRPCReactProvider } from "~/trpc/react";
|
import { TRPCReactProvider } from "~/trpc/react";
|
||||||
import { ThemeProvider } from "~/components/ThemeProvider";
|
import { ThemeProvider } from "~/components/contexts/ThemeProvider";
|
||||||
|
import { ToastProvider } from "~/components/contexts/ToastProvider";
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
|
@ -24,9 +25,9 @@ export default function RootLayout({
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={`font-sans ${inter.variable} min-h-screen min-w-full`}>
|
<body className={`font-sans ${inter.variable} min-h-screen min-w-full`}>
|
||||||
<TRPCReactProvider headers={headers()}>
|
<TRPCReactProvider cookies={cookies().toString()}>
|
||||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
|
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
|
||||||
{children}
|
<ToastProvider>{children}</ToastProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</TRPCReactProvider>
|
</TRPCReactProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|
106
src/app/setup/SetupForm.tsx
Normal file
106
src/app/setup/SetupForm.tsx
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
CardFooter,
|
||||||
|
} from "~/components/ui/card";
|
||||||
|
import { Input } from "~/components/ui/input";
|
||||||
|
import { Label } from "~/components/ui/label";
|
||||||
|
import { Required } from "~/components/ui/required";
|
||||||
|
import { api } from "~/trpc/react";
|
||||||
|
|
||||||
|
export function SetupForm() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// setup mutation
|
||||||
|
const [toastLoading, setToastLoading] = useState<
|
||||||
|
string | number | undefined
|
||||||
|
>();
|
||||||
|
const setupInstance = api.setup.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
router.push("/dashboard");
|
||||||
|
toast.success("Successfully setup instance!", { id: toastLoading });
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.message, { id: toastLoading });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
validate: {
|
||||||
|
password: (value) => value.length === 0 && "Password is required",
|
||||||
|
username: (value) => value.length === 0 && "Username is required",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="m-auto h-fit max-w-xl">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Setup Hostforge 🚀</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Welcome to Hostforge — a modern self-hosted platform for deploying and
|
||||||
|
managing your own applications.
|
||||||
|
<br /> <br /> To start, please enter your desired username and
|
||||||
|
password for the administrator account. You can setup two factor
|
||||||
|
authentication later.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form
|
||||||
|
id="setupform"
|
||||||
|
className="flex flex-col gap-4"
|
||||||
|
onSubmit={form.onSubmit((data) => {
|
||||||
|
setToastLoading(toast.loading("Setting up instance..."));
|
||||||
|
setupInstance.mutate(data);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="username">
|
||||||
|
Username
|
||||||
|
<Required />
|
||||||
|
</Label>
|
||||||
|
<Input {...form.getInputProps("username")} id="username" />
|
||||||
|
{form.errors.username && (
|
||||||
|
<div className="text-red-500">{form.errors.username}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="password">
|
||||||
|
Password
|
||||||
|
<Required />
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
{...form.getInputProps("password")}
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
{form.errors.password && (
|
||||||
|
<div className="text-red-500">{form.errors.password}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button className="w-full" type="submit" form="setupform">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1 +1,9 @@
|
||||||
export default function SetupInstance() {}
|
import { SetupForm } from "./SetupForm";
|
||||||
|
|
||||||
|
export default function SetupInstance() {
|
||||||
|
return (
|
||||||
|
<div className="min-w-screen flex min-h-screen justify-center p-4 align-middle">
|
||||||
|
<SetupForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
21
src/components/contexts/ToastProvider.tsx
Normal file
21
src/components/contexts/ToastProvider.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
import { type ReactNode } from "react";
|
||||||
|
import { Toaster } from "sonner";
|
||||||
|
|
||||||
|
export function ToastProvider({ children }: { children: ReactNode }) {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Toaster
|
||||||
|
position="bottom-center"
|
||||||
|
richColors
|
||||||
|
theme={theme.resolvedTheme === "dark" ? "dark" : "light"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "~/utils/utils";
|
import { cn } from "~/utils/utils";
|
||||||
|
import { CgSpinner } from "react-icons/cg";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
@ -38,17 +38,36 @@ export interface ButtonProps
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
VariantProps<typeof buttonVariants> {
|
VariantProps<typeof buttonVariants> {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
asChild = false,
|
||||||
|
children,
|
||||||
|
isLoading,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
const Comp = asChild ? Slot : "button";
|
const Comp = asChild ? Slot : "button";
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(
|
||||||
|
buttonVariants({ variant, size, className }),
|
||||||
|
isLoading && "cursor-not-allowed brightness-75 filter",
|
||||||
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
disabled={isLoading || props.disabled}
|
||||||
|
>
|
||||||
|
{isLoading && <CgSpinner className="mr-2 animate-spin" />}
|
||||||
|
{children}
|
||||||
|
</Comp>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,15 +2,18 @@ import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "~/utils/utils";
|
import { cn } from "~/utils/utils";
|
||||||
|
|
||||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||||
|
error?: boolean | string;
|
||||||
|
};
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className, type, ...props }, ref) => {
|
({ className, type, error, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50",
|
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
error && "border-red-500 focus-visible:ring-red-500",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
1
src/components/ui/required.tsx
Normal file
1
src/components/ui/required.tsx
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const Required = () => <span className="px-1 text-red-500">*</span>;
|
|
@ -7,6 +7,17 @@ export async function register() {
|
||||||
const logger = (await import("./server/utils/logger")).default;
|
const logger = (await import("./server/utils/logger")).default;
|
||||||
const { migrate } = await import("drizzle-orm/better-sqlite3/migrator");
|
const { migrate } = await import("drizzle-orm/better-sqlite3/migrator");
|
||||||
const { db } = await import("./server/db");
|
const { db } = await import("./server/db");
|
||||||
|
const { mkdir, stat } = await import("fs/promises");
|
||||||
|
const { dirname } = await import("path");
|
||||||
|
|
||||||
|
// check if database folder exists
|
||||||
|
try {
|
||||||
|
const dir = dirname(env.DATABASE_PATH);
|
||||||
|
await stat(dir);
|
||||||
|
} catch (e) {
|
||||||
|
await mkdir(dirname(env.DATABASE_PATH), { recursive: true });
|
||||||
|
logger.debug(`Created database folder ${dirname(env.DATABASE_PATH)}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (env.NODE_ENV === "production") {
|
if (env.NODE_ENV === "production") {
|
||||||
logger.child({ module: "database" }).info("⚙️ Migrating database");
|
logger.child({ module: "database" }).info("⚙️ Migrating database");
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { postRouter } from "~/server/api/routers/post";
|
|
||||||
import { createTRPCRouter } from "~/server/api/trpc";
|
import { createTRPCRouter } from "~/server/api/trpc";
|
||||||
|
import { setupProcedure } from "./routers/setup";
|
||||||
|
import { authRouter } from "./routers/auth";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the primary router for your server.
|
* This is the primary router for your server.
|
||||||
|
@ -7,7 +8,8 @@ import { createTRPCRouter } from "~/server/api/trpc";
|
||||||
* All routers added in /api/routers should be manually added here.
|
* All routers added in /api/routers should be manually added here.
|
||||||
*/
|
*/
|
||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
post: postRouter,
|
setup: setupProcedure,
|
||||||
|
auth: authRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|
|
@ -64,7 +64,7 @@ export const authRouter = createTRPCRouter({
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await Session.createForUser(user.id, ctx.request);
|
const session = await Session.createForUser(user.id, ctx.request);
|
||||||
ctx.request.cookies.set("session", session.data.token);
|
ctx.headers.set("Set-Cookie", session.getCookieString());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
|
||||||
|
|
||||||
export const postRouter = createTRPCRouter({
|
|
||||||
hello: publicProcedure
|
|
||||||
.input(z.object({ text: z.string() }))
|
|
||||||
.query(({ input }) => {
|
|
||||||
return {
|
|
||||||
greeting: `Hello ${input.text}`,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
|
|
||||||
create: publicProcedure
|
|
||||||
.input(z.object({ name: z.string().min(1) }))
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
// simulate a slow db call
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
return ctx.db.post.create({
|
|
||||||
data: {
|
|
||||||
name: input.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
getLatest: publicProcedure.query(({ ctx }) => {
|
|
||||||
return ctx.db.post.findFirst({
|
|
||||||
orderBy: { createdAt: "desc" },
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
});
|
|
|
@ -7,12 +7,13 @@
|
||||||
* need to use are documented accordingly near the end.
|
* need to use are documented accordingly near the end.
|
||||||
*/
|
*/
|
||||||
import { TRPCError, initTRPC } from "@trpc/server";
|
import { TRPCError, initTRPC } from "@trpc/server";
|
||||||
import { type NextRequest } from "next/server";
|
import { NextResponse, type NextRequest } from "next/server";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
import { Session } from "../auth/Session";
|
import { Session } from "../auth/Session";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. CONTEXT
|
* 1. CONTEXT
|
||||||
|
@ -52,9 +53,12 @@ export const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||||
*
|
*
|
||||||
* @see https://trpc.io/docs/context
|
* @see https://trpc.io/docs/context
|
||||||
*/
|
*/
|
||||||
export const createTRPCContext = async (opts: { req: NextRequest }) => {
|
export const createTRPCContext = async (opts: {
|
||||||
|
req: NextRequest;
|
||||||
|
resHeaders: Headers;
|
||||||
|
}) => {
|
||||||
// resolve session data
|
// resolve session data
|
||||||
const sessionToken = opts.req.cookies.get("session")?.value;
|
const sessionToken = opts.req.cookies.get("sessionToken")?.value;
|
||||||
|
|
||||||
const session: Session | null = sessionToken
|
const session: Session | null = sessionToken
|
||||||
? await Session.fetchFromTokenAndUpdate(sessionToken, {
|
? await Session.fetchFromTokenAndUpdate(sessionToken, {
|
||||||
|
@ -64,7 +68,7 @@ export const createTRPCContext = async (opts: { req: NextRequest }) => {
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return createInnerTRPCContext({
|
return createInnerTRPCContext({
|
||||||
headers: opts.req.headers,
|
headers: opts.resHeaders,
|
||||||
session,
|
session,
|
||||||
request: opts.req,
|
request: opts.req,
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { db } from "../db";
|
||||||
import { users, sessions } from "../db/schema";
|
import { users, sessions } from "../db/schema";
|
||||||
import { randomBytes } from "crypto";
|
import { randomBytes } from "crypto";
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
import { NextRequest } from "next/server";
|
import { NextRequest, userAgent } from "next/server";
|
||||||
|
|
||||||
export type SessionUpdateData = Partial<{
|
export type SessionUpdateData = Partial<{
|
||||||
ua: string;
|
ua: string;
|
||||||
|
@ -11,6 +11,12 @@ export type SessionUpdateData = Partial<{
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export class Session {
|
export class Session {
|
||||||
|
/**
|
||||||
|
* The length of time a session should last.
|
||||||
|
* currently 30 days.
|
||||||
|
*/
|
||||||
|
static readonly EXPIRE_TIME = 1000 * 60 * 60 * 24 * 30;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch a session from a session token.
|
* Fetch a session from a session token.
|
||||||
* @param token The session cookie
|
* @param token The session cookie
|
||||||
|
@ -74,7 +80,7 @@ export class Session {
|
||||||
|
|
||||||
// parse context
|
// parse context
|
||||||
const parsedContext =
|
const parsedContext =
|
||||||
context instanceof NextRequest
|
context instanceof Request
|
||||||
? Session.getContextFromRequest(context)
|
? Session.getContextFromRequest(context)
|
||||||
: context;
|
: context;
|
||||||
|
|
||||||
|
@ -98,7 +104,7 @@ export class Session {
|
||||||
*/
|
*/
|
||||||
static getContextFromRequest(request: NextRequest) {
|
static getContextFromRequest(request: NextRequest) {
|
||||||
return {
|
return {
|
||||||
ua: request.headers.get("user-agent") ?? undefined,
|
ua: userAgent(request).ua ?? undefined,
|
||||||
ip: request.ip,
|
ip: request.ip,
|
||||||
} satisfies SessionUpdateData;
|
} satisfies SessionUpdateData;
|
||||||
}
|
}
|
||||||
|
@ -134,4 +140,14 @@ export class Session {
|
||||||
async delete() {
|
async delete() {
|
||||||
await db.delete(sessions).where(eq(sessions.token, this.data.token));
|
await db.delete(sessions).where(eq(sessions.token, this.data.token));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a cookie string for this session.
|
||||||
|
*/
|
||||||
|
getCookieString() {
|
||||||
|
const expire = new Date(this.data.createdAt + Session.EXPIRE_TIME);
|
||||||
|
return `sessionToken=${
|
||||||
|
this.data.token
|
||||||
|
}; Expires=${expire.toUTCString()}; Path=/; HttpOnly; SameSite=Strict`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,12 @@ sqlite.pragma("journal_mode = WAL");
|
||||||
// load uuidv7 extension
|
// load uuidv7 extension
|
||||||
// built from https://github.com/craigpastro/sqlite-uuidv7
|
// built from https://github.com/craigpastro/sqlite-uuidv7
|
||||||
sqlite.loadExtension(
|
sqlite.loadExtension(
|
||||||
env.SQLITE_UUIDV7_EXT_PATH ?? join(__dirname, "../../exts/sqlite-uuidv7"),
|
env.SQLITE_UUIDV7_EXT_PATH ??
|
||||||
|
join(
|
||||||
|
// cannot use __dirname since this file will change locations when compiled
|
||||||
|
process.cwd(),
|
||||||
|
"./exts/sqlite-uuidv7",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const db = drizzle(sqlite);
|
export const db = drizzle(sqlite);
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
|
|
||||||
// util
|
// util
|
||||||
const uuidv7 = sql`(uuid_generate_v7())`;
|
const uuidv7 = sql`(uuid_generate_v7())`;
|
||||||
const now = sql`CURRENT_TIMESTAMP`;
|
const now = sql<number>`CURRENT_TIMESTAMP`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User table.
|
* User table.
|
||||||
|
@ -45,7 +45,7 @@ export const sessions = sqliteTable("session", {
|
||||||
lastIP: text("last_ip"),
|
lastIP: text("last_ip"),
|
||||||
// NOT IN MILLISECONDS!
|
// NOT IN MILLISECONDS!
|
||||||
lastAccessed: integer("last_accessed", { mode: "timestamp" }),
|
lastAccessed: integer("last_accessed", { mode: "timestamp" }),
|
||||||
createdAt: integer("created_at").default(now),
|
createdAt: integer("created_at").default(now).notNull(),
|
||||||
userId: text("id").notNull(),
|
userId: text("id").notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
|
@ -9,63 +9,65 @@
|
||||||
|
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 240 10% 3.9%;
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 240 10% 3.9%;
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
--primary: 240 5.9% 10%;
|
--primary: 240 5.9% 10%;
|
||||||
--primary-foreground: 0 0% 98%;
|
--primary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
--secondary: 240 4.8% 95.9%;
|
--secondary: 240 4.8% 95.9%;
|
||||||
--secondary-foreground: 240 5.9% 10%;
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
--muted: 240 4.8% 95.9%;
|
--muted: 240 4.8% 95.9%;
|
||||||
--muted-foreground: 240 3.8% 46.1%;
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
|
||||||
--accent: 240 4.8% 95.9%;
|
--accent: 240 4.8% 95.9%;
|
||||||
--accent-foreground: 240 5.9% 10%;
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
--border: 240 5.9% 90%;
|
--border: 240 5.9% 90%;
|
||||||
--input: 240 5.9% 90%;
|
--input: 240 5.9% 90%;
|
||||||
--ring: 240 10% 3.9%;
|
--ring: 240 10% 3.9%;
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 240 10% 3.9%;
|
--background: 22.5 66.67% 2.35%;
|
||||||
--foreground: 0 0% 98%;
|
--foreground: 20 100% 94.12%;
|
||||||
|
|
||||||
--card: 240 10% 3.9%;
|
--primary: 19.5 100% 60.78%;
|
||||||
--card-foreground: 0 0% 98%;
|
--primary-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--popover: 240 10% 3.9%;
|
--card: 22.5 66.67% 2.35%;
|
||||||
--popover-foreground: 0 0% 98%;
|
--card-foreground: 20 100% 94.12%;
|
||||||
|
|
||||||
--primary: 0 0% 98%;
|
--popover: 22.5 66.67% 2.35%;
|
||||||
--primary-foreground: 240 5.9% 10%;
|
--popover-foreground: 20 100% 94.12%;
|
||||||
|
|
||||||
--secondary: 240 3.7% 15.9%;
|
--secondary: 19.71 64.81% 21.18%;
|
||||||
--secondary-foreground: 0 0% 98%;
|
--secondary-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--muted: 240 3.7% 15.9%;
|
--muted: 25.71 11.48% 11.96%;
|
||||||
--muted-foreground: 240 5% 64.9%;
|
--muted-foreground: 15 1.61% 51.37%;
|
||||||
|
|
||||||
--accent: 240 3.7% 15.9%;
|
--accent: 19.71 64.81% 21.18%;
|
||||||
--accent-foreground: 0 0% 98%;
|
--accent-foreground: 20 100% 94.12%;
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--border: 240 3.7% 15.9%;
|
--border: 0 0% 14.9%;
|
||||||
--input: 240 3.7% 15.9%;
|
--input: 0 0% 14.9%;
|
||||||
--ring: 240 4.9% 83.9%;
|
--ring: 19.5 100% 60.78%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
|
@ -73,4 +75,4 @@
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
25
src/trpc/auth.ts
Normal file
25
src/trpc/auth.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { TRPCLink } from "@trpc/client";
|
||||||
|
import { observable } from "@trpc/server/observable";
|
||||||
|
import { AppRouter } from "~/server/api/root";
|
||||||
|
import { httpLink } from "@trpc/client/links/httpLink";
|
||||||
|
|
||||||
|
type LinkOptions = Parameters<typeof httpLink>[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple re-implementation of httpLink, except it only acts as a terminating link for requests to *auth* routes.
|
||||||
|
* Cannot set cookies in a streaming response, but we don't want to require every request to be a non-streaming batch request, so this is the best option.
|
||||||
|
*/
|
||||||
|
export const authLink: (opts: LinkOptions) => TRPCLink<AppRouter> = (opts) => {
|
||||||
|
const originalLink = httpLink(opts);
|
||||||
|
|
||||||
|
return (runtime) =>
|
||||||
|
({ next, op }) => {
|
||||||
|
return observable((observer) => {
|
||||||
|
if (!op.path.startsWith("auth.")) {
|
||||||
|
return next(op).subscribe(observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
originalLink(runtime)({ next, op }).subscribe(observer);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,18 +1,41 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client";
|
import {
|
||||||
|
HTTPBatchStreamLinkOptions,
|
||||||
|
loggerLink,
|
||||||
|
unstable_httpBatchStreamLink,
|
||||||
|
} from "@trpc/client";
|
||||||
import { createTRPCReact } from "@trpc/react-query";
|
import { createTRPCReact } from "@trpc/react-query";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { type AppRouter } from "~/server/api/root";
|
import { type AppRouter } from "~/server/api/root";
|
||||||
import { getUrl, transformer } from "./shared";
|
import { getUrl, transformer } from "./shared";
|
||||||
|
import { authLink } from "./auth";
|
||||||
|
|
||||||
export const api = createTRPCReact<AppRouter>();
|
export const api = createTRPCReact<AppRouter>();
|
||||||
|
|
||||||
|
const sharedLinkOptions = (cookies: string) =>
|
||||||
|
({
|
||||||
|
url: getUrl(),
|
||||||
|
headers() {
|
||||||
|
return {
|
||||||
|
cookie: cookies,
|
||||||
|
"x-trpc-source": "react",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
fetch(url, options) {
|
||||||
|
return fetch(url, {
|
||||||
|
...options,
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}) satisfies HTTPBatchStreamLinkOptions;
|
||||||
|
|
||||||
export function TRPCReactProvider(props: {
|
export function TRPCReactProvider(props: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
headers: Headers;
|
cookies: string;
|
||||||
}) {
|
}) {
|
||||||
const [queryClient] = useState(() => new QueryClient());
|
const [queryClient] = useState(() => new QueryClient());
|
||||||
|
|
||||||
|
@ -25,16 +48,10 @@ export function TRPCReactProvider(props: {
|
||||||
process.env.NODE_ENV === "development" ||
|
process.env.NODE_ENV === "development" ||
|
||||||
(op.direction === "down" && op.result instanceof Error),
|
(op.direction === "down" && op.result instanceof Error),
|
||||||
}),
|
}),
|
||||||
unstable_httpBatchStreamLink({
|
authLink(sharedLinkOptions(props.cookies)),
|
||||||
url: getUrl(),
|
unstable_httpBatchStreamLink(sharedLinkOptions(props.cookies)),
|
||||||
headers() {
|
|
||||||
const heads = new Map(props.headers);
|
|
||||||
heads.set("x-trpc-source", "react");
|
|
||||||
return Object.fromEntries(heads);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -3,10 +3,10 @@ import {
|
||||||
loggerLink,
|
loggerLink,
|
||||||
unstable_httpBatchStreamLink,
|
unstable_httpBatchStreamLink,
|
||||||
} from "@trpc/client";
|
} from "@trpc/client";
|
||||||
import { headers } from "next/headers";
|
|
||||||
|
|
||||||
import { type AppRouter } from "~/server/api/root";
|
import { type AppRouter } from "~/server/api/root";
|
||||||
import { getUrl, transformer } from "./shared";
|
import { getUrl, transformer } from "./shared";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
export const api = createTRPCProxyClient<AppRouter>({
|
export const api = createTRPCProxyClient<AppRouter>({
|
||||||
transformer,
|
transformer,
|
||||||
|
@ -19,9 +19,10 @@ export const api = createTRPCProxyClient<AppRouter>({
|
||||||
unstable_httpBatchStreamLink({
|
unstable_httpBatchStreamLink({
|
||||||
url: getUrl(),
|
url: getUrl(),
|
||||||
headers() {
|
headers() {
|
||||||
const heads = new Map(headers());
|
return {
|
||||||
heads.set("x-trpc-source", "rsc");
|
cookie: cookies().toString(),
|
||||||
return Object.fromEntries(heads);
|
"x-trpc-source": "rsc",
|
||||||
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|
Loading…
Reference in a new issue