critical bug fixed + improvements

This commit is contained in:
Daniel 2023-06-25 01:24:35 +03:30
parent fa71d9ba86
commit 0ddd9079bf
13 changed files with 167 additions and 108 deletions

View File

@ -1,7 +1,7 @@
import useCollectionStore from "@/store/collections"; import useCollectionStore from "@/store/collections";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import CreatableSelect from "react-select/creatable"; import Select from "react-select";
import { styles } from "./styles"; import { styles } from "./styles";
import { Options } from "./types"; import { Options } from "./types";
@ -43,7 +43,7 @@ export default function CollectionSelection({ onChange, defaultValue }: Props) {
}, [collections]); }, [collections]);
return ( return (
<CreatableSelect <Select
isClearable isClearable
onChange={onChange} onChange={onChange}
options={options} options={options}

View File

@ -62,7 +62,9 @@ export default function LinkCard({ link, count, className }: Props) {
<div <div
className={`bg-gradient-to-tr from-slate-200 from-10% to-gray-50 via-20% shadow hover:shadow-none cursor-pointer duration-100 rounded-2xl relative group ${className}`} className={`bg-gradient-to-tr from-slate-200 from-10% to-gray-50 via-20% shadow hover:shadow-none cursor-pointer duration-100 rounded-2xl relative group ${className}`}
> >
{permissions && ( {(permissions === true ||
permissions?.canUpdate ||
permissions?.canDelete) && (
<div <div
onClick={() => setExpandDropdown(!expandDropdown)} onClick={() => setExpandDropdown(!expandDropdown)}
id={"expand-dropdown" + link.id} id={"expand-dropdown" + link.id}
@ -83,7 +85,8 @@ export default function LinkCard({ link, count, className }: Props) {
modal: "LINK", modal: "LINK",
state: true, state: true,
method: "UPDATE", method: "UPDATE",
isOwner: permissions === true, isOwnerOrMod:
permissions === true || (permissions?.canUpdate as boolean),
active: link, active: link,
}); });
}} }}
@ -106,7 +109,7 @@ export default function LinkCard({ link, count, className }: Props) {
<div className="flex flex-col justify-between w-full"> <div className="flex flex-col justify-between w-full">
<div className="flex items-baseline gap-1"> <div className="flex items-baseline gap-1">
<p className="text-sm text-sky-400 font-bold">{count + 1}.</p> <p className="text-sm text-sky-400 font-bold">{count + 1}.</p>
<p className="text-lg text-sky-500 font-bold truncate max-w-[10rem]"> <p className="text-lg text-sky-500 font-bold truncate max-w-[10rem] capitalize">
{link.name} {link.name}
</p> </p>
</div> </div>
@ -117,7 +120,7 @@ export default function LinkCard({ link, count, className }: Props) {
className="w-4 h-4 mt-1 drop-shadow" className="w-4 h-4 mt-1 drop-shadow"
style={{ color: collection?.color }} style={{ color: collection?.color }}
/> />
<p className="text-sky-900 truncate max-w-[10rem]"> <p className="text-sky-900 truncate max-w-[10rem] capitalize">
{collection?.name} {collection?.name}
</p> </p>
</div> </div>
@ -132,7 +135,7 @@ export default function LinkCard({ link, count, className }: Props) {
{expandDropdown ? ( {expandDropdown ? (
<Dropdown <Dropdown
items={[ items={[
permissions === true || permissions?.canUpdate permissions === true
? { ? {
name: name:
link?.pinnedBy && link.pinnedBy[0] link?.pinnedBy && link.pinnedBy[0]
@ -158,7 +161,8 @@ export default function LinkCard({ link, count, className }: Props) {
modal: "LINK", modal: "LINK",
state: true, state: true,
method: "UPDATE", method: "UPDATE",
isOwner: permissions === true, isOwnerOrMod:
permissions === true || permissions?.canUpdate,
active: link, active: link,
defaultIndex: 1, defaultIndex: 1,
}); });

View File

@ -97,7 +97,7 @@ export default function DeleteCollection({
)} )}
<div <div
className={`mx-auto mt-2 text-white flex items-center gap-2 py-2 px-5 rounded-md select-none font-bold duration-100 ${ className={`mx-auto mt-2 text-white flex items-center gap-2 py-2 px-5 rounded-md select-none font-bold duration-100 ${
permissions === true permissions === true
? inputField === collection.name ? inputField === collection.name
? "bg-red-500 hover:bg-red-400 cursor-pointer" ? "bg-red-500 hover:bg-red-400 cursor-pointer"

View File

@ -7,7 +7,7 @@ type Props =
| { | {
toggleLinkModal: Function; toggleLinkModal: Function;
method: "CREATE"; method: "CREATE";
isOwner?: boolean; isOwnerOrMod?: boolean;
activeLink?: LinkIncludingShortenedCollectionAndTags; activeLink?: LinkIncludingShortenedCollectionAndTags;
defaultIndex?: number; defaultIndex?: number;
className?: string; className?: string;
@ -15,17 +15,17 @@ type Props =
| { | {
toggleLinkModal: Function; toggleLinkModal: Function;
method: "UPDATE"; method: "UPDATE";
isOwner: boolean; isOwnerOrMod: boolean;
activeLink: LinkIncludingShortenedCollectionAndTags; activeLink: LinkIncludingShortenedCollectionAndTags;
defaultIndex?: number; defaultIndex?: number;
className?: string; className?: string;
}; };
export default function CollectionModal({ export default function LinkModal({
className, className,
defaultIndex, defaultIndex,
toggleLinkModal, toggleLinkModal,
isOwner, isOwnerOrMod,
activeLink, activeLink,
method, method,
}: Props) { }: Props) {
@ -37,10 +37,10 @@ export default function CollectionModal({
)} )}
<Tab.List <Tab.List
className={`flex justify-center flex-col max-w-[15rem] sm:max-w-[30rem] mx-auto sm:flex-row gap-2 sm:gap-3 mb-5 text-sky-600 ${ className={`flex justify-center flex-col max-w-[15rem] sm:max-w-[30rem] mx-auto sm:flex-row gap-2 sm:gap-3 mb-5 text-sky-600 ${
isOwner ? "" : "pb-8" isOwnerOrMod ? "" : "pb-8"
}`} }`}
> >
{method === "UPDATE" && isOwner && ( {method === "UPDATE" && isOwnerOrMod && (
<> <>
<Tab <Tab
className={({ selected }) => className={({ selected }) =>

View File

@ -29,7 +29,7 @@ export default function ModalManagement() {
<LinkModal <LinkModal
toggleLinkModal={toggleModal} toggleLinkModal={toggleModal}
method={modal.method} method={modal.method}
isOwner={modal.isOwner as boolean} isOwnerOrMod={modal.isOwnerOrMod as boolean}
defaultIndex={modal.defaultIndex} defaultIndex={modal.defaultIndex}
activeLink={modal.active as LinkIncludingShortenedCollectionAndTags} activeLink={modal.active as LinkIncludingShortenedCollectionAndTags}
/> />

View File

@ -1,6 +1,6 @@
import { prisma } from "@/lib/api/db"; import { prisma } from "@/lib/api/db";
import getPermission from "@/lib/api/getPermission"; import getPermission from "@/lib/api/getPermission";
import { UsersAndCollections } from "@prisma/client"; import { Collection, UsersAndCollections } from "@prisma/client";
import fs from "fs"; import fs from "fs";
export default async function deleteCollection( export default async function deleteCollection(
@ -12,7 +12,11 @@ export default async function deleteCollection(
if (!collectionId) if (!collectionId)
return { response: "Please choose a valid collection.", status: 401 }; return { response: "Please choose a valid collection.", status: 401 };
const collectionIsAccessible = await getPermission(userId, collectionId); const collectionIsAccessible = (await getPermission(userId, collectionId)) as
| (Collection & {
members: UsersAndCollections[];
})
| null;
const memberHasAccess = collectionIsAccessible?.members.some( const memberHasAccess = collectionIsAccessible?.members.some(
(e: UsersAndCollections) => e.userId === userId (e: UsersAndCollections) => e.userId === userId

View File

@ -1,6 +1,7 @@
import { prisma } from "@/lib/api/db"; import { prisma } from "@/lib/api/db";
import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
import getPermission from "@/lib/api/getPermission"; import getPermission from "@/lib/api/getPermission";
import { Collection, UsersAndCollections } from "@prisma/client";
export default async function updateCollection( export default async function updateCollection(
collection: CollectionIncludingMembersAndLinkCount, collection: CollectionIncludingMembersAndLinkCount,
@ -9,7 +10,14 @@ export default async function updateCollection(
if (!collection.id) if (!collection.id)
return { response: "Please choose a valid collection.", status: 401 }; return { response: "Please choose a valid collection.", status: 401 };
const collectionIsAccessible = await getPermission(userId, collection.id); const collectionIsAccessible = (await getPermission(
userId,
collection.id
)) as
| (Collection & {
members: UsersAndCollections[];
})
| null;
if (!(collectionIsAccessible?.ownerId === userId)) if (!(collectionIsAccessible?.ownerId === userId))
return { response: "Collection is not accessible.", status: 401 }; return { response: "Collection is not accessible.", status: 401 };

View File

@ -1,7 +1,7 @@
import { prisma } from "@/lib/api/db"; import { prisma } from "@/lib/api/db";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import fs from "fs"; import fs from "fs";
import { Link, UsersAndCollections } from "@prisma/client"; import { Collection, Link, UsersAndCollections } from "@prisma/client";
import getPermission from "@/lib/api/getPermission"; import getPermission from "@/lib/api/getPermission";
export default async function deleteLink( export default async function deleteLink(
@ -11,7 +11,14 @@ export default async function deleteLink(
if (!link || !link.collectionId) if (!link || !link.collectionId)
return { response: "Please choose a valid link.", status: 401 }; return { response: "Please choose a valid link.", status: 401 };
const collectionIsAccessible = await getPermission(userId, link.collectionId); const collectionIsAccessible = (await getPermission(
userId,
link.collectionId
)) as
| (Collection & {
members: UsersAndCollections[];
})
| null;
const memberHasAccess = collectionIsAccessible?.members.some( const memberHasAccess = collectionIsAccessible?.members.some(
(e: UsersAndCollections) => e.userId === userId && e.canDelete (e: UsersAndCollections) => e.userId === userId && e.canDelete

View File

@ -2,7 +2,7 @@ import { prisma } from "@/lib/api/db";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import getTitle from "../../getTitle"; import getTitle from "../../getTitle";
import archive from "../../archive"; import archive from "../../archive";
import { Link, UsersAndCollections } from "@prisma/client"; import { Collection, Link, UsersAndCollections } from "@prisma/client";
import getPermission from "@/lib/api/getPermission"; import getPermission from "@/lib/api/getPermission";
import { existsSync, mkdirSync } from "fs"; import { existsSync, mkdirSync } from "fs";
@ -13,16 +13,20 @@ export default async function postLink(
link.collection.name = link.collection.name.trim(); link.collection.name = link.collection.name.trim();
if (!link.name) { if (!link.name) {
return { response: "Please enter a valid name for the link.", status: 401 }; return { response: "Please enter a valid name for the link.", status: 400 };
} else if (!link.collection.name) { } else if (!link.collection.name) {
return { response: "Please enter a valid collection name.", status: 401 }; return { response: "Please enter a valid collection.", status: 400 };
} }
if (link.collection.id) { if (link.collection.id) {
const collectionIsAccessible = await getPermission( const collectionIsAccessible = (await getPermission(
userId, userId,
link.collection.id link.collection.id
); )) as
| (Collection & {
members: UsersAndCollections[];
})
| null;
const memberHasAccess = collectionIsAccessible?.members.some( const memberHasAccess = collectionIsAccessible?.members.some(
(e: UsersAndCollections) => e.userId === userId && e.canCreate (e: UsersAndCollections) => e.userId === userId && e.canCreate

View File

@ -1,84 +1,103 @@
import { prisma } from "@/lib/api/db"; import { prisma } from "@/lib/api/db";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import { UsersAndCollections } from "@prisma/client"; import { Collection, Link, UsersAndCollections } from "@prisma/client";
import getPermission from "@/lib/api/getPermission"; import getPermission from "@/lib/api/getPermission";
export default async function updateLink( export default async function updateLink(
link: LinkIncludingShortenedCollectionAndTags, link: LinkIncludingShortenedCollectionAndTags,
userId: number userId: number
) { ) {
if (!link) return { response: "Please choose a valid link.", status: 401 }; if (!link || !link.collection.id)
return {
response: "Please choose a valid link and collection.",
status: 401,
};
if (link.collection.id) { const targetLink = (await getPermission(
const collectionIsAccessible = await getPermission( userId,
userId, link.collection.id,
link.collection.id link.id
); )) as
| (Link & {
collection: Collection & {
members: UsersAndCollections[];
};
})
| null;
const memberHasAccess = collectionIsAccessible?.members.some( const memberHasAccess = targetLink?.collection.members.some(
(e: UsersAndCollections) => e.userId === userId && e.canCreate (e: UsersAndCollections) => e.userId === userId && e.canUpdate
); );
if (!(collectionIsAccessible?.ownerId === userId || memberHasAccess)) const isCollectionOwner =
return { response: "Collection is not accessible.", status: 401 }; targetLink?.collection.ownerId === link.collection.ownerId &&
} else { link.collection.ownerId === userId &&
link.collection.ownerId = userId; targetLink?.collection.ownerId === userId;
}
const updatedLink = await prisma.link.update({ // Makes sure collection members (non-owners) cannot move a link to/from a collection.
where: { if (!isCollectionOwner)
id: link.id, return {
}, response: "You can't move a link to/from a collection you don't own.",
data: { status: 401,
name: link.name, };
description: link.description, else if (targetLink?.collection.ownerId !== userId && !memberHasAccess)
collection: { return {
connectOrCreate: { response: "Collection is not accessible.",
where: { status: 401,
name_ownerId: { };
ownerId: link.collection.ownerId, else {
name: link.collection.name, const updatedLink = await prisma.link.update({
}, where: {
}, id: link.id,
create: {
name: link.collection.name,
ownerId: userId,
},
},
}, },
tags: { data: {
set: [], name: link.name,
connectOrCreate: link.tags.map((tag) => ({ description: link.description,
where: { collection:
name_ownerId: { targetLink?.collection.ownerId === link.collection.ownerId &&
name: tag.name, link.collection.ownerId === userId
ownerId: link.collection.ownerId, ? {
}, connect: {
}, id: link.collection.id,
create: { },
name: tag.name, }
owner: { : undefined,
connect: { tags: {
id: link.collection.ownerId, set: [],
connectOrCreate: link.tags.map((tag) => ({
where: {
name_ownerId: {
name: tag.name,
ownerId: link.collection.ownerId,
}, },
}, },
}, create: {
})), name: tag.name,
owner: {
connect: {
id: link.collection.ownerId,
},
},
},
})),
},
pinnedBy:
link?.pinnedBy && link.pinnedBy[0]
? { connect: { id: userId } }
: { disconnect: { id: userId } },
}, },
pinnedBy: include: {
link?.pinnedBy && link.pinnedBy[0] tags: true,
? { connect: { id: userId } } collection: true,
: { disconnect: { id: userId } }, pinnedBy: isCollectionOwner
}, ? {
include: { where: { id: userId },
tags: true, select: { id: true },
collection: true, }
pinnedBy: { : undefined,
where: { id: userId },
select: { id: true },
}, },
}, });
});
return { response: updatedLink, status: 200 }; return { response: updatedLink, status: 200 };
}
} }

View File

@ -2,17 +2,33 @@ import { prisma } from "@/lib/api/db";
export default async function getPermission( export default async function getPermission(
userId: number, userId: number,
collectionId: number collectionId: number,
linkId?: number
) { ) {
const check = await prisma.collection.findFirst({ if (linkId) {
where: { const link = await prisma.link.findUnique({
AND: { where: {
id: collectionId, id: linkId,
OR: [{ ownerId: userId }, { members: { some: { userId } } }],
}, },
}, include: {
include: { members: true }, collection: {
}); include: { members: true },
},
},
});
return check; return link;
} else {
const check = await prisma.collection.findFirst({
where: {
AND: {
id: collectionId,
OR: [{ ownerId: userId }, { members: { some: { userId } } }],
},
},
include: { members: true },
});
return check;
}
} }

View File

@ -16,7 +16,7 @@ type Modal =
modal: "LINK"; modal: "LINK";
state: boolean; state: boolean;
method: "CREATE"; method: "CREATE";
isOwner?: boolean; isOwnerOrMod?: boolean;
active?: LinkIncludingShortenedCollectionAndTags; active?: LinkIncludingShortenedCollectionAndTags;
defaultIndex?: number; defaultIndex?: number;
} }
@ -24,7 +24,7 @@ type Modal =
modal: "LINK"; modal: "LINK";
state: boolean; state: boolean;
method: "UPDATE"; method: "UPDATE";
isOwner: boolean; isOwnerOrMod: boolean;
active: LinkIncludingShortenedCollectionAndTags; active: LinkIncludingShortenedCollectionAndTags;
defaultIndex?: number; defaultIndex?: number;
} }
@ -40,7 +40,7 @@ type Modal =
modal: "COLLECTION"; modal: "COLLECTION";
state: boolean; state: boolean;
method: "CREATE"; method: "CREATE";
isOwner: boolean; isOwner?: boolean;
active?: CollectionIncludingMembersAndLinkCount; active?: CollectionIncludingMembersAndLinkCount;
defaultIndex?: number; defaultIndex?: number;
} }

View File

@ -12,10 +12,7 @@ export interface LinkIncludingShortenedCollectionAndTags
pinnedBy?: { pinnedBy?: {
id: number; id: number;
}[]; }[];
collection: OptionalExcluding< collection: OptionalExcluding<Collection, "name" | "ownerId">;
Pick<Collection, "id" | "ownerId" | "name" | "color">,
"name" | "ownerId"
>;
} }
export interface Member { export interface Member {