diff --git a/components/DashboardItem.tsx b/components/DashboardItem.tsx index d891398..3de882e 100644 --- a/components/DashboardItem.tsx +++ b/components/DashboardItem.tsx @@ -20,7 +20,7 @@ export default function dashboardItem({ name, value, icon }: Props) {

{name}

-

+

{value}

diff --git a/components/FilterSearchDropdown.tsx b/components/FilterSearchDropdown.tsx index 0b32bee..90648e0 100644 --- a/components/FilterSearchDropdown.tsx +++ b/components/FilterSearchDropdown.tsx @@ -56,7 +56,7 @@ export default function FilterSearchDropdown({ } /> setSearchFilter({ diff --git a/components/LinkSidebar.tsx b/components/LinkSidebar.tsx index 6ffc2f5..63ddb0a 100644 --- a/components/LinkSidebar.tsx +++ b/components/LinkSidebar.tsx @@ -20,7 +20,7 @@ type Props = { onClick?: Function; }; -export default function SettingsSidebar({ className, onClick }: Props) { +export default function LinkSidebar({ className, onClick }: Props) { const session = useSession(); const userId = session.data?.user.id; diff --git a/components/Modal/Link/PreservedFormats.tsx b/components/Modal/Link/PreservedFormats.tsx index 1a6712b..0cd21c1 100644 --- a/components/Modal/Link/PreservedFormats.tsx +++ b/components/Modal/Link/PreservedFormats.tsx @@ -27,7 +27,14 @@ export default function PreservedFormats() { useEffect(() => { let interval: NodeJS.Timer | undefined; if (link?.screenshotPath === "pending" || link?.pdfPath === "pending") { - interval = setInterval(() => getLink(link.id as number), 5000); + let isPublicRoute = router.pathname.startsWith("/public") + ? true + : undefined; + + interval = setInterval( + () => getLink(link.id as number, isPublicRoute), + 5000 + ); } else { if (interval) { clearInterval(interval); diff --git a/components/ProfilePhoto.tsx b/components/ProfilePhoto.tsx index 8575d6d..993eb40 100644 --- a/components/ProfilePhoto.tsx +++ b/components/ProfilePhoto.tsx @@ -38,6 +38,7 @@ export default function ProfilePhoto({ src, className, priority }: Props) { width={112} priority={priority} draggable={false} + onError={() => setImage("")} className={`h-10 w-10 bg-sky-600 dark:bg-sky-600 shadow rounded-full aspect-square border border-slate-200 dark:border-neutral-700 ${ className || "" }`} diff --git a/components/PublicPage/PublicLinkCard.tsx b/components/PublicPage/PublicLinkCard.tsx index 21a24d2..3d6b01d 100644 --- a/components/PublicPage/PublicLinkCard.tsx +++ b/components/PublicPage/PublicLinkCard.tsx @@ -5,6 +5,7 @@ import { Link as LinkType, Tag } from "@prisma/client"; import isValidUrl from "@/lib/client/isValidUrl"; import unescapeString from "@/lib/client/unescapeString"; import { TagIncludingLinkCount } from "@/types/global"; +import Link from "next/link"; interface LinksIncludingTags extends LinkType { tags: TagIncludingLinkCount[]; @@ -27,75 +28,69 @@ export default function LinkCard({ link, count }: Props) { }); return ( - -
- {url && ( - <> - { - const target = e.target as HTMLElement; - target.style.display = "none"; - }} - /> - { - const target = e.target as HTMLElement; - target.style.display = "none"; - }} - /> - - )} -
-
-
-

{count + 1}

-

- {unescapeString(link.name || link.description)} -

-
- -

- {unescapeString(link.description)} +

+
+ ); } diff --git a/components/PublicPage/PublicSearchBar.tsx b/components/PublicPage/PublicSearchBar.tsx index b890e83..525daa3 100644 --- a/components/PublicPage/PublicSearchBar.tsx +++ b/components/PublicPage/PublicSearchBar.tsx @@ -11,11 +11,13 @@ type Props = { export default function PublicSearchBar({ placeHolder }: Props) { const router = useRouter(); - const [searchQuery, setSearchQuery] = useState(""); + const [searchQuery, setSearchQuery] = useState(""); useEffect(() => { - console.log(router); - }); + router.query.q + ? setSearchQuery(decodeURIComponent(router.query.q as string)) + : setSearchQuery(""); + }, [router.query.q]); return (
@@ -36,16 +38,21 @@ export default function PublicSearchBar({ placeHolder }: Props) { toast.error("The search query should not contain '%'."); setSearchQuery(e.target.value.replace("%", "")); }} - onKeyDown={(e) => - e.key === "Enter" && - router.push( - "/public/collections/" + - router.query.id + - "?q=" + - encodeURIComponent(searchQuery) - ) - } - className="border text-sm border-sky-100 bg-white dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600 rounded-md pl-7 py-1 pr-1 w-44 sm:w-60 dark:hover:border-neutral-600 md:focus:w-80 hover:border-sky-300 duration-100 outline-none dark:bg-neutral-800" + onKeyDown={(e) => { + if (e.key === "Enter") { + if (!searchQuery) { + return router.push("/public/collections/" + router.query.id); + } + + return router.push( + "/public/collections/" + + router.query.id + + "?q=" + + encodeURIComponent(searchQuery || "") + ); + } + }} + className="border text-sm border-sky-100 bg-gray-50 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600 rounded-md pl-8 py-2 pr-2 w-44 sm:w-60 dark:hover:border-neutral-600 md:focus:w-80 hover:border-sky-300 duration-100 outline-none dark:bg-neutral-800" />
); diff --git a/layouts/LinkLayout.tsx b/layouts/LinkLayout.tsx index b2c135e..398acda 100644 --- a/layouts/LinkLayout.tsx +++ b/layouts/LinkLayout.tsx @@ -5,8 +5,7 @@ import useModalStore from "@/store/modals"; import { useRouter } from "next/router"; import ClickAwayHandler from "@/components/ClickAwayHandler"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faBars, faChevronLeft } from "@fortawesome/free-solid-svg-icons"; -import Link from "next/link"; +import { faChevronLeft } from "@fortawesome/free-solid-svg-icons"; import useWindowDimensions from "@/hooks/useWindowDimensions"; import { faPen, @@ -66,22 +65,22 @@ export default function LinkLayout({ children }: Props) { const [link, setLink] = useState(); useEffect(() => { - if (links) setLink(links.find((e) => e.id === Number(router.query.id))); + if (links[0]) setLink(links.find((e) => e.id === Number(router.query.id))); }, [links]); useEffect(() => { if (link) setLinkCollection(collections.find((e) => e.id === link?.collection.id)); - }, [link]); + }, [link, collections]); return ( <>
-
+ {/*
-
+
*/}
@@ -93,84 +92,95 @@ export default function LinkLayout({ children }: Props) {
*/}
router.push(`/collections/${linkCollection?.id}`)} + onClick={() => { + if (router.pathname.startsWith("/public")) { + router.push( + `/public/collections/${ + linkCollection?.id || link?.collection.id + }` + ); + } else { + router.push(`/collections/${linkCollection?.id}`); + } + }} className="inline-flex gap-1 hover:opacity-60 items-center select-none cursor-pointer p-2 lg:p-0 lg:px-1 lg:my-2 text-gray-500 dark:text-gray-300 rounded-md duration-100" > Back{" "} - to {linkCollection?.name} + to{" "} + + {linkCollection?.name || link?.collection?.name} +
-
-
- {link?.collection.ownerId === userId || - linkCollection?.members.some( - (e) => e.userId === userId && e.canUpdate - ) ? ( -
{ - link - ? setModal({ - modal: "LINK", - state: true, - active: link, - method: "UPDATE", - }) - : undefined; - }} - className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`} - > - -
- ) : undefined} - +
+ {link?.collection?.ownerId === userId || + linkCollection?.members.some( + (e) => e.userId === userId && e.canUpdate + ) ? (
{ link ? setModal({ modal: "LINK", state: true, active: link, - method: "FORMATS", + method: "UPDATE", }) : undefined; }} - title="Preserved Formats" className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`} >
+ ) : undefined} - {link?.collection.ownerId === userId || - linkCollection?.members.some( - (e) => e.userId === userId && e.canDelete - ) ? ( -
{ - if (link?.id) { - removeLink(link.id); - router.back(); - } - }} - title="Delete" - className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`} - > - -
- ) : undefined} +
{ + link + ? setModal({ + modal: "LINK", + state: true, + active: link, + method: "FORMATS", + }) + : undefined; + }} + title="Preserved Formats" + className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`} + > +
+ + {link?.collection?.ownerId === userId || + linkCollection?.members.some( + (e) => e.userId === userId && e.canDelete + ) ? ( +
{ + if (link?.id) { + removeLink(link.id); + router.back(); + } + }} + title="Delete" + className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`} + > + +
+ ) : undefined}
diff --git a/lib/api/controllers/links/linkId/getLinkById.ts b/lib/api/controllers/links/linkId/getLinkById.ts index 83a3ceb..bf872c2 100644 --- a/lib/api/controllers/links/linkId/getLinkById.ts +++ b/lib/api/controllers/links/linkId/getLinkById.ts @@ -1,5 +1,5 @@ import { prisma } from "@/lib/api/db"; -import { Collection, Link, UsersAndCollections } from "@prisma/client"; +import { Collection, UsersAndCollections } from "@prisma/client"; import getPermission from "@/lib/api/getPermission"; export default async function getLinkById(userId: number, linkId: number) { @@ -27,7 +27,7 @@ export default async function getLinkById(userId: number, linkId: number) { status: 401, }; else { - const updatedLink = await prisma.link.findUnique({ + const link = await prisma.link.findUnique({ where: { id: linkId, }, @@ -43,6 +43,6 @@ export default async function getLinkById(userId: number, linkId: number) { }, }); - return { response: updatedLink, status: 200 }; + return { response: link, status: 200 }; } } diff --git a/lib/api/controllers/public/links/linkId/getLinkById.ts b/lib/api/controllers/public/links/linkId/getLinkById.ts new file mode 100644 index 0000000..2e1d87d --- /dev/null +++ b/lib/api/controllers/public/links/linkId/getLinkById.ts @@ -0,0 +1,24 @@ +import { prisma } from "@/lib/api/db"; + +export default async function getLinkById(linkId: number) { + if (!linkId) + return { + response: "Please choose a valid link.", + status: 401, + }; + + const link = await prisma.link.findFirst({ + where: { + id: linkId, + collection: { + isPublic: true, + }, + }, + include: { + tags: true, + collection: true, + }, + }); + + return { response: link, status: 200 }; +} diff --git a/lib/api/getPermission.ts b/lib/api/getPermission.ts index 3d1b2bc..cefd4e1 100644 --- a/lib/api/getPermission.ts +++ b/lib/api/getPermission.ts @@ -1,24 +1,35 @@ import { prisma } from "@/lib/api/db"; type Props = { - userId: number; + userId?: number; collectionId?: number; linkId?: number; + isPublic?: boolean; }; export default async function getPermission({ userId, collectionId, linkId, + isPublic, }: Props) { if (linkId) { const check = await prisma.collection.findFirst({ where: { - links: { - some: { - id: linkId, + [isPublic ? "OR" : "AND"]: [ + { + id: collectionId, + OR: [{ ownerId: userId }, { members: { some: { userId } } }], + links: { + some: { + id: linkId, + }, + }, }, - }, + { + isPublic: isPublic ? true : undefined, + }, + ], }, include: { members: true }, }); @@ -27,10 +38,15 @@ export default async function getPermission({ } else if (collectionId) { const check = await prisma.collection.findFirst({ where: { - AND: { - id: collectionId, - OR: [{ ownerId: userId }, { members: { some: { userId } } }], - }, + [isPublic ? "OR" : "AND"]: [ + { + id: collectionId, + OR: [{ ownerId: userId }, { members: { some: { userId } } }], + }, + { + isPublic: isPublic ? true : undefined, + }, + ], }, include: { members: true }, }); diff --git a/pages/api/v1/archives/[...params].ts b/pages/api/v1/archives/[...params].ts index 1436c04..065ba2f 100644 --- a/pages/api/v1/archives/[...params].ts +++ b/pages/api/v1/archives/[...params].ts @@ -1,20 +1,20 @@ import type { NextApiRequest, NextApiResponse } from "next"; import getPermission from "@/lib/api/getPermission"; import readFile from "@/lib/api/storage/readFile"; -import verifyUser from "@/lib/api/verifyUser"; +import { getToken } from "next-auth/jwt"; export default async function Index(req: NextApiRequest, res: NextApiResponse) { if (!req.query.params) return res.status(401).json({ response: "Invalid parameters." }); - const user = await verifyUser({ req, res }); - if (!user) return; + const token = await getToken({ req }); + const userId = token?.id; const collectionId = req.query.params[0]; const linkId = req.query.params[1]; const collectionIsAccessible = await getPermission({ - userId: user.id, + userId, collectionId: Number(collectionId), }); diff --git a/pages/api/v1/avatar/[id].ts b/pages/api/v1/avatar/[id].ts index f20b9b6..3399cc3 100644 --- a/pages/api/v1/avatar/[id].ts +++ b/pages/api/v1/avatar/[id].ts @@ -2,26 +2,43 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { prisma } from "@/lib/api/db"; import readFile from "@/lib/api/storage/readFile"; import verifyUser from "@/lib/api/verifyUser"; +import { getToken } from "next-auth/jwt"; export default async function Index(req: NextApiRequest, res: NextApiResponse) { const queryId = Number(req.query.id); - const user = await verifyUser({ req, res }); - if (!user) return; - if (!queryId) return res .setHeader("Content-Type", "text/plain") .status(401) .send("Invalid parameters."); - if (user.id !== queryId) { - const targetUser = await prisma.user.findUnique({ + const token = await getToken({ req }); + const userId = token?.id; + + const targetUser = await prisma.user.findUnique({ + where: { + id: queryId, + }, + include: { + whitelistedUsers: true, + }, + }); + + if (targetUser?.isPrivate) { + if (!userId) { + return res + .setHeader("Content-Type", "text/plain") + .status(400) + .send("File inaccessible."); + } + + const user = await prisma.user.findUnique({ where: { - id: queryId, + id: userId, }, include: { - whitelistedUsers: true, + subscriptions: true, }, }); @@ -29,15 +46,18 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) { (whitelistedUsername) => whitelistedUsername.username ); - if ( - targetUser?.isPrivate && - user.username && - !whitelistedUsernames?.includes(user.username) - ) { + if (!user?.username) { return res .setHeader("Content-Type", "text/plain") .status(400) - .send("File not found."); + .send("File inaccessible."); + } + + if (user.username && !whitelistedUsernames?.includes(user.username)) { + return res + .setHeader("Content-Type", "text/plain") + .status(400) + .send("File inaccessible."); } } diff --git a/pages/api/v1/public/collections/[id].ts b/pages/api/v1/public/collections/[id].ts index c9b2fde..04178f8 100644 --- a/pages/api/v1/public/collections/[id].ts +++ b/pages/api/v1/public/collections/[id].ts @@ -1,7 +1,7 @@ import getPublicCollection from "@/lib/api/controllers/public/collections/getPublicCollection"; import type { NextApiRequest, NextApiResponse } from "next"; -export default async function collections( +export default async function collection( req: NextApiRequest, res: NextApiResponse ) { diff --git a/pages/api/v1/public/collections/links/[id].ts b/pages/api/v1/public/collections/links/[id].ts deleted file mode 100644 index e69de29..0000000 diff --git a/pages/api/v1/public/links/[id].ts b/pages/api/v1/public/links/[id].ts new file mode 100644 index 0000000..b3e854d --- /dev/null +++ b/pages/api/v1/public/links/[id].ts @@ -0,0 +1,13 @@ +import getLinkById from "@/lib/api/controllers/public/links/linkId/getLinkById"; +import type { NextApiRequest, NextApiResponse } from "next"; + +export default async function link(req: NextApiRequest, res: NextApiResponse) { + if (!req?.query?.id) { + return res.status(401).json({ response: "Please choose a valid link." }); + } + + if (req.method === "GET") { + const link = await getLinkById(Number(req?.query?.id)); + return res.status(link.status).json({ response: link.response }); + } +} diff --git a/pages/links/[id].tsx b/pages/links/[id].tsx index b05d0fb..6f18235 100644 --- a/pages/links/[id].tsx +++ b/pages/links/[id].tsx @@ -146,7 +146,7 @@ export default function Index() { >