diff --git a/components/InputSelect/CollectionSelection.tsx b/components/InputSelect/CollectionSelection.tsx index 239d039..238b8f0 100644 --- a/components/InputSelect/CollectionSelection.tsx +++ b/components/InputSelect/CollectionSelection.tsx @@ -1,7 +1,7 @@ import useCollectionStore from "@/store/collections"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; -import CreatableSelect from "react-select/creatable"; +import Select from "react-select"; import { styles } from "./styles"; import { Options } from "./types"; @@ -43,7 +43,7 @@ export default function CollectionSelection({ onChange, defaultValue }: Props) { }, [collections]); return ( - - {permissions && ( + {(permissions === true || + permissions?.canUpdate || + permissions?.canDelete) && (
setExpandDropdown(!expandDropdown)} id={"expand-dropdown" + link.id} @@ -83,7 +85,8 @@ export default function LinkCard({ link, count, className }: Props) { modal: "LINK", state: true, method: "UPDATE", - isOwner: permissions === true, + isOwnerOrMod: + permissions === true || (permissions?.canUpdate as boolean), active: link, }); }} @@ -106,7 +109,7 @@ export default function LinkCard({ link, count, className }: Props) {

{count + 1}.

-

+

{link.name}

@@ -117,7 +120,7 @@ export default function LinkCard({ link, count, className }: Props) { className="w-4 h-4 mt-1 drop-shadow" style={{ color: collection?.color }} /> -

+

{collection?.name}

@@ -132,7 +135,7 @@ export default function LinkCard({ link, count, className }: Props) { {expandDropdown ? ( - {method === "UPDATE" && isOwner && ( + {method === "UPDATE" && isOwnerOrMod && ( <> diff --git a/components/ModalManagement.tsx b/components/ModalManagement.tsx index 16ff279..092ffee 100644 --- a/components/ModalManagement.tsx +++ b/components/ModalManagement.tsx @@ -29,7 +29,7 @@ export default function ModalManagement() { diff --git a/lib/api/controllers/collections/deleteCollection.ts b/lib/api/controllers/collections/deleteCollection.ts index d45308a..e6354f1 100644 --- a/lib/api/controllers/collections/deleteCollection.ts +++ b/lib/api/controllers/collections/deleteCollection.ts @@ -1,6 +1,6 @@ import { prisma } from "@/lib/api/db"; import getPermission from "@/lib/api/getPermission"; -import { UsersAndCollections } from "@prisma/client"; +import { Collection, UsersAndCollections } from "@prisma/client"; import fs from "fs"; export default async function deleteCollection( @@ -12,7 +12,11 @@ export default async function deleteCollection( if (!collectionId) 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( (e: UsersAndCollections) => e.userId === userId diff --git a/lib/api/controllers/collections/updateCollection.ts b/lib/api/controllers/collections/updateCollection.ts index d1a6b65..b476eec 100644 --- a/lib/api/controllers/collections/updateCollection.ts +++ b/lib/api/controllers/collections/updateCollection.ts @@ -1,6 +1,7 @@ import { prisma } from "@/lib/api/db"; import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; import getPermission from "@/lib/api/getPermission"; +import { Collection, UsersAndCollections } from "@prisma/client"; export default async function updateCollection( collection: CollectionIncludingMembersAndLinkCount, @@ -9,7 +10,14 @@ export default async function updateCollection( if (!collection.id) 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)) return { response: "Collection is not accessible.", status: 401 }; diff --git a/lib/api/controllers/links/deleteLink.ts b/lib/api/controllers/links/deleteLink.ts index 7849c1e..d52d333 100644 --- a/lib/api/controllers/links/deleteLink.ts +++ b/lib/api/controllers/links/deleteLink.ts @@ -1,7 +1,7 @@ import { prisma } from "@/lib/api/db"; import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import fs from "fs"; -import { Link, UsersAndCollections } from "@prisma/client"; +import { Collection, Link, UsersAndCollections } from "@prisma/client"; import getPermission from "@/lib/api/getPermission"; export default async function deleteLink( @@ -11,7 +11,14 @@ export default async function deleteLink( if (!link || !link.collectionId) 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( (e: UsersAndCollections) => e.userId === userId && e.canDelete diff --git a/lib/api/controllers/links/postLink.ts b/lib/api/controllers/links/postLink.ts index 767624f..474b517 100644 --- a/lib/api/controllers/links/postLink.ts +++ b/lib/api/controllers/links/postLink.ts @@ -2,7 +2,7 @@ import { prisma } from "@/lib/api/db"; import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import getTitle from "../../getTitle"; import archive from "../../archive"; -import { Link, UsersAndCollections } from "@prisma/client"; +import { Collection, Link, UsersAndCollections } from "@prisma/client"; import getPermission from "@/lib/api/getPermission"; import { existsSync, mkdirSync } from "fs"; @@ -13,16 +13,20 @@ export default async function postLink( link.collection.name = link.collection.name.trim(); 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) { - return { response: "Please enter a valid collection name.", status: 401 }; + return { response: "Please enter a valid collection.", status: 400 }; } if (link.collection.id) { - const collectionIsAccessible = await getPermission( + const collectionIsAccessible = (await getPermission( userId, link.collection.id - ); + )) as + | (Collection & { + members: UsersAndCollections[]; + }) + | null; const memberHasAccess = collectionIsAccessible?.members.some( (e: UsersAndCollections) => e.userId === userId && e.canCreate diff --git a/lib/api/controllers/links/updateLink.ts b/lib/api/controllers/links/updateLink.ts index dba7d26..cdd0f67 100644 --- a/lib/api/controllers/links/updateLink.ts +++ b/lib/api/controllers/links/updateLink.ts @@ -1,84 +1,103 @@ import { prisma } from "@/lib/api/db"; import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; -import { UsersAndCollections } from "@prisma/client"; +import { Collection, Link, UsersAndCollections } from "@prisma/client"; import getPermission from "@/lib/api/getPermission"; export default async function updateLink( link: LinkIncludingShortenedCollectionAndTags, 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 collectionIsAccessible = await getPermission( - userId, - link.collection.id - ); + const targetLink = (await getPermission( + userId, + link.collection.id, + link.id + )) as + | (Link & { + collection: Collection & { + members: UsersAndCollections[]; + }; + }) + | null; - const memberHasAccess = collectionIsAccessible?.members.some( - (e: UsersAndCollections) => e.userId === userId && e.canCreate - ); + const memberHasAccess = targetLink?.collection.members.some( + (e: UsersAndCollections) => e.userId === userId && e.canUpdate + ); - if (!(collectionIsAccessible?.ownerId === userId || memberHasAccess)) - return { response: "Collection is not accessible.", status: 401 }; - } else { - link.collection.ownerId = userId; - } + const isCollectionOwner = + targetLink?.collection.ownerId === link.collection.ownerId && + link.collection.ownerId === userId && + targetLink?.collection.ownerId === userId; - const updatedLink = await prisma.link.update({ - where: { - id: link.id, - }, - data: { - name: link.name, - description: link.description, - collection: { - connectOrCreate: { - where: { - name_ownerId: { - ownerId: link.collection.ownerId, - name: link.collection.name, - }, - }, - create: { - name: link.collection.name, - ownerId: userId, - }, - }, + // Makes sure collection members (non-owners) cannot move a link to/from a collection. + if (!isCollectionOwner) + return { + response: "You can't move a link to/from a collection you don't own.", + status: 401, + }; + else if (targetLink?.collection.ownerId !== userId && !memberHasAccess) + return { + response: "Collection is not accessible.", + status: 401, + }; + else { + const updatedLink = await prisma.link.update({ + where: { + id: link.id, }, - tags: { - 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, + data: { + name: link.name, + description: link.description, + collection: + targetLink?.collection.ownerId === link.collection.ownerId && + link.collection.ownerId === userId + ? { + connect: { + id: link.collection.id, + }, + } + : undefined, + tags: { + 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: - link?.pinnedBy && link.pinnedBy[0] - ? { connect: { id: userId } } - : { disconnect: { id: userId } }, - }, - include: { - tags: true, - collection: true, - pinnedBy: { - where: { id: userId }, - select: { id: true }, + include: { + tags: true, + collection: true, + pinnedBy: isCollectionOwner + ? { + where: { id: userId }, + select: { id: true }, + } + : undefined, }, - }, - }); + }); - return { response: updatedLink, status: 200 }; + return { response: updatedLink, status: 200 }; + } } diff --git a/lib/api/getPermission.ts b/lib/api/getPermission.ts index d4d99af..f76b470 100644 --- a/lib/api/getPermission.ts +++ b/lib/api/getPermission.ts @@ -2,17 +2,33 @@ import { prisma } from "@/lib/api/db"; export default async function getPermission( userId: number, - collectionId: number + collectionId: number, + linkId?: number ) { - const check = await prisma.collection.findFirst({ - where: { - AND: { - id: collectionId, - OR: [{ ownerId: userId }, { members: { some: { userId } } }], + if (linkId) { + const link = await prisma.link.findUnique({ + where: { + id: linkId, }, - }, - include: { members: true }, - }); + include: { + 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; + } } diff --git a/store/modals.ts b/store/modals.ts index 0af80cb..047bc28 100644 --- a/store/modals.ts +++ b/store/modals.ts @@ -16,7 +16,7 @@ type Modal = modal: "LINK"; state: boolean; method: "CREATE"; - isOwner?: boolean; + isOwnerOrMod?: boolean; active?: LinkIncludingShortenedCollectionAndTags; defaultIndex?: number; } @@ -24,7 +24,7 @@ type Modal = modal: "LINK"; state: boolean; method: "UPDATE"; - isOwner: boolean; + isOwnerOrMod: boolean; active: LinkIncludingShortenedCollectionAndTags; defaultIndex?: number; } @@ -40,7 +40,7 @@ type Modal = modal: "COLLECTION"; state: boolean; method: "CREATE"; - isOwner: boolean; + isOwner?: boolean; active?: CollectionIncludingMembersAndLinkCount; defaultIndex?: number; } diff --git a/types/global.ts b/types/global.ts index b8e2161..ab96c7a 100644 --- a/types/global.ts +++ b/types/global.ts @@ -12,10 +12,7 @@ export interface LinkIncludingShortenedCollectionAndTags pinnedBy?: { id: number; }[]; - collection: OptionalExcluding< - Pick, - "name" | "ownerId" - >; + collection: OptionalExcluding; } export interface Member {