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 { 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 (
<CreatableSelect
<Select
isClearable
onChange={onChange}
options={options}

View File

@ -62,7 +62,9 @@ export default function LinkCard({ link, count, className }: Props) {
<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}`}
>
{permissions && (
{(permissions === true ||
permissions?.canUpdate ||
permissions?.canDelete) && (
<div
onClick={() => 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) {
<div className="flex flex-col justify-between w-full">
<div className="flex items-baseline gap-1">
<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}
</p>
</div>
@ -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 }}
/>
<p className="text-sky-900 truncate max-w-[10rem]">
<p className="text-sky-900 truncate max-w-[10rem] capitalize">
{collection?.name}
</p>
</div>
@ -132,7 +135,7 @@ export default function LinkCard({ link, count, className }: Props) {
{expandDropdown ? (
<Dropdown
items={[
permissions === true || permissions?.canUpdate
permissions === true
? {
name:
link?.pinnedBy && link.pinnedBy[0]
@ -158,7 +161,8 @@ export default function LinkCard({ link, count, className }: Props) {
modal: "LINK",
state: true,
method: "UPDATE",
isOwner: permissions === true,
isOwnerOrMod:
permissions === true || permissions?.canUpdate,
active: link,
defaultIndex: 1,
});

View File

@ -97,7 +97,7 @@ export default function DeleteCollection({
)}
<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
? inputField === collection.name
? "bg-red-500 hover:bg-red-400 cursor-pointer"

View File

@ -7,7 +7,7 @@ type Props =
| {
toggleLinkModal: Function;
method: "CREATE";
isOwner?: boolean;
isOwnerOrMod?: boolean;
activeLink?: LinkIncludingShortenedCollectionAndTags;
defaultIndex?: number;
className?: string;
@ -15,17 +15,17 @@ type Props =
| {
toggleLinkModal: Function;
method: "UPDATE";
isOwner: boolean;
isOwnerOrMod: boolean;
activeLink: LinkIncludingShortenedCollectionAndTags;
defaultIndex?: number;
className?: string;
};
export default function CollectionModal({
export default function LinkModal({
className,
defaultIndex,
toggleLinkModal,
isOwner,
isOwnerOrMod,
activeLink,
method,
}: Props) {
@ -37,10 +37,10 @@ export default function CollectionModal({
)}
<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 ${
isOwner ? "" : "pb-8"
isOwnerOrMod ? "" : "pb-8"
}`}
>
{method === "UPDATE" && isOwner && (
{method === "UPDATE" && isOwnerOrMod && (
<>
<Tab
className={({ selected }) =>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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