aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-02-22 17:33:12 +0000
committerMohamedBassem <me@mbassem.com>2024-02-22 17:34:37 +0000
commit2ac3c39a9c80305bb959d88561e78f65a1cd1be1 (patch)
treeabdf860a648d691377914702f4d5c804e02fd341
parent61a1b2f40cf69d8c2055becf9119881cafa9da81 (diff)
downloadkarakeep-2ac3c39a9c80305bb959d88561e78f65a1cd1be1.tar.zst
feature: Adding some loading card while the link is getting crawled
-rw-r--r--README.md3
-rw-r--r--packages/web/app/dashboard/bookmarks/components/AddLink.tsx16
-rw-r--r--packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx7
-rw-r--r--packages/web/app/dashboard/bookmarks/components/LinkCard.tsx85
-rw-r--r--packages/web/app/signin/components/CredentialsForm.tsx4
-rw-r--r--packages/web/lib/hooks/use-loading-card.ts9
-rw-r--r--packages/web/lib/types/api/bookmarks.ts3
-rw-r--r--packages/web/server/api/routers/bookmarks.ts25
8 files changed, 102 insertions, 50 deletions
diff --git a/README.md b/README.md
index 8c35b14c..69a1e47c 100644
--- a/README.md
+++ b/README.md
@@ -36,12 +36,13 @@ The app is configured with env variables.
## Security Considerations
If you're going to give app access to untrusted users, there's some security considerations that you'll need to be aware of given how the crawler works. The crawler is basically running a browser to fetch the content of the bookmarks. Any untrusted user can submit bookmarks to be crawled from your server and they'll be able to see the crawling result. This can be abused in multiple ways:
+
1. Untrused users can submit crawl requests to websites that you don't want to be coming out of your IPs.
2. Crawling user controlled websites can expose your origin IP (and location) even if your service is hosted behind cloudflare for example.
3. The crawling requests will be coming out from your own network, which untrusted users can leverage to crawl internal non-internet exposed endpoints.
-
To mitigate those risks, you can do one of the following:
+
1. Limit access to trusted users
2. Let the browser traffic go through some VPN with restricted network policies.
3. Host the browser container outside of your network.
diff --git a/packages/web/app/dashboard/bookmarks/components/AddLink.tsx b/packages/web/app/dashboard/bookmarks/components/AddLink.tsx
index 7663543f..242a52a5 100644
--- a/packages/web/app/dashboard/bookmarks/components/AddLink.tsx
+++ b/packages/web/app/dashboard/bookmarks/components/AddLink.tsx
@@ -9,32 +9,24 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { toast } from "@/components/ui/use-toast";
import { api } from "@/lib/trpc";
import { ActionButton } from "@/components/ui/action-button";
-import { useLoadingCard } from "@/lib/hooks/use-loading-card";
const formSchema = z.object({
url: z.string().url({ message: "The link must be a valid URL" }),
});
export default function AddLink() {
- const { setLoading } = useLoadingCard();
+ const form = useForm<z.infer<typeof formSchema>>({
+ resolver: zodResolver(formSchema),
+ });
+
const invalidateBookmarksCache = api.useUtils().bookmarks.invalidate;
const bookmarkLinkMutator = api.bookmarks.bookmarkLink.useMutation({
- onMutate: () => {
- setLoading(true);
- },
onSuccess: () => {
invalidateBookmarksCache();
},
onError: () => {
toast({ description: "Something went wrong", variant: "destructive" });
},
- onSettled: () => {
- setLoading(false);
- },
- });
-
- const form = useForm<z.infer<typeof formSchema>>({
- resolver: zodResolver(formSchema),
});
const onError: SubmitErrorHandler<z.infer<typeof formSchema>> = (errors) => {
diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx
index c1c8f3e0..dc98472e 100644
--- a/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx
+++ b/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx
@@ -1,7 +1,5 @@
"use client";
-import { useLoadingCard } from "@/lib/hooks/use-loading-card";
-import BookmarkCardSkeleton from "./BookmarkCardSkeleton";
import LinkCard from "./LinkCard";
import { ZBookmark, ZGetBookmarksRequest } from "@/lib/types/api/bookmarks";
import { api } from "@/lib/trpc";
@@ -13,8 +11,6 @@ function renderBookmark(bookmark: ZBookmark) {
}
}
-export const dynamic = "force-dynamic";
-
export default function BookmarksGrid({
query,
bookmarks: initialBookmarks,
@@ -24,12 +20,9 @@ export default function BookmarksGrid({
}) {
const { data } = api.bookmarks.getBookmarks.useQuery(query, {
initialData: { bookmarks: initialBookmarks },
- staleTime: Infinity,
});
- const { loading } = useLoadingCard();
return (
<div className="container grid grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
- {loading && <BookmarkCardSkeleton />}
{data.bookmarks.map((b) => renderBookmark(b))}
</div>
);
diff --git a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx
index 7413c2fe..35696134 100644
--- a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx
+++ b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import { Badge } from "@/components/ui/badge";
import {
ImageCard,
@@ -10,9 +12,71 @@ import {
import { ZBookmark } from "@/lib/types/api/bookmarks";
import Link from "next/link";
import BookmarkOptions from "./BookmarkOptions";
+import { api } from "@/lib/trpc";
+import { Skeleton } from "@/components/ui/skeleton";
+
+function isStillCrawling(bookmark: ZBookmark) {
+ return (
+ !bookmark.content.crawledAt && Date.now() - bookmark.createdAt < 30 * 1000
+ );
+}
-export default function LinkCard({ bookmark }: { bookmark: ZBookmark }) {
+function TagList(bookmark: ZBookmark, stillCrawling: boolean) {
+ if (stillCrawling) {
+ return (
+ <ImageCardBody className="space-y-2">
+ <Skeleton className="h-4 w-full" />
+ <Skeleton className="h-4 w-full" />
+ <Skeleton className="h-4 w-full" />
+ </ImageCardBody>
+ );
+ }
+ return (
+ <ImageCardBody className="flex h-full flex-wrap space-x-1 overflow-hidden">
+ {bookmark.tags.map((t) => (
+ <Link
+ className="flex h-full flex-col justify-end"
+ key={t.id}
+ href={`/dashboard/tags/${t.name}`}
+ >
+ <Badge
+ variant="default"
+ className="text-nowrap bg-gray-300 text-gray-500 hover:text-white"
+ >
+ #{t.name}
+ </Badge>
+ </Link>
+ ))}
+ </ImageCardBody>
+ );
+}
+
+export default function LinkCard({
+ bookmark: initialData,
+}: {
+ bookmark: ZBookmark;
+}) {
+ const { data: bookmark } = api.bookmarks.getBookmark.useQuery(
+ {
+ id: initialData.id,
+ },
+ {
+ initialData,
+ refetchInterval: (query) => {
+ const data = query.state.data;
+ if (!data) {
+ return false;
+ }
+ // If the link is not crawled and
+ if (isStillCrawling(data)) {
+ return 1000;
+ }
+ return false;
+ },
+ },
+ );
const link = bookmark.content;
+ const isCrawling = isStillCrawling(bookmark);
const parsedUrl = new URL(link.url);
// A dummy white pixel for when there's no image.
@@ -28,7 +92,7 @@ export default function LinkCard({ bookmark }: { bookmark: ZBookmark }) {
}
>
<Link href={link.url}>
- <ImageCardBanner src={image} />
+ <ImageCardBanner src={isCrawling ? "/blur.avif" : image} />
</Link>
<ImageCardContent>
<ImageCardTitle>
@@ -38,22 +102,7 @@ export default function LinkCard({ bookmark }: { bookmark: ZBookmark }) {
</ImageCardTitle>
{/* There's a hack here. Every tag has the full hight of the container itself. That why, when we enable flex-wrap,
the overflowed don't show up. */}
- <ImageCardBody className="flex h-full flex-wrap space-x-1 overflow-hidden">
- {bookmark.tags.map((t) => (
- <Link
- className="flex h-full flex-col justify-end"
- key={t.id}
- href={`/dashboard/tags/${t.name}`}
- >
- <Badge
- variant="default"
- className="text-nowrap bg-gray-300 text-gray-500 hover:text-white"
- >
- #{t.name}
- </Badge>
- </Link>
- ))}
- </ImageCardBody>
+ {TagList(bookmark, isCrawling)}
<ImageCardFooter>
<div className="flex justify-between text-gray-500">
<div className="my-auto">
diff --git a/packages/web/app/signin/components/CredentialsForm.tsx b/packages/web/app/signin/components/CredentialsForm.tsx
index 60b61156..f47708f6 100644
--- a/packages/web/app/signin/components/CredentialsForm.tsx
+++ b/packages/web/app/signin/components/CredentialsForm.tsx
@@ -84,7 +84,7 @@ function SignIn() {
);
}}
/>
- <ActionButton type="submit" loading={false}>
+ <ActionButton type="submit" loading={form.formState.isSubmitting}>
Sign In
</ActionButton>
</div>
@@ -195,7 +195,7 @@ function SignUp() {
);
}}
/>
- <ActionButton type="submit" loading={false}>
+ <ActionButton type="submit" loading={form.formState.isSubmitting}>
Sign Up
</ActionButton>
</div>
diff --git a/packages/web/lib/hooks/use-loading-card.ts b/packages/web/lib/hooks/use-loading-card.ts
deleted file mode 100644
index bb32a8f1..00000000
--- a/packages/web/lib/hooks/use-loading-card.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { create } from "zustand";
-
-export const useLoadingCard = create<{
- loading: boolean;
- setLoading: (val: boolean) => void;
-}>((set) => ({
- loading: false,
- setLoading: (val: boolean) => set({ loading: val }),
-}));
diff --git a/packages/web/lib/types/api/bookmarks.ts b/packages/web/lib/types/api/bookmarks.ts
index 772ef153..94f89e55 100644
--- a/packages/web/lib/types/api/bookmarks.ts
+++ b/packages/web/lib/types/api/bookmarks.ts
@@ -8,6 +8,7 @@ export const zBookmarkedLinkSchema = z.object({
description: z.string().nullish(),
imageUrl: z.string().url().nullish(),
favicon: z.string().url().nullish(),
+ crawledAt: z.date().nullish(),
});
export type ZBookmarkedLink = z.infer<typeof zBookmarkedLinkSchema>;
@@ -18,7 +19,7 @@ export type ZBookmarkContent = z.infer<typeof zBookmarkContentSchema>;
export const zBookmarkSchema = z.object({
id: z.string(),
- createdAt: z.coerce.date(),
+ createdAt: z.date(),
archived: z.boolean(),
favourited: z.boolean(),
tags: z.array(zBookmarkTagSchema),
diff --git a/packages/web/server/api/routers/bookmarks.ts b/packages/web/server/api/routers/bookmarks.ts
index 37d12eb9..65f20ef5 100644
--- a/packages/web/server/api/routers/bookmarks.ts
+++ b/packages/web/server/api/routers/bookmarks.ts
@@ -26,6 +26,7 @@ const defaultBookmarkFields = {
description: true,
imageUrl: true,
favicon: true,
+ crawledAt: true,
},
},
tags: {
@@ -153,6 +154,30 @@ export const bookmarksAppRouter = router({
bookmarkId: input.bookmarkId,
});
}),
+ getBookmark: authedProcedure
+ .input(
+ z.object({
+ id: z.string(),
+ }),
+ )
+ .output(zBookmarkSchema)
+ .query(async ({ input, ctx }) => {
+ const bookmark = await prisma.bookmark.findUnique({
+ where: {
+ userId: ctx.user.id,
+ id: input.id,
+ },
+ select: defaultBookmarkFields,
+ });
+ if (!bookmark) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Bookmark not found",
+ });
+ }
+
+ return toZodSchema(bookmark);
+ }),
getBookmarks: authedProcedure
.input(zGetBookmarksRequestSchema)
.output(zGetBookmarksResponseSchema)