diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-06-07 16:46:36 +0000 |
|---|---|---|
| committer | Mohamed Bassem <me@mbassem.com> | 2025-06-07 16:46:36 +0000 |
| commit | bc65a73872cf0707d2433c289d1f04423325ed95 (patch) | |
| tree | 95de3c17907e0ea79fbca1d058263b87b3bb4c6f | |
| parent | a98f02369c5b2aea8831cbbff840fbd2ae395a7d (diff) | |
| download | karakeep-bc65a73872cf0707d2433c289d1f04423325ed95.tar.zst | |
fix: Use a new public list metadata endpoint for metadata generation
| -rw-r--r-- | apps/web/app/public/lists/[listId]/page.tsx | 16 | ||||
| -rw-r--r-- | apps/web/components/public/lists/PublicBookmarkGrid.tsx | 1 | ||||
| -rw-r--r-- | packages/api/routes/public.ts | 44 | ||||
| -rw-r--r-- | packages/api/routes/public/assets.ts | 49 | ||||
| -rw-r--r-- | packages/trpc/models/lists.ts | 44 | ||||
| -rw-r--r-- | packages/trpc/routers/publicBookmarks.ts | 24 |
6 files changed, 126 insertions, 52 deletions
diff --git a/apps/web/app/public/lists/[listId]/page.tsx b/apps/web/app/public/lists/[listId]/page.tsx index c0495b9f..cdfc46d0 100644 --- a/apps/web/app/public/lists/[listId]/page.tsx +++ b/apps/web/app/public/lists/[listId]/page.tsx @@ -12,13 +12,22 @@ export async function generateMetadata({ }: { params: { listId: string }; }): Promise<Metadata> { - // TODO: Don't load the entire list, just create an endpoint to get the list name try { - const resp = await api.publicBookmarks.getPublicBookmarksInList({ + const resp = await api.publicBookmarks.getPublicListMetadata({ listId: params.listId, }); return { - title: `${resp.list.name} - Karakeep`, + title: `${resp.name} by ${resp.ownerName} - Karakeep`, + description: + resp.description && resp.description.length > 0 + ? `${resp.description} by ${resp.ownerName} on Karakeep` + : undefined, + applicationName: "Karakeep", + authors: [ + { + name: resp.ownerName, + }, + ], }; } catch (e) { if (e instanceof TRPCError && e.code === "NOT_FOUND") { @@ -67,6 +76,7 @@ export default async function PublicListPage({ description: list.description, icon: list.icon, numItems: list.numItems, + ownerName: list.ownerName, }} bookmarks={bookmarks} nextCursor={nextCursor} diff --git a/apps/web/components/public/lists/PublicBookmarkGrid.tsx b/apps/web/components/public/lists/PublicBookmarkGrid.tsx index 038ac3ae..0aa16eae 100644 --- a/apps/web/components/public/lists/PublicBookmarkGrid.tsx +++ b/apps/web/components/public/lists/PublicBookmarkGrid.tsx @@ -192,6 +192,7 @@ export default function PublicBookmarkGrid({ description: string | null | undefined; icon: string; numItems: number; + ownerName: string; }; bookmarks: ZPublicBookmark[]; nextCursor: ZCursor | null; diff --git a/packages/api/routes/public.ts b/packages/api/routes/public.ts index d17049c4..160a9379 100644 --- a/packages/api/routes/public.ts +++ b/packages/api/routes/public.ts @@ -1,47 +1,7 @@ -import { zValidator } from "@hono/zod-validator"; -import { and, eq } from "drizzle-orm"; import { Hono } from "hono"; -import { z } from "zod"; -import { assets } from "@karakeep/db/schema"; -import { verifySignedToken } from "@karakeep/shared/signedTokens"; -import { zAssetSignedTokenSchema } from "@karakeep/shared/types/assets"; +import assets from "./public/assets"; -import { unauthedMiddleware } from "../middlewares/auth"; -import { serveAsset } from "../utils/assets"; - -const app = new Hono().get( - "/assets/:assetId", - unauthedMiddleware, - zValidator( - "query", - z.object({ - token: z.string(), - }), - ), - async (c) => { - const assetId = c.req.param("assetId"); - const tokenPayload = verifySignedToken( - c.req.valid("query").token, - zAssetSignedTokenSchema, - ); - if (!tokenPayload) { - return c.json({ error: "Invalid or expired token" }, { status: 403 }); - } - if (tokenPayload.assetId !== assetId) { - return c.json({ error: "Invalid or expired token" }, { status: 403 }); - } - const userId = tokenPayload.userId; - - const assetDb = await c.var.ctx.db.query.assets.findFirst({ - where: and(eq(assets.id, assetId), eq(assets.userId, userId)), - }); - - if (!assetDb) { - return c.json({ error: "Asset not found" }, { status: 404 }); - } - return await serveAsset(c, assetId, userId); - }, -); +const app = new Hono().route("/assets", assets); export default app; diff --git a/packages/api/routes/public/assets.ts b/packages/api/routes/public/assets.ts new file mode 100644 index 00000000..4f2827d5 --- /dev/null +++ b/packages/api/routes/public/assets.ts @@ -0,0 +1,49 @@ +import { zValidator } from "@hono/zod-validator"; +import { and, eq } from "drizzle-orm"; +import { Hono } from "hono"; +import { z } from "zod"; + +import { assets } from "@karakeep/db/schema"; +import { verifySignedToken } from "@karakeep/shared/signedTokens"; +import { zAssetSignedTokenSchema } from "@karakeep/shared/types/assets"; + +import { unauthedMiddleware } from "../../middlewares/auth"; +import { serveAsset } from "../../utils/assets"; + +const app = new Hono() + // Public assets, they require signed token for auth + .get( + "/:assetId", + unauthedMiddleware, + zValidator( + "query", + z.object({ + token: z.string(), + }), + ), + async (c) => { + const assetId = c.req.param("assetId"); + const tokenPayload = verifySignedToken( + c.req.valid("query").token, + zAssetSignedTokenSchema, + ); + if (!tokenPayload) { + return c.json({ error: "Invalid or expired token" }, { status: 403 }); + } + if (tokenPayload.assetId !== assetId) { + return c.json({ error: "Invalid or expired token" }, { status: 403 }); + } + const userId = tokenPayload.userId; + + const assetDb = await c.var.ctx.db.query.assets.findFirst({ + where: and(eq(assets.id, assetId), eq(assets.userId, userId)), + }); + + if (!assetDb) { + return c.json({ error: "Asset not found" }, { status: 404 }); + } + return await serveAsset(c, assetId, userId); + }, + ); + +export default app; diff --git a/packages/trpc/models/lists.ts b/packages/trpc/models/lists.ts index 2631ca7e..39d78ac1 100644 --- a/packages/trpc/models/lists.ts +++ b/packages/trpc/models/lists.ts @@ -63,15 +63,10 @@ export abstract class List implements PrivacyAware { } } - static async getPublicListContents( + private static async getPublicList( ctx: Context, listId: string, token: string | null, - pagination: { - limit: number; - order: Exclude<ZSortOrder, "relevance">; - cursor: ZCursor | null | undefined; - }, ) { const listdb = await ctx.db.query.bookmarkLists.findFirst({ where: and( @@ -81,6 +76,13 @@ export abstract class List implements PrivacyAware { token !== null ? eq(bookmarkLists.rssToken, token) : undefined, ), ), + with: { + user: { + columns: { + name: true, + }, + }, + }, }); if (!listdb) { throw new TRPCError({ @@ -88,6 +90,35 @@ export abstract class List implements PrivacyAware { message: "List not found", }); } + return listdb; + } + + static async getPublicListMetadata( + ctx: Context, + listId: string, + token: string | null, + ) { + const listdb = await this.getPublicList(ctx, listId, token); + return { + userId: listdb.userId, + name: listdb.name, + description: listdb.description, + icon: listdb.icon, + ownerName: listdb.user.name, + }; + } + + static async getPublicListContents( + ctx: Context, + listId: string, + token: string | null, + pagination: { + limit: number; + order: Exclude<ZSortOrder, "relevance">; + cursor: ZCursor | null | undefined; + }, + ) { + const listdb = await this.getPublicList(ctx, listId, token); // The token here acts as an authed context, so we can create // an impersonating context for the list owner as long as @@ -109,6 +140,7 @@ export abstract class List implements PrivacyAware { icon: list.list.icon, name: list.list.name, description: list.list.description, + ownerName: listdb.user.name, numItems: bookmarkIds.length, }, bookmarks: bookmarks.bookmarks.map((b) => b.asPublicBookmark()), diff --git a/packages/trpc/routers/publicBookmarks.ts b/packages/trpc/routers/publicBookmarks.ts index 6b643354..be852b67 100644 --- a/packages/trpc/routers/publicBookmarks.ts +++ b/packages/trpc/routers/publicBookmarks.ts @@ -12,6 +12,28 @@ import { publicProcedure, router } from "../index"; import { List } from "../models/lists"; export const publicBookmarks = router({ + getPublicListMetadata: publicProcedure + .input( + z.object({ + listId: z.string(), + }), + ) + .output( + zBookmarkListSchema + .pick({ + name: true, + description: true, + icon: true, + }) + .merge(z.object({ ownerName: z.string() })), + ) + .query(async ({ input, ctx }) => { + return await List.getPublicListMetadata( + ctx, + input.listId, + /* token */ null, + ); + }), getPublicBookmarksInList: publicProcedure .input( z.object({ @@ -29,7 +51,7 @@ export const publicBookmarks = router({ description: true, icon: true, }) - .merge(z.object({ numItems: z.number() })), + .merge(z.object({ numItems: z.number(), ownerName: z.string() })), bookmarks: z.array(zPublicBookmarkSchema), nextCursor: zCursorV2.nullable(), }), |
