From 0d5579b56d2096bec4b57de63c9c6cf36104add3 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 9 Mar 2023 01:01:24 +0330 Subject: [PATCH] feat!: added PDF and screenshot archive support --- .gitignore | 5 +- components/AddLinkModal.tsx | 24 +++------- components/LinkList.tsx | 5 +- components/Navbar.tsx | 14 +++--- lib/api/archive.ts | 21 ++++++++ .../controllers/collections/postCollection.ts | 9 +++- lib/api/controllers/links/postLink.ts | 48 ++++++++++++++++--- lib/api/getTitle.ts | 9 ++++ lib/api/hasAccessToCollection.ts | 15 ++++++ package.json | 3 ++ pages/api/archives/[...params].ts | 44 +++++++++++++++++ pages/api/auth/register.ts | 6 --- .../migration.sql | 1 - prisma/schema.prisma | 1 - store/links.ts | 4 +- types/global.ts | 2 +- yarn.lock | 22 +++++++++ 17 files changed, 186 insertions(+), 47 deletions(-) create mode 100644 lib/api/archive.ts create mode 100644 lib/api/getTitle.ts create mode 100644 lib/api/hasAccessToCollection.ts create mode 100644 pages/api/archives/[...params].ts rename prisma/migrations/{20230306200620_ => 20230308212222_}/migration.sql (98%) diff --git a/.gitignore b/.gitignore index 814dcf1..bebba3a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - # dependencies /node_modules /.pnp @@ -35,3 +33,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# generated files and folders +/data diff --git a/components/AddLinkModal.tsx b/components/AddLinkModal.tsx index 5e74380..fd4bfcb 100644 --- a/components/AddLinkModal.tsx +++ b/components/AddLinkModal.tsx @@ -7,14 +7,14 @@ import { useRouter } from "next/router"; import { NewLink } from "@/types/global"; import useLinkSlice from "@/store/links"; -export default function () { +export default function ({ toggleLinkModal }: { toggleLinkModal: Function }) { const router = useRouter(); const [newLink, setNewLink] = useState({ name: "", url: "", tags: [], - collectionId: { id: Number(router.query.id) }, + collection: { id: Number(router.query.id) }, }); const { addLink } = useLinkSlice(); @@ -30,23 +30,13 @@ export default function () { const setCollection = (e: any) => { const collection = { id: e?.value, isNew: e?.__isNew__ }; - setNewLink({ ...newLink, collectionId: collection }); + setNewLink({ ...newLink, collection: collection }); }; - const postLink = async () => { - const response = await fetch("/api/routes/links", { - body: JSON.stringify(newLink), - headers: { - "Content-Type": "application/json", - }, - method: "POST", - }); + const submitLink = async () => { + const response = await addLink(newLink); - const data = await response.json(); - - console.log(newLink); - - console.log(data); + if (response) toggleLinkModal(); }; return ( @@ -87,7 +77,7 @@ export default function () {
addLink(newLink)} + onClick={submitLink} > Add Link diff --git a/components/LinkList.tsx b/components/LinkList.tsx index 8afd658..0917971 100644 --- a/components/LinkList.tsx +++ b/components/LinkList.tsx @@ -9,7 +9,7 @@ export default function ({ }) { return (
-
+ {/*

{count + 1}.

{link.name}

@@ -17,7 +17,8 @@ export default function ({ {link.tags.map((e, i) => (

{e.name}

))} -
+
*/} + {JSON.stringify(link)}
); } diff --git a/components/Navbar.tsx b/components/Navbar.tsx index d439ed4..7293b68 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -59,10 +59,10 @@ export default function () { } }, [router, collections, tags]); - const [collectionInput, setCollectionInput] = useState(false); + const [linkModal, setLinkModal] = useState(false); - const toggleCollectionInput = () => { - setCollectionInput(!collectionInput); + const toggleLinkModal = () => { + setLinkModal(!linkModal); }; return ( @@ -76,7 +76,7 @@ export default function () {
- {collectionInput ? ( + {linkModal ? (
- +
) : null} diff --git a/lib/api/archive.ts b/lib/api/archive.ts new file mode 100644 index 0000000..9dd401c --- /dev/null +++ b/lib/api/archive.ts @@ -0,0 +1,21 @@ +import { chromium, devices } from "playwright"; + +export default async (url: string, collectionId: number, linkId: number) => { + const archivePath = `data/archives/${collectionId}/${linkId}`; + + const browser = await chromium.launch(); + const context = await browser.newContext(devices["Desktop Chrome"]); + const page = await context.newPage(); + + // const contexts = browser.contexts(); + // console.log(contexts.length); + + await page.goto(url); + + await page.pdf({ path: archivePath + ".pdf" }); + + await page.screenshot({ fullPage: true, path: archivePath + ".png" }); + + await context.close(); + await browser.close(); +}; diff --git a/lib/api/controllers/collections/postCollection.ts b/lib/api/controllers/collections/postCollection.ts index 058bf32..d938755 100644 --- a/lib/api/controllers/collections/postCollection.ts +++ b/lib/api/controllers/collections/postCollection.ts @@ -1,6 +1,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { prisma } from "@/lib/api/db"; import { Session } from "next-auth"; +import { existsSync, mkdirSync } from "fs"; export default async function ( req: NextApiRequest, @@ -39,7 +40,7 @@ export default async function ( return res.status(400).json({ response: "Collection already exists." }); } - const createCollection = await prisma.collection.create({ + const newCollection = await prisma.collection.create({ data: { owner: { connect: { @@ -50,7 +51,11 @@ export default async function ( }, }); + const collectionPath = `data/archives/${newCollection.id}`; + if (!existsSync(collectionPath)) + mkdirSync(collectionPath, { recursive: true }); + return res.status(200).json({ - response: createCollection, + response: newCollection, }); } diff --git a/lib/api/controllers/links/postLink.ts b/lib/api/controllers/links/postLink.ts index 3cb87be..c758725 100644 --- a/lib/api/controllers/links/postLink.ts +++ b/lib/api/controllers/links/postLink.ts @@ -2,6 +2,11 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { prisma } from "@/lib/api/db"; import { Session } from "next-auth"; import { LinkAndTags, NewLink } from "@/types/global"; +import { existsSync, mkdirSync } from "fs"; +import getTitle from "../../getTitle"; +import archive from "../../archive"; +import { Link } from "@prisma/client"; +import AES from "crypto-js/aes"; export default async function ( req: NextApiRequest, @@ -21,8 +26,8 @@ export default async function ( .json({ response: "Please enter a valid name for the link." }); } - if (link.collectionId.isNew) { - const collectionId = link.collectionId.id as string; + if (link.collection.isNew) { + const collectionId = link.collection.id as string; const findCollection = await prisma.user.findFirst({ where: { @@ -43,7 +48,7 @@ export default async function ( return res.status(400).json({ response: "Collection already exists." }); } - const createCollection = await prisma.collection.create({ + const newCollection = await prisma.collection.create({ data: { owner: { connect: { @@ -54,12 +59,18 @@ export default async function ( }, }); - link.collectionId.id = createCollection.id; + const collectionPath = `data/archives/${newCollection.id}`; + if (!existsSync(collectionPath)) + mkdirSync(collectionPath, { recursive: true }); + + link.collection.id = newCollection.id; } - const collectionId = link.collectionId.id as number; + const collectionId = link.collection.id as number; - const createLink: LinkAndTags = await prisma.link.create({ + const title = await getTitle(link.url); + + const newLink: Link = await prisma.link.create({ data: { name: link.name, url: link.url, @@ -86,11 +97,34 @@ export default async function ( }, })), }, + title, + isFavorites: false, + screenshotPath: "", + pdfPath: "", }, + }); + + const AES_SECRET = process.env.AES_SECRET as string; + + const screenShotHashedPath = AES.encrypt( + `data/archives/${newLink.collectionId}/${newLink.id}.png`, + AES_SECRET + ).toString(); + + const pdfHashedPath = AES.encrypt( + `data/archives/${newLink.collectionId}/${newLink.id}.pdf`, + AES_SECRET + ).toString(); + + const updatedLink: LinkAndTags = await prisma.link.update({ + where: { id: newLink.id }, + data: { screenshotPath: screenShotHashedPath, pdfPath: pdfHashedPath }, include: { tags: true }, }); + archive(updatedLink.url, updatedLink.collectionId, updatedLink.id); + return res.status(200).json({ - response: createLink, + response: updatedLink, }); } diff --git a/lib/api/getTitle.ts b/lib/api/getTitle.ts new file mode 100644 index 0000000..07187ee --- /dev/null +++ b/lib/api/getTitle.ts @@ -0,0 +1,9 @@ +export default async (url: string) => { + const response = await fetch(url); + const text = await response.text(); + + // regular expression to find the tag + let match = text.match(/<title.*>([^<]*)<\/title>/); + if (match) return match[1]; + else return ""; +}; diff --git a/lib/api/hasAccessToCollection.ts b/lib/api/hasAccessToCollection.ts new file mode 100644 index 0000000..d7ad859 --- /dev/null +++ b/lib/api/hasAccessToCollection.ts @@ -0,0 +1,15 @@ +import { prisma } from "@/lib/api/db"; + +export default async (userId: number, collectionId: number) => { + const check: any = await prisma.collection.findFirst({ + where: { + AND: { + id: collectionId, + OR: [{ ownerId: userId }, { members: { some: { userId } } }], + }, + }, + include: { members: true }, + }); + + return check; +}; diff --git a/package.json b/package.json index ed4cb04..dd18e92 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,17 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@next/font": "13.1.6", "@prisma/client": "^4.9.0", + "@types/crypto-js": "^4.1.1", "@types/node": "18.11.18", "@types/react": "18.0.27", "@types/react-dom": "18.0.10", "bcrypt": "^5.1.0", + "crypto-js": "^4.1.1", "eslint": "8.33.0", "eslint-config-next": "13.1.6", "next": "13.1.6", "next-auth": "^4.19.1", + "playwright": "^1.31.2", "react": "18.2.0", "react-dom": "18.2.0", "react-select": "^5.7.0", diff --git a/pages/api/archives/[...params].ts b/pages/api/archives/[...params].ts new file mode 100644 index 0000000..5ec56b3 --- /dev/null +++ b/pages/api/archives/[...params].ts @@ -0,0 +1,44 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "pages/api/auth/[...nextauth]"; +import AES from "crypto-js/aes"; +import enc from "crypto-js/enc-utf8"; +import path from "path"; +import fs from "fs"; +import hasAccessToCollection from "@/lib/api/hasAccessToCollection"; + +export default async function (req: NextApiRequest, res: NextApiResponse) { + if (!req.query.params) + return res.status(401).json({ response: "Invalid parameters." }); + + const collectionId = req.query.params[0]; + + const session = await getServerSession(req, res, authOptions); + + if (!session?.user?.email) + return res.status(401).json({ response: "You must be logged in." }); + + const collectionIsAccessible = await hasAccessToCollection( + session.user.id, + Number(collectionId) + ); + + if (!collectionIsAccessible) + return res + .status(401) + .json({ response: "You don't have access to this collection." }); + + const AES_SECRET = process.env.AES_SECRET as string; + const encryptedPath = decodeURIComponent(req.query.params[1]) as string; + const decryptedPath = AES.decrypt(encryptedPath, AES_SECRET).toString(enc); + + const filePath = path.join(process.cwd(), decryptedPath); + const file = fs.readFileSync(filePath); + + if (filePath.endsWith(".pdf")) + res.setHeader("Content-Type", "application/pdf"); + + if (filePath.endsWith(".png")) res.setHeader("Content-Type", "image/png"); + + return res.status(200).send(file); +} diff --git a/pages/api/auth/register.ts b/pages/api/auth/register.ts index 1ef1f44..d0d2006 100644 --- a/pages/api/auth/register.ts +++ b/pages/api/auth/register.ts @@ -37,17 +37,11 @@ export default async function ( name: body.name, email: body.email, password: hashedPassword, - collections: { - create: { - name: "First Collection", - }, - }, }, }); res.status(201).json({ message: "User successfully created." }); } else if (checkIfUserExists) { - console.log(checkIfUserExists); res.status(400).json({ message: "User already exists." }); } } diff --git a/prisma/migrations/20230306200620_/migration.sql b/prisma/migrations/20230308212222_/migration.sql similarity index 98% rename from prisma/migrations/20230306200620_/migration.sql rename to prisma/migrations/20230308212222_/migration.sql index 801d317..0158c62 100644 --- a/prisma/migrations/20230306200620_/migration.sql +++ b/prisma/migrations/20230308212222_/migration.sql @@ -24,7 +24,6 @@ CREATE TABLE "UsersAndCollections" ( "userId" INTEGER NOT NULL, "collectionId" INTEGER NOT NULL, "canCreate" BOOLEAN NOT NULL, - "canRead" BOOLEAN NOT NULL, "canUpdate" BOOLEAN NOT NULL, "canDelete" BOOLEAN NOT NULL, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 09c6e0a..872617b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -36,7 +36,6 @@ model UsersAndCollections { collectionId Int canCreate Boolean - canRead Boolean canUpdate Boolean canDelete Boolean diff --git a/store/links.ts b/store/links.ts index 63e2df8..bcdaf8a 100644 --- a/store/links.ts +++ b/store/links.ts @@ -4,7 +4,7 @@ import { LinkAndTags, NewLink } from "@/types/global"; type LinkSlice = { links: LinkAndTags[]; setLinks: () => void; - addLink: (linkName: NewLink) => void; + addLink: (linkName: NewLink) => Promise<boolean>; updateLink: (link: LinkAndTags) => void; removeLink: (linkId: number) => void; }; @@ -33,6 +33,8 @@ const useLinkSlice = create<LinkSlice>()((set) => ({ set((state) => ({ links: [...state.links, data.response], })); + + return response.ok; }, updateLink: (link) => set((state) => ({ diff --git a/types/global.ts b/types/global.ts index c8d0a23..e9a8de5 100644 --- a/types/global.ts +++ b/types/global.ts @@ -8,7 +8,7 @@ export interface NewLink { name: string; url: string; tags: string[]; - collectionId: { + collection: { id: string | number; isNew?: boolean; }; diff --git a/yarn.lock b/yarn.lock index f9d8433..0aeaf73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -396,6 +396,11 @@ dependencies: "@types/node" "*" +"@types/crypto-js@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.1.1.tgz#602859584cecc91894eb23a4892f38cfa927890d" + integrity sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -868,6 +873,11 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +crypto-js@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf" + integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw== + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -2373,6 +2383,18 @@ pify@^2.3.0: resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== +playwright-core@1.31.2: + version "1.31.2" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.31.2.tgz#debf4b215d14cb619adb7e511c164d068075b2ed" + integrity sha512-a1dFgCNQw4vCsG7bnojZjDnPewZcw7tZUNFN0ZkcLYKj+mPmXvg4MpaaKZ5SgqPsOmqIf2YsVRkgqiRDxD+fDQ== + +playwright@^1.31.2: + version "1.31.2" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.31.2.tgz#4252280586c596746122cd1fdf9f8ff6a63fa852" + integrity sha512-jpC47n2PKQNtzB7clmBuWh6ftBRS/Bt5EGLigJ9k2QAKcNeYXZkEaDH5gmvb6+AbcE0DO6GnXdbl9ogG6Eh+og== + dependencies: + playwright-core "1.31.2" + postcss-import@^14.1.0: version "14.1.0" resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0"