diff --git a/.env.sample b/.env.sample index c3174e6..270538e 100644 --- a/.env.sample +++ b/.env.sample @@ -16,6 +16,7 @@ NEXT_PUBLIC_CREDENTIALS_ENABLED= DISABLE_NEW_SSO_USERS= RE_ARCHIVE_LIMIT= NEXT_PUBLIC_MAX_FILE_SIZE= +MAX_LINKS_PER_USER= # AWS S3 Settings SPACES_KEY= diff --git a/components/LinkViews/LinkComponents/LinkActions.tsx b/components/LinkViews/LinkComponents/LinkActions.tsx index 5bd18c0..015e32d 100644 --- a/components/LinkViews/LinkComponents/LinkActions.tsx +++ b/components/LinkViews/LinkComponents/LinkActions.tsx @@ -15,7 +15,7 @@ type Props = { link: LinkIncludingShortenedCollectionAndTags; collection: CollectionIncludingMembersAndLinkCount; position?: string; -} +}; export default function LinkActions({ link, collection, position }: Props) { const permissions = usePermissions(link.collection.id as number); @@ -23,7 +23,6 @@ export default function LinkActions({ link, collection, position }: Props) { const [editLinkModal, setEditLinkModal] = useState(false); const [deleteLinkModal, setDeleteLinkModal] = useState(false); const [preservedFormatsModal, setPreservedFormatsModal] = useState(false); - const [expandedLink, setExpandedLink] = useState(false); const { account } = useAccountStore(); @@ -42,7 +41,7 @@ export default function LinkActions({ link, collection, position }: Props) { toast.dismiss(load); response.ok && - toast.success(`Link ${isAlreadyPinned ? "Unpinned!" : "Pinned!"}`); + toast.success(`Link ${isAlreadyPinned ? "Unpinned!" : "Pinned!"}`); }; const deleteLink = async () => { @@ -57,85 +56,82 @@ export default function LinkActions({ link, collection, position }: Props) { return (
- {permissions === true || - permissions?.canUpdate || - permissions?.canDelete ? ( +
-
- -
-
    - {permissions === true ? ( -
  • -
    { - (document?.activeElement as HTMLElement)?.blur(); - pinLink(); - }} - > - {link?.pinnedBy && link.pinnedBy[0] - ? "Unpin" - : "Pin to Dashboard"} -
    -
  • - ) : undefined} - {permissions === true || permissions?.canUpdate ? ( -
  • -
    { - (document?.activeElement as HTMLElement)?.blur(); - setEditLinkModal(true); - }} - > - Edit -
    -
  • - ) : undefined} - {permissions === true ? ( -
  • -
    { - (document?.activeElement as HTMLElement)?.blur(); - setPreservedFormatsModal(true); - // updateArchive(); - }} - > - Preserved Formats -
    -
  • - ) : undefined} - {permissions === true || permissions?.canDelete ? ( -
  • -
    { - (document?.activeElement as HTMLElement)?.blur(); - e.shiftKey ? deleteLink() : setDeleteLinkModal(true); - }} - > - Delete -
    -
  • - ) : undefined} -
+
- ) : undefined} +
    + {permissions === true ? ( +
  • +
    { + (document?.activeElement as HTMLElement)?.blur(); + pinLink(); + }} + > + {link?.pinnedBy && link.pinnedBy[0] + ? "Unpin" + : "Pin to Dashboard"} +
    +
  • + ) : undefined} + {permissions === true || permissions?.canUpdate ? ( +
  • +
    { + (document?.activeElement as HTMLElement)?.blur(); + setEditLinkModal(true); + }} + > + Edit +
    +
  • + ) : undefined} +
  • +
    { + (document?.activeElement as HTMLElement)?.blur(); + setPreservedFormatsModal(true); + // updateArchive(); + }} + > + Preserved Formats +
    +
  • + {permissions === true || permissions?.canDelete ? ( +
  • +
    { + (document?.activeElement as HTMLElement)?.blur(); + e.shiftKey ? deleteLink() : setDeleteLinkModal(true); + }} + > + Delete +
    +
  • + ) : undefined} +
