diff options
| author | MohamedBassem <me@mbassem.com> | 2024-02-11 14:54:52 +0000 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2024-02-11 14:55:09 +0000 |
| commit | 2c2d05fd0a2c3c26d765f8a6beb88d907a097c1d (patch) | |
| tree | c4738ba0bc011d60361f89aca9be3293474ab9e9 /packages/web/server | |
| parent | c2f1d6d8b8a0f09820153fc736806b147d46abfe (diff) | |
| download | karakeep-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.ts | 14 | ||||
| -rw-r--r-- | packages/web/server/api/routers/_app.ts | 7 | ||||
| -rw-r--r-- | packages/web/server/api/routers/bookmarks.ts | 139 | ||||
| -rw-r--r-- | packages/web/server/api/trpc.ts | 31 | ||||
| -rw-r--r-- | packages/web/server/auth.ts | 39 | ||||
| -rw-r--r-- | packages/web/server/config.ts | 22 | ||||
| -rw-r--r-- | packages/web/server/routers/_app.ts | 17 | ||||
| -rw-r--r-- | packages/web/server/trpc.ts | 9 | ||||
| -rw-r--r-- | packages/web/server/utils.ts | 16 |
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`, - }), - ], - }; - }, -}); |
