feat!: added PDF and screenshot archive support

This commit is contained in:
Daniel 2023-03-09 01:01:24 +03:30
parent bd3b2f50f2
commit 0d5579b56d
17 changed files with 186 additions and 47 deletions

5
.gitignore vendored
View File

@ -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

View File

@ -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<NewLink>({
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 () {
<div
className="mx-auto mt-2 bg-sky-500 text-white flex items-center gap-2 py-2 px-5 rounded select-none font-bold cursor-pointer duration-100 hover:bg-sky-400"
onClick={() => addLink(newLink)}
onClick={submitLink}
>
<FontAwesomeIcon icon={faPlus} className="h-5" />
Add Link

View File

@ -9,7 +9,7 @@ export default function ({
}) {
return (
<div className="border border-sky-100 mb-5 bg-gray-100 p-5 rounded">
<div className="flex items-baseline gap-1">
{/* <div className="flex items-baseline gap-1">
<p className="text-sm text-sky-600">{count + 1}.</p>
<p className="text-lg text-sky-500">{link.name}</p>
</div>
@ -17,7 +17,8 @@ export default function ({
{link.tags.map((e, i) => (
<p key={i}>{e.name}</p>
))}
</div>
</div> */}
{JSON.stringify(link)}
</div>
);
}

View File

@ -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 () {
<div className="flex items-center gap-3">
<FontAwesomeIcon
icon={faPlus}
onClick={toggleCollectionInput}
onClick={toggleLinkModal}
className="select-none cursor-pointer w-5 h-5 text-white bg-sky-500 p-2 rounded hover:bg-sky-400 duration-100"
/>
<FontAwesomeIcon
@ -90,13 +90,13 @@ export default function () {
Sign Out
</div>
{collectionInput ? (
{linkModal ? (
<div className="fixed top-0 bottom-0 right-0 left-0 bg-gray-500 bg-opacity-10 flex items-center fade-in">
<ClickAwayHandler
onClickOutside={toggleCollectionInput}
onClickOutside={toggleLinkModal}
className="w-fit mx-auto"
>
<AddLinkModal />
<AddLinkModal toggleLinkModal={toggleLinkModal} />
</ClickAwayHandler>
</div>
) : null}

21
lib/api/archive.ts Normal file
View File

@ -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();
};

View File

@ -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,
});
}

View File

@ -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,
});
}

9
lib/api/getTitle.ts Normal file
View File

@ -0,0 +1,9 @@
export default async (url: string) => {
const response = await fetch(url);
const text = await response.text();
// regular expression to find the <title> tag
let match = text.match(/<title.*>([^<]*)<\/title>/);
if (match) return match[1];
else return "";
};

View File

@ -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;
};

View File

@ -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",

View File

@ -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);
}

View File

@ -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." });
}
}

View File

@ -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,

View File

@ -36,7 +36,6 @@ model UsersAndCollections {
collectionId Int
canCreate Boolean
canRead Boolean
canUpdate Boolean
canDelete Boolean

View File

@ -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) => ({

View File

@ -8,7 +8,7 @@ export interface NewLink {
name: string;
url: string;
tags: string[];
collectionId: {
collection: {
id: string | number;
isNew?: boolean;
};

View File

@ -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"