+
{editLinkModal ? ( { - let isPublicRoute = router.pathname.startsWith("/public") - ? true - : undefined; - (async () => { - const data = await getLink(link.id as number, isPublicRoute); + const data = await getLink(link.id as number, isPublic); setLink( (data as any).response as LinkIncludingShortenedCollectionAndTags ); @@ -93,7 +91,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { if (!isReady()) { interval = setInterval(async () => { - const data = await getLink(link.id as number, isPublicRoute); + const data = await getLink(link.id as number, isPublic); setLink( (data as any).response as LinkIncludingShortenedCollectionAndTags ); @@ -123,8 +121,11 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { toast.dismiss(load); if (response.ok) { + const newLink = await getLink(link?.id as number); + setLink( + (newLink as any).response as LinkIncludingShortenedCollectionAndTags + ); toast.success(`Link is being archived...`); - await getLink(link?.id as number); } else toast.error(data.response); }; @@ -148,20 +149,15 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
{isReady() ? ( <> - {readabilityAvailable(link) ? ( - - ) : undefined} - {screenshotAvailable(link) ? ( @@ -176,6 +172,15 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { downloadable={true} /> ) : undefined} + + {readabilityAvailable(link) ? ( + + ) : undefined} ) : (

- The Link preservation is in the queue + Link preservation is in the queue

Please check back later to see the result diff --git a/components/PreserverdFormatRow.tsx b/components/PreserverdFormatRow.tsx index e470829..393bee8 100644 --- a/components/PreserverdFormatRow.tsx +++ b/components/PreserverdFormatRow.tsx @@ -32,13 +32,11 @@ export default function PreservedFormatRow({ const router = useRouter(); - useEffect(() => { - let isPublicRoute = router.pathname.startsWith("/public") - ? true - : undefined; + let isPublic = router.pathname.startsWith("/public") ? true : undefined; + useEffect(() => { (async () => { - const data = await getLink(link.id as number, isPublicRoute); + const data = await getLink(link.id as number, isPublic); setLink( (data as any).response as LinkIncludingShortenedCollectionAndTags ); @@ -47,7 +45,7 @@ export default function PreservedFormatRow({ let interval: any; if (link?.screenshotPath === "pending" || link?.pdfPath === "pending") { interval = setInterval(async () => { - const data = await getLink(link.id as number, isPublicRoute); + const data = await getLink(link.id as number, isPublic); setLink( (data as any).response as LinkIncludingShortenedCollectionAndTags ); @@ -65,23 +63,6 @@ export default function PreservedFormatRow({ }; }, [link?.screenshotPath, link?.pdfPath, link?.readabilityPath]); - const updateArchive = async () => { - const load = toast.loading("Sending request..."); - - const response = await fetch(`/api/v1/links/${link?.id}/archive`, { - method: "PUT", - }); - - const data = await response.json(); - - toast.dismiss(load); - - if (response.ok) { - toast.success(`Link is being archived...`); - getLink(link?.id as number); - } else toast.error(data.response); - }; - const handleDownload = () => { const path = `/api/v1/archives/${link?.id}?format=${format}`; fetch(path) @@ -121,7 +102,9 @@ export default function PreservedFormatRow({ ) : undefined} diff --git a/components/PublicPage/PublicLinkCard.tsx b/components/PublicPage/PublicLinkCard.tsx index f683061..97c9586 100644 --- a/components/PublicPage/PublicLinkCard.tsx +++ b/components/PublicPage/PublicLinkCard.tsx @@ -4,6 +4,8 @@ import isValidUrl from "@/lib/shared/isValidUrl"; import unescapeString from "@/lib/client/unescapeString"; import { TagIncludingLinkCount } from "@/types/global"; import Link from "next/link"; +import { useState } from "react"; +import PreservedFormatsModal from "../ModalContent/PreservedFormatsModal"; interface LinksIncludingTags extends LinkType { tags: TagIncludingLinkCount[]; @@ -25,6 +27,8 @@ export default function LinkCard({ link, count }: Props) { day: "numeric", }); + const [preservedFormatsModal, setPreservedFormatsModal] = useState(false); + return (

@@ -77,15 +81,52 @@ export default function LinkCard({ link, count }: Props) {
{unescapeString(link.description)}{" "} -

Read

+

Visit

+
+
+ +
+
    +
  • +
    { + (document?.activeElement as HTMLElement)?.blur(); + setPreservedFormatsModal(true); + // updateArchive(); + }} + > + Preserved Formats +
    +
  • +
+
+ {preservedFormatsModal ? ( + setPreservedFormatsModal(false)} + activeLink={link as any} + /> + ) : undefined}
); } diff --git a/lib/api/archiveHandler.ts b/lib/api/archiveHandler.ts index b49fd82..037b42d 100644 --- a/lib/api/archiveHandler.ts +++ b/lib/api/archiveHandler.ts @@ -7,6 +7,7 @@ import { JSDOM } from "jsdom"; import DOMPurify from "dompurify"; import { Collection, Link, User } from "@prisma/client"; import validateUrlSize from "./validateUrlSize"; +import removeFile from "./storage/removeFile"; type LinksAndCollectionAndOwner = Link & { collection: Collection & { @@ -74,7 +75,7 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) { const content = await page.content(); - // TODO Webarchive + // TODO single file // const session = await page.context().newCDPSession(page); // const doc = await session.send("Page.captureSnapshot", { // format: "mhtml", @@ -189,6 +190,13 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) { : undefined, }, }); + else { + removeFile({ filePath: `archives/${link.collectionId}/${link.id}.png` }); + removeFile({ filePath: `archives/${link.collectionId}/${link.id}.pdf` }); + removeFile({ + filePath: `archives/${link.collectionId}/${link.id}_readability.json`, + }); + } await browser.close(); } diff --git a/lib/api/controllers/links/postLink.ts b/lib/api/controllers/links/postLink.ts index 912e972..9af69e1 100644 --- a/lib/api/controllers/links/postLink.ts +++ b/lib/api/controllers/links/postLink.ts @@ -6,6 +6,8 @@ import getPermission from "@/lib/api/getPermission"; import createFolder from "@/lib/api/storage/createFolder"; import validateUrlSize from "../../validateUrlSize"; +const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000; + export default async function postLink( link: LinkIncludingShortenedCollectionAndTags, userId: number @@ -24,6 +26,20 @@ export default async function postLink( link.collection.name = "Unorganized"; } + const numberOfLinksTheUserHas = await prisma.link.count({ + where: { + collection: { + ownerId: userId, + }, + }, + }); + + if (numberOfLinksTheUserHas + 1 > MAX_LINKS_PER_USER) + return { + response: `Error: Each user can only have a maximum of ${MAX_LINKS_PER_USER} Links.`, + status: 400, + }; + link.collection.name = link.collection.name.trim(); if (link.collection.id) { diff --git a/lib/api/controllers/migration/importFromHTMLFile.ts b/lib/api/controllers/migration/importFromHTMLFile.ts index 7791777..a2fae27 100644 --- a/lib/api/controllers/migration/importFromHTMLFile.ts +++ b/lib/api/controllers/migration/importFromHTMLFile.ts @@ -1,8 +1,9 @@ import { prisma } from "@/lib/api/db"; -import { Backup } from "@/types/global"; import createFolder from "@/lib/api/storage/createFolder"; import { JSDOM } from "jsdom"; +const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000; + export default async function importFromHTMLFile( userId: number, rawData: string @@ -10,6 +11,23 @@ export default async function importFromHTMLFile( const dom = new JSDOM(rawData); const document = dom.window.document; + const bookmarks = document.querySelectorAll("A"); + const totalImports = bookmarks.length; + + const numberOfLinksTheUserHas = await prisma.link.count({ + where: { + collection: { + ownerId: userId, + }, + }, + }); + + if (totalImports + numberOfLinksTheUserHas > MAX_LINKS_PER_USER) + return { + response: `Error: Each user can only have a maximum of ${MAX_LINKS_PER_USER} Links.`, + status: 400, + }; + const folders = document.querySelectorAll("H3"); await prisma diff --git a/lib/api/controllers/migration/importFromLinkwarden.ts b/lib/api/controllers/migration/importFromLinkwarden.ts index 78af4e3..51f8ecf 100644 --- a/lib/api/controllers/migration/importFromLinkwarden.ts +++ b/lib/api/controllers/migration/importFromLinkwarden.ts @@ -2,9 +2,34 @@ import { prisma } from "@/lib/api/db"; import { Backup } from "@/types/global"; import createFolder from "@/lib/api/storage/createFolder"; -export default async function getData(userId: number, rawData: string) { +const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000; + +export default async function importFromLinkwarden( + userId: number, + rawData: string +) { const data: Backup = JSON.parse(rawData); + let totalImports = 0; + + data.collections.forEach((collection) => { + totalImports += collection.links.length; + }); + + const numberOfLinksTheUserHas = await prisma.link.count({ + where: { + collection: { + ownerId: userId, + }, + }, + }); + + if (totalImports + numberOfLinksTheUserHas > MAX_LINKS_PER_USER) + return { + response: `Error: Each user can only have a maximum of ${MAX_LINKS_PER_USER} Links.`, + status: 400, + }; + await prisma .$transaction( async () => { diff --git a/lib/api/controllers/public/users/getPublicUser.ts b/lib/api/controllers/public/users/getPublicUser.ts index f67d126..04c7994 100644 --- a/lib/api/controllers/public/users/getPublicUser.ts +++ b/lib/api/controllers/public/users/getPublicUser.ts @@ -75,7 +75,7 @@ export default async function getPublicUser( username: lessSensitiveInfo.username, image: lessSensitiveInfo.image, archiveAsScreenshot: lessSensitiveInfo.archiveAsScreenshot, - archiveAsPdf: lessSensitiveInfo.archiveAsPDF, + archiveAsPDF: lessSensitiveInfo.archiveAsPDF, }; return { response: data, status: 200 }; diff --git a/pages/preserved/[id].tsx b/pages/preserved/[id].tsx index 0547f9b..84e614a 100644 --- a/pages/preserved/[id].tsx +++ b/pages/preserved/[id].tsx @@ -46,7 +46,14 @@ export default function Index() { + )} + {link && Number(router.query.format) === ArchivedFormat.jpeg && ( + )}
diff --git a/pages/public/collections/[id].tsx b/pages/public/collections/[id].tsx index 9f0d010..586ca4a 100644 --- a/pages/public/collections/[id].tsx +++ b/pages/public/collections/[id].tsx @@ -96,11 +96,11 @@ export default function PublicCollections() { return collection ? (
{collection ? ( @@ -208,6 +208,11 @@ export default function PublicCollections() { {links ?.filter((e) => e.collectionId === Number(router.query.id)) .map((e, i) => { + const linkWithCollectionData = { + ...e, + collection: collection, // Append collection data + }; + return ( - + ); diff --git a/pages/public/preserved/[id].tsx b/pages/public/preserved/[id].tsx new file mode 100644 index 0000000..79710bf --- /dev/null +++ b/pages/public/preserved/[id].tsx @@ -0,0 +1,63 @@ +import React, { useEffect, useState } from "react"; +import useLinkStore from "@/store/links"; +import { useRouter } from "next/router"; +import { + ArchivedFormat, + LinkIncludingShortenedCollectionAndTags, +} from "@/types/global"; +import ReadableView from "@/components/ReadableView"; + +export default function Index() { + const { links, getLink } = useLinkStore(); + + const [link, setLink] = useState(); + + const router = useRouter(); + + let isPublic = router.pathname.startsWith("/public") ? true : false; + + useEffect(() => { + const fetchLink = async () => { + if (router.query.id) { + await getLink(Number(router.query.id), isPublic); + } + }; + + fetchLink(); + }, []); + + useEffect(() => { + if (links[0]) setLink(links.find((e) => e.id === Number(router.query.id))); + }, [links]); + + return ( +
+ {/*
+ Readable +
*/} + {link && Number(router.query.format) === ArchivedFormat.readability && ( + + )} + {link && Number(router.query.format) === ArchivedFormat.pdf && ( + + )} + {link && Number(router.query.format) === ArchivedFormat.png && ( + + )} + {link && Number(router.query.format) === ArchivedFormat.jpeg && ( + + )} +
+ ); +} diff --git a/pages/settings/account.tsx b/pages/settings/account.tsx index a4ff709..dcd0a87 100644 --- a/pages/settings/account.tsx +++ b/pages/settings/account.tsx @@ -10,7 +10,6 @@ import SubmitButton from "@/components/SubmitButton"; import React from "react"; import { MigrationFormat, MigrationRequest } from "@/types/global"; import Link from "next/link"; -import ClickAwayHandler from "@/components/ClickAwayHandler"; import Checkbox from "@/components/Checkbox"; export default function Account() { @@ -85,9 +84,9 @@ export default function Account() { setSubmitLoader(false); }; - const [importDropdown, setImportDropdown] = useState(false); - const importBookmarks = async (e: any, format: MigrationFormat) => { + setSubmitLoader(true); + const file: File = e.target.files[0]; if (file) { @@ -112,18 +111,19 @@ export default function Account() { toast.dismiss(load); - toast.success("Imported the Bookmarks! Reloading the page..."); - - setImportDropdown(false); - - setTimeout(() => { - location.reload(); - }, 2000); + if (response.ok) { + toast.success("Imported the Bookmarks! Reloading the page..."); + setTimeout(() => { + location.reload(); + }, 2000); + } else toast.error(data.response as string); }; reader.onerror = function (e) { console.log("Error:", e); }; } + + setSubmitLoader(false); }; const [whitelistedUsersTextbox, setWhiteListedUsersTextbox] = useState(""); diff --git a/types/enviornment.d.ts b/types/enviornment.d.ts index b34d57a..9ba4241 100644 --- a/types/enviornment.d.ts +++ b/types/enviornment.d.ts @@ -10,6 +10,7 @@ declare global { AUTOSCROLL_TIMEOUT?: string; RE_ARCHIVE_LIMIT?: string; NEXT_PUBLIC_MAX_FILE_SIZE?: string; + MAX_LINKS_PER_USER?: string; SPACES_KEY?: string; SPACES_SECRET?: string;