feat!: added PDF and screenshot archive support
This commit is contained in:
parent
bd3b2f50f2
commit
0d5579b56d
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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();
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 "";
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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." });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
@ -36,7 +36,6 @@ model UsersAndCollections {
|
|||
collectionId Int
|
||||
|
||||
canCreate Boolean
|
||||
canRead Boolean
|
||||
canUpdate Boolean
|
||||
canDelete Boolean
|
||||
|
||||
|
|
|
@ -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) => ({
|
||||
|
|
|
@ -8,7 +8,7 @@ export interface NewLink {
|
|||
name: string;
|
||||
url: string;
|
||||
tags: string[];
|
||||
collectionId: {
|
||||
collection: {
|
||||
id: string | number;
|
||||
isNew?: boolean;
|
||||
};
|
||||
|
|
22
yarn.lock
22
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"
|
||||
|
|
Ŝarĝante…
Reference in New Issue