From 2ac3c39a9c80305bb959d88561e78f65a1cd1be1 Mon Sep 17 00:00:00 2001 From: MohamedBassem Date: Thu, 22 Feb 2024 17:33:12 +0000 Subject: feature: Adding some loading card while the link is getting crawled --- README.md | 3 +- .../app/dashboard/bookmarks/components/AddLink.tsx | 16 +--- .../bookmarks/components/BookmarksGrid.tsx | 7 -- .../dashboard/bookmarks/components/LinkCard.tsx | 85 +++++++++++++++++----- .../web/app/signin/components/CredentialsForm.tsx | 4 +- packages/web/lib/hooks/use-loading-card.ts | 9 --- packages/web/lib/types/api/bookmarks.ts | 3 +- packages/web/server/api/routers/bookmarks.ts | 25 +++++++ 8 files changed, 102 insertions(+), 50 deletions(-) delete mode 100644 packages/web/lib/hooks/use-loading-card.ts 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>({ + 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>({ - resolver: zodResolver(formSchema), }); const onError: SubmitErrorHandler> = (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 (
- {loading && } {data.bookmarks.map((b) => renderBookmark(b))}
); 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 ( + + + + + + ); + } + return ( + + {bookmark.tags.map((t) => ( + + + #{t.name} + + + ))} + + ); +} + +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 }) { } > - + @@ -38,22 +102,7 @@ export default function LinkCard({ bookmark }: { bookmark: ZBookmark }) { {/* 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. */} - - {bookmark.tags.map((t) => ( - - - #{t.name} - - - ))} - + {TagList(bookmark, isCrawling)}
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() { ); }} /> - + Sign In
@@ -195,7 +195,7 @@ function SignUp() { ); }} /> - + Sign Up
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; @@ -18,7 +19,7 @@ export type ZBookmarkContent = z.infer; 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) -- cgit v1.2.3-70-g09d2