diff --git a/src/app/(dashboard)/project/[id]/_components/CreateService.tsx b/src/app/(dashboard)/project/[id]/_components/CreateService.tsx index f08520f..2ed6515 100644 --- a/src/app/(dashboard)/project/[id]/_components/CreateService.tsx +++ b/src/app/(dashboard)/project/[id]/_components/CreateService.tsx @@ -76,7 +76,10 @@ export function CreateService() {
- mutate.mutate({ name: data.name, projectId: project.id }), + mutate.mutate({ + name: data.name, + projectId: project.internalName, + }), )} > { - console.error(error); - toast.error(error.message, { id: toastLoading }); + trpc: { + context: { + toastId: toastLoading, + }, }, }); @@ -95,7 +96,7 @@ export default function LoginForm() { className="w-full" type="submit" form="loginform" - isLoading={login.isLoading} + isLoading={login.isPending} > Log In diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index accb023..db170d8 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,5 +1,10 @@ +import { redirect } from "next/navigation"; +import { api } from "~/trpc/server"; import LoginForm from "./Login"; -export default function LoginPage() { +export default async function LoginPage() { + const loggedIn = await api.auth.me.query().catch(() => null); + if (loggedIn) return redirect("/"); + return ; } diff --git a/src/server/api/trpc.ts b/src/server/api/trpc.ts index 5d67b14..1c4b89f 100644 --- a/src/server/api/trpc.ts +++ b/src/server/api/trpc.ts @@ -176,6 +176,8 @@ export const authenticatedProcedure = t.procedure.use( if (!ctx.session) { throw new TRPCError({ code: "UNAUTHORIZED", + // NOTE: if you change this, you must also change it in ~/trpc/react.ts + cause: new Error("User is not logged in."), message: "You must be logged in to perform this action.", }); } @@ -187,57 +189,3 @@ export const authenticatedProcedure = t.procedure.use( }); }), ); - -/** - * Project-related procedures - * - * Abstracts away finding a project by ID or internal name, and returns the project object. - * REMINDER: if you override `input`, you must include `projectId` in the new input object. - */ -// export const projectAuthenticatedProcedure = authenticatedProcedure -// .use( -// t.middleware(async ({ ctx, input, next }) => { -// if ( -// !input || -// typeof input != "object" || -// !("projectId" in input) || -// typeof input.projectId != "string" -// ) { -// console.log(input); - -// throw new TRPCError({ -// code: "NOT_FOUND", -// message: "Expected a project ID or internal name.", -// }); -// } - -// const [project] = await ctx.db -// .select({ -// id: projects.id, -// friendlyName: projects.friendlyName, -// internalName: projects.internalName, -// createdAt: projects.createdAt, -// }) -// .from(projects) -// .where( -// or( -// eq(projects.id, input.projectId), -// eq(projects.internalName, input.projectId), -// ), -// ) -// .limit(1); - -// if (!project) -// throw new TRPCError({ -// code: "NOT_FOUND", -// message: "Project not found or insufficient permissions.", -// }); - -// return next({ -// ctx: { -// project: project, -// }, -// }); -// }), -// ) -// .input(z.object({ projectId: z.string() })); diff --git a/src/server/db/index.ts b/src/server/db/index.ts index 81fcfe5..664f6cf 100644 --- a/src/server/db/index.ts +++ b/src/server/db/index.ts @@ -1,8 +1,7 @@ -import { drizzle } from "drizzle-orm/better-sqlite3"; import SQLite3 from "better-sqlite3"; +import { drizzle } from "drizzle-orm/better-sqlite3"; import { join } from "path"; import { env } from "~/env"; -import logger from "../utils/logger"; const globalForDB = globalThis as unknown as { db: ReturnType; @@ -24,6 +23,7 @@ function createDatabaseInstance() { "./exts/sqlite-uuidv7", ), ); + return drizzle(sqlite); } diff --git a/src/trpc/links.ts b/src/trpc/links.ts index d865951..87c4192 100644 --- a/src/trpc/links.ts +++ b/src/trpc/links.ts @@ -1,7 +1,8 @@ import { type TRPCLink } from "@trpc/client"; -import { observable } from "@trpc/server/observable"; -import { type AppRouter } from "~/server/api/root"; import { httpLink } from "@trpc/client/links/httpLink"; +import { observable, tap } from "@trpc/server/observable"; +import { toast } from "sonner"; +import { type AppRouter } from "~/server/api/root"; type LinkOptions = Parameters[0]; @@ -23,3 +24,27 @@ export const authLink: (opts: LinkOptions) => TRPCLink = (opts) => { }); }; }; + +/** + * Logs errors to a toast + */ +export const toastLink: TRPCLink = () => { + return ({ next, op }) => { + return observable((observer) => { + return next(op) + .pipe( + tap({ + // next(result) {}, + error(result) { + if (op.context?.hideErrors) return; + + toast.error(result.message, { + id: op.context?.toastId as string | undefined, + }); + }, + }), + ) + .subscribe(observer); + }); + }; +}; diff --git a/src/trpc/react.tsx b/src/trpc/react.tsx index 24a2bc6..df5a7a3 100644 --- a/src/trpc/react.tsx +++ b/src/trpc/react.tsx @@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { + TRPCClientError, createWSClient, loggerLink, splitLink, @@ -12,8 +13,9 @@ import { import { createTRPCReact } from "@trpc/react-query"; import { useState } from "react"; +import { useRouter } from "next/navigation"; import { type AppRouter } from "~/server/api/root"; -import { authLink } from "./links"; +import { authLink, toastLink } from "./links"; import { getUrl, transformer } from "./shared"; export const api = createTRPCReact(); @@ -40,7 +42,32 @@ export function TRPCReactProvider(props: { children: React.ReactNode; cookies: string; }) { - const [queryClient] = useState(() => new QueryClient()); + const router = useRouter(); + + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + retry: (failureCount, error) => { + if (!(error instanceof TRPCClientError)) return false; + + // check if is an unauthorized error + if ( + error.cause && + error.cause.message === "User is not logged in." + ) { + router.push("/login"); + return false; + } + + // retry 3 times, then give up + return failureCount < 3; + }, + }, + }, + }), + ); const [trpcClient] = useState(() => api.createClient({ @@ -52,6 +79,7 @@ export function TRPCReactProvider(props: { (op.direction === "down" && op.result instanceof Error), }), + toastLink, authLink(sharedLinkOptions(props.cookies)), splitLink({ @@ -59,9 +87,6 @@ export function TRPCReactProvider(props: { return op.type === "subscription"; }, - // true: httpLink(sharedLinkOptions(props.cookies)), - // false: unstable_httpBatchStreamLink(sharedLinkOptions(props.cookies)), - true: wsLink({ client: createWSClient({ url: getUrl().replace("http", "ws"), @@ -74,15 +99,6 @@ export function TRPCReactProvider(props: { false: unstable_httpBatchStreamLink(sharedLinkOptions(props.cookies)), }), - - // authLink(sharedLinkOptions(props.cookies)), - // wsLink({ - // client: createWSClient({ - // url: getUrl().replace("http", "ws"), - // }), - // }), - // wsLink(sharedLinkOptions(props.cookies)), - // unstable_httpBatchStreamLink(sharedLinkOptions(props.cookies)), ], }), );