aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/web/app/public/lists/[listId]/page.tsx16
-rw-r--r--apps/web/components/public/lists/PublicBookmarkGrid.tsx1
-rw-r--r--packages/api/routes/public.ts44
-rw-r--r--packages/api/routes/public/assets.ts49
-rw-r--r--packages/trpc/models/lists.ts44
-rw-r--r--packages/trpc/routers/publicBookmarks.ts24
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(),
}),