diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-06-01 20:46:41 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-01 20:46:41 +0100 |
| commit | ea1d0023bfee55358ebb1a96f3d06e783a219c0d (patch) | |
| tree | 5bddd451728cb7dd377574a9ea1ea591bca069c4 /packages/shared | |
| parent | 3afe1e21df6dcc0483e74e0db02d9d82af32ecea (diff) | |
| download | karakeep-ea1d0023bfee55358ebb1a96f3d06e783a219c0d.tar.zst | |
feat: Add support for public lists (#1511)
* WIP: public lists
* Drop viewing modes
* Add the public endpoint for assets
* regen the openapi spec
* proper handling for different asset types
* Add num bookmarks and a no bookmark banner
* Correctly set page title
* Add a not-found page
* merge the RSS and public list endpoints
* Add e2e tests for the public endpoints
* Redesign the share list modal
* Make NEXTAUTH_SECRET not required
* propery render text bookmarks
* rebase migration
* fix public token tests
* Add more tests
Diffstat (limited to 'packages/shared')
| -rw-r--r-- | packages/shared/config.ts | 7 | ||||
| -rw-r--r-- | packages/shared/signedTokens.ts | 71 | ||||
| -rw-r--r-- | packages/shared/types/assets.ts | 6 | ||||
| -rw-r--r-- | packages/shared/types/bookmarks.ts | 2 | ||||
| -rw-r--r-- | packages/shared/types/lists.ts | 2 | ||||
| -rw-r--r-- | packages/shared/utils/bookmarkUtils.ts | 22 |
6 files changed, 106 insertions, 4 deletions
diff --git a/packages/shared/config.ts b/packages/shared/config.ts index 218b46b0..b899dbeb 100644 --- a/packages/shared/config.ts +++ b/packages/shared/config.ts @@ -18,6 +18,7 @@ const optionalStringBool = () => const allEnv = z.object({ API_URL: z.string().url().default("http://localhost:3000"), NEXTAUTH_URL: z.string().url().default("http://localhost:3000"), + NEXTAUTH_SECRET: z.string().optional(), DISABLE_SIGNUPS: stringBool("false"), DISABLE_PASSWORD_AUTH: stringBool("false"), OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING: stringBool("false"), @@ -94,6 +95,12 @@ const serverConfigSchema = allEnv.transform((val) => { apiUrl: val.API_URL, publicUrl: val.NEXTAUTH_URL, publicApiUrl: `${val.NEXTAUTH_URL}/api`, + signingSecret: () => { + if (!val.NEXTAUTH_SECRET) { + throw new Error("NEXTAUTH_SECRET is not set"); + } + return val.NEXTAUTH_SECRET; + }, auth: { disableSignups: val.DISABLE_SIGNUPS, disablePasswordAuth: val.DISABLE_PASSWORD_AUTH, diff --git a/packages/shared/signedTokens.ts b/packages/shared/signedTokens.ts new file mode 100644 index 00000000..b5e27f3e --- /dev/null +++ b/packages/shared/signedTokens.ts @@ -0,0 +1,71 @@ +import crypto from "node:crypto"; +import { z } from "zod"; + +import serverConfig from "./config"; + +const zTokenPayload = z.object({ + payload: z.unknown(), + expiresAt: z.number(), +}); + +const zSignedTokenPayload = z.object({ + payload: zTokenPayload, + signature: z.string(), +}); + +export type SignedTokenPayload = z.infer<typeof zSignedTokenPayload>; + +export function createSignedToken( + payload: unknown, + expiryEpoch?: number, +): string { + const expiresAt = expiryEpoch ?? Date.now() + 5 * 60 * 1000; // 5 minutes from now + + const toBeSigned: z.infer<typeof zTokenPayload> = { + payload, + expiresAt, + }; + + const payloadString = JSON.stringify(toBeSigned); + const signature = crypto + .createHmac("sha256", serverConfig.signingSecret()) + .update(payloadString) + .digest("hex"); + + const tokenData: z.infer<typeof zSignedTokenPayload> = { + payload: toBeSigned, + signature, + }; + + return Buffer.from(JSON.stringify(tokenData)).toString("base64"); +} + +export function verifySignedToken<T>( + token: string, + schema: z.ZodSchema<T>, +): T | null { + try { + const tokenData = zSignedTokenPayload.parse( + JSON.parse(Buffer.from(token, "base64").toString()), + ); + const { payload, signature } = tokenData; + + // Verify signature + const expectedSignature = crypto + .createHmac("sha256", serverConfig.signingSecret()) + .update(JSON.stringify(payload)) + .digest("hex"); + + if (signature !== expectedSignature) { + return null; + } + // Check expiry + if (Date.now() > payload.expiresAt) { + return null; + } + + return schema.parse(payload.payload); + } catch { + return null; + } +} diff --git a/packages/shared/types/assets.ts b/packages/shared/types/assets.ts new file mode 100644 index 00000000..fe0adcfd --- /dev/null +++ b/packages/shared/types/assets.ts @@ -0,0 +1,6 @@ +import { z } from "zod"; + +export const zAssetSignedTokenSchema = z.object({ + assetId: z.string(), + userId: z.string(), +}); diff --git a/packages/shared/types/bookmarks.ts b/packages/shared/types/bookmarks.ts index 3522fad3..ea1ab717 100644 --- a/packages/shared/types/bookmarks.ts +++ b/packages/shared/types/bookmarks.ts @@ -250,6 +250,7 @@ export const zPublicBookmarkSchema = z.object({ title: z.string().nullish(), tags: z.array(z.string()), description: z.string().nullish(), + bannerImageUrl: z.string().nullable(), content: z.discriminatedUnion("type", [ z.object({ type: z.literal(BookmarkTypes.LINK), @@ -264,6 +265,7 @@ export const zPublicBookmarkSchema = z.object({ type: z.literal(BookmarkTypes.ASSET), assetType: z.enum(["image", "pdf"]), assetId: z.string(), + assetUrl: z.string(), fileName: z.string().nullish(), sourceUrl: z.string().nullish(), }), diff --git a/packages/shared/types/lists.ts b/packages/shared/types/lists.ts index 7ef5687c..51fb458c 100644 --- a/packages/shared/types/lists.ts +++ b/packages/shared/types/lists.ts @@ -47,6 +47,7 @@ export const zBookmarkListSchema = z.object({ parentId: z.string().nullable(), type: z.enum(["manual", "smart"]).default("manual"), query: z.string().nullish(), + public: z.boolean(), }); export type ZBookmarkList = z.infer<typeof zBookmarkListSchema>; @@ -66,6 +67,7 @@ export const zEditBookmarkListSchema = z.object({ icon: z.string().optional(), parentId: z.string().nullish(), query: z.string().min(1).optional(), + public: z.boolean().optional(), }); export const zEditBookmarkListSchemaWithValidation = zEditBookmarkListSchema diff --git a/packages/shared/utils/bookmarkUtils.ts b/packages/shared/utils/bookmarkUtils.ts index 31d7b698..97ef08fc 100644 --- a/packages/shared/utils/bookmarkUtils.ts +++ b/packages/shared/utils/bookmarkUtils.ts @@ -3,18 +3,32 @@ import { getAssetUrl } from "./assetUtils"; const MAX_LOADING_MSEC = 30 * 1000; -export function getBookmarkLinkImageUrl(bookmark: ZBookmarkedLink) { +export function getBookmarkLinkAssetIdOrUrl(bookmark: ZBookmarkedLink) { if (bookmark.imageAssetId) { - return { url: getAssetUrl(bookmark.imageAssetId), localAsset: true }; + return { assetId: bookmark.imageAssetId, localAsset: true as const }; } if (bookmark.screenshotAssetId) { - return { url: getAssetUrl(bookmark.screenshotAssetId), localAsset: true }; + return { assetId: bookmark.screenshotAssetId, localAsset: true as const }; } return bookmark.imageUrl - ? { url: bookmark.imageUrl, localAsset: false } + ? { url: bookmark.imageUrl, localAsset: false as const } : null; } +export function getBookmarkLinkImageUrl(bookmark: ZBookmarkedLink) { + const assetOrUrl = getBookmarkLinkAssetIdOrUrl(bookmark); + if (!assetOrUrl) { + return null; + } + if (!assetOrUrl.localAsset) { + return assetOrUrl; + } + return { + url: getAssetUrl(assetOrUrl.assetId), + localAsset: true, + }; +} + export function isBookmarkStillCrawling(bookmark: ZBookmark) { return ( bookmark.content.type == BookmarkTypes.LINK && |
