aboutsummaryrefslogtreecommitdiffstats
path: root/packages/web/server
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-02-11 14:54:52 +0000
committerMohamedBassem <me@mbassem.com>2024-02-11 14:55:09 +0000
commit2c2d05fd0a2c3c26d765f8a6beb88d907a097c1d (patch)
treec4738ba0bc011d60361f89aca9be3293474ab9e9 /packages/web/server
parentc2f1d6d8b8a0f09820153fc736806b147d46abfe (diff)
downloadkarakeep-2c2d05fd0a2c3c26d765f8a6beb88d907a097c1d.tar.zst
refactor: Migrating to trpc instead of next's route handers
Diffstat (limited to 'packages/web/server')
-rw-r--r--packages/web/server/api/client.ts14
-rw-r--r--packages/web/server/api/routers/_app.ts7
-rw-r--r--packages/web/server/api/routers/bookmarks.ts139
-rw-r--r--packages/web/server/api/trpc.ts31
-rw-r--r--packages/web/server/auth.ts39
-rw-r--r--packages/web/server/config.ts22
-rw-r--r--packages/web/server/routers/_app.ts17
-rw-r--r--packages/web/server/trpc.ts9
-rw-r--r--packages/web/server/utils.ts16
9 files changed, 252 insertions, 42 deletions
diff --git a/packages/web/server/api/client.ts b/packages/web/server/api/client.ts
new file mode 100644
index 00000000..7008e10d
--- /dev/null
+++ b/packages/web/server/api/client.ts
@@ -0,0 +1,14 @@
+import { appRouter } from "./routers/_app";
+import { getServerAuthSession } from "@/server/auth";
+import { Context, createCallerFactory } from "./trpc";
+
+export const createContext = async (): Promise<Context> => {
+ const session = await getServerAuthSession();
+ return {
+ session,
+ };
+};
+
+const createCaller = createCallerFactory(appRouter);
+
+export const api = createCaller(createContext);
diff --git a/packages/web/server/api/routers/_app.ts b/packages/web/server/api/routers/_app.ts
new file mode 100644
index 00000000..a4f9c629
--- /dev/null
+++ b/packages/web/server/api/routers/_app.ts
@@ -0,0 +1,7 @@
+import { router } from "../trpc";
+import { bookmarksAppRouter } from "./bookmarks";
+export const appRouter = router({
+ bookmarks: bookmarksAppRouter,
+});
+// export type definition of API
+export type AppRouter = typeof appRouter;
diff --git a/packages/web/server/api/routers/bookmarks.ts b/packages/web/server/api/routers/bookmarks.ts
new file mode 100644
index 00000000..0b97563f
--- /dev/null
+++ b/packages/web/server/api/routers/bookmarks.ts
@@ -0,0 +1,139 @@
+import { z } from "zod";
+import { authedProcedure, router } from "../trpc";
+import {
+ ZBookmark,
+ ZBookmarkContent,
+ zBookmarkSchema,
+ zGetBookmarksRequestSchema,
+ zGetBookmarksResponseSchema,
+ zNewBookmarkRequestSchema,
+ zUpdateBookmarksRequestSchema,
+} from "@/lib/types/api/bookmarks";
+import { prisma } from "@remember/db";
+import { LinkCrawlerQueue } from "@remember/shared/queues";
+
+const defaultBookmarkFields = {
+ id: true,
+ favourited: true,
+ archived: true,
+ createdAt: true,
+ link: {
+ select: {
+ url: true,
+ title: true,
+ description: true,
+ imageUrl: true,
+ favicon: true,
+ },
+ },
+ tags: {
+ include: {
+ tag: true,
+ },
+ },
+};
+
+async function dummyPrismaReturnType() {
+ const x = await prisma.bookmark.findFirstOrThrow({
+ select: defaultBookmarkFields,
+ });
+ return x;
+}
+
+function toZodSchema(
+ bookmark: Awaited<ReturnType<typeof dummyPrismaReturnType>>,
+): ZBookmark {
+ const { tags, link, ...rest } = bookmark;
+
+ let content: ZBookmarkContent;
+ if (link) {
+ content = { type: "link", ...link };
+ } else {
+ throw new Error("Unknown content type");
+ }
+
+ return {
+ tags: tags.map((t) => t.tag),
+ content,
+ ...rest,
+ };
+}
+
+export const bookmarksAppRouter = router({
+ bookmarkLink: authedProcedure
+ .input(zNewBookmarkRequestSchema)
+ .output(zBookmarkSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { url } = input;
+ const userId = ctx.user.id;
+
+ const bookmark = await prisma.bookmark.create({
+ data: {
+ link: {
+ create: {
+ url,
+ },
+ },
+ userId,
+ },
+ select: defaultBookmarkFields,
+ });
+
+ // Enqueue crawling request
+ await LinkCrawlerQueue.add("crawl", {
+ bookmarkId: bookmark.id,
+ url: url,
+ });
+
+ return toZodSchema(bookmark);
+ }),
+
+ updateBookmark: authedProcedure
+ .input(zUpdateBookmarksRequestSchema)
+ .output(zBookmarkSchema)
+ .mutation(async ({ input, ctx }) => {
+ const bookmark = await prisma.bookmark.update({
+ where: {
+ id: input.bookmarkId,
+ userId: ctx.user.id,
+ },
+ data: {
+ archived: input.archived,
+ favourited: input.favourited,
+ },
+ select: defaultBookmarkFields,
+ });
+ return toZodSchema(bookmark);
+ }),
+
+ deleteBookmark: authedProcedure
+ .input(z.object({ bookmarkId: z.string() }))
+ .mutation(async ({ input, ctx }) => {
+ await prisma.bookmark.delete({
+ where: {
+ id: input.bookmarkId,
+ userId: ctx.user.id,
+ },
+ });
+ }),
+ getBookmarks: authedProcedure
+ .input(zGetBookmarksRequestSchema)
+ .output(zGetBookmarksResponseSchema)
+ .query(async ({ input, ctx }) => {
+ const bookmarks = (
+ await prisma.bookmark.findMany({
+ where: {
+ userId: ctx.user.id,
+ archived: input.archived,
+ favourited: input.favourited,
+ },
+ orderBy: {
+ createdAt: "desc",
+ },
+ select: defaultBookmarkFields,
+ })
+ ).map(toZodSchema);
+
+ return { bookmarks };
+ }),
+});
diff --git a/packages/web/server/api/trpc.ts b/packages/web/server/api/trpc.ts
new file mode 100644
index 00000000..82aa2d18
--- /dev/null
+++ b/packages/web/server/api/trpc.ts
@@ -0,0 +1,31 @@
+import { TRPCError, initTRPC } from "@trpc/server";
+import { Session } from "next-auth";
+
+export type Context = {
+ session: Session | null;
+};
+
+// Avoid exporting the entire t-object
+// since it's not very descriptive.
+// For instance, the use of a t variable
+// is common in i18n libraries.
+const t = initTRPC.context<Context>().create();
+export const createCallerFactory = t.createCallerFactory;
+// Base router and procedure helpers
+export const router = t.router;
+export const procedure = t.procedure;
+export const publicProcedure = t.procedure;
+
+export const authedProcedure = t.procedure.use(function isAuthed(opts) {
+ const user = opts.ctx.session?.user;
+
+ if (!user) {
+ throw new TRPCError({ code: "UNAUTHORIZED" });
+ }
+
+ return opts.next({
+ ctx: {
+ user,
+ },
+ });
+});
diff --git a/packages/web/server/auth.ts b/packages/web/server/auth.ts
new file mode 100644
index 00000000..05d3d296
--- /dev/null
+++ b/packages/web/server/auth.ts
@@ -0,0 +1,39 @@
+import NextAuth, { NextAuthOptions, getServerSession } from "next-auth";
+import { PrismaAdapter } from "@next-auth/prisma-adapter";
+import AuthentikProvider from "next-auth/providers/authentik";
+import serverConfig from "@/server/config";
+import { prisma } from "@remember/db";
+import { DefaultSession } from "next-auth";
+
+declare module "next-auth" {
+ /**
+ * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
+ */
+ export interface Session {
+ user: {
+ id: string;
+ } & DefaultSession["user"];
+ }
+}
+
+const providers = [];
+
+if (serverConfig.auth.authentik) {
+ providers.push(AuthentikProvider(serverConfig.auth.authentik));
+}
+
+export const authOptions: NextAuthOptions = {
+ // Configure one or more authentication providers
+ adapter: PrismaAdapter(prisma),
+ providers: providers,
+ callbacks: {
+ session({ session, user }) {
+ session.user = { ...user };
+ return session;
+ },
+ },
+};
+
+export const authHandler = NextAuth(authOptions);
+
+export const getServerAuthSession = () => getServerSession(authOptions);
diff --git a/packages/web/server/config.ts b/packages/web/server/config.ts
new file mode 100644
index 00000000..dbf6620e
--- /dev/null
+++ b/packages/web/server/config.ts
@@ -0,0 +1,22 @@
+function buildAuthentikConfig() {
+ const { AUTHENTIK_ID, AUTHENTIK_SECRET, AUTHENTIK_ISSUER } = process.env;
+
+ if (!AUTHENTIK_ID || !AUTHENTIK_SECRET || !AUTHENTIK_ISSUER) {
+ return undefined;
+ }
+
+ return {
+ clientId: AUTHENTIK_ID,
+ clientSecret: AUTHENTIK_SECRET,
+ issuer: AUTHENTIK_ISSUER,
+ };
+}
+
+const serverConfig = {
+ api_url: process.env.API_URL || "http://localhost:3000",
+ auth: {
+ authentik: buildAuthentikConfig(),
+ },
+};
+
+export default serverConfig;
diff --git a/packages/web/server/routers/_app.ts b/packages/web/server/routers/_app.ts
deleted file mode 100644
index 47c586b7..00000000
--- a/packages/web/server/routers/_app.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { z } from "zod";
-import { procedure, router } from "../trpc";
-export const appRouter = router({
- hello: procedure
- .input(
- z.object({
- text: z.string(),
- }),
- )
- .query((opts) => {
- return {
- greeting: `hello ${opts.input.text}`,
- };
- }),
-});
-// export type definition of API
-export type AppRouter = typeof appRouter;
diff --git a/packages/web/server/trpc.ts b/packages/web/server/trpc.ts
deleted file mode 100644
index b34424ed..00000000
--- a/packages/web/server/trpc.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { initTRPC } from '@trpc/server';
-// Avoid exporting the entire t-object
-// since it's not very descriptive.
-// For instance, the use of a t variable
-// is common in i18n libraries.
-const t = initTRPC.create();
-// Base router and procedure helpers
-export const router = t.router;
-export const procedure = t.procedure;
diff --git a/packages/web/server/utils.ts b/packages/web/server/utils.ts
deleted file mode 100644
index 70c06585..00000000
--- a/packages/web/server/utils.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { httpBatchLink } from "@trpc/client";
-import { createTRPCNext } from "@trpc/next";
-import type { AppRouter } from "../server/routers/_app";
-import serverConfig from "@/lib/config";
-
-export const trpc = createTRPCNext<AppRouter>({
- config(_opts) {
- return {
- links: [
- httpBatchLink({
- url: `${serverConfig.api_url}/api/trpc`,
- }),
- ],
- };
- },
-});