From cc8e8dbe9aa0fb39ffc508ee664b5a20afbf07b6 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 27 Apr 2023 00:10:48 +0330 Subject: [PATCH] added collaboration mode for collections --- components/Modal/AddCollection.tsx | 186 +++++++++++++++++- components/Modal/EditCollection.tsx | 0 .../controllers/collections/getCollections.ts | 8 +- .../controllers/collections/postCollection.ts | 12 +- lib/api/controllers/links/deleteLink.ts | 7 +- lib/api/controllers/links/postLink.ts | 4 +- lib/api/controllers/links/updateLink.ts | 7 +- lib/api/controllers/users/getUsers.ts | 22 +++ ...AccessToCollection.ts => getPermission.ts} | 0 pages/api/archives/[...params].ts | 4 +- pages/api/routes/tags/index.ts | 9 +- pages/api/routes/users/index.ts | 23 +++ pages/register.tsx | 1 - store/collections.ts | 18 +- store/links.ts | 2 + types/global.ts | 13 +- 16 files changed, 279 insertions(+), 37 deletions(-) create mode 100644 components/Modal/EditCollection.tsx create mode 100644 lib/api/controllers/users/getUsers.ts rename lib/api/{hasAccessToCollection.ts => getPermission.ts} (100%) create mode 100644 pages/api/routes/users/index.ts diff --git a/components/Modal/AddCollection.tsx b/components/Modal/AddCollection.tsx index d2674b7..899e282 100644 --- a/components/Modal/AddCollection.tsx +++ b/components/Modal/AddCollection.tsx @@ -5,9 +5,10 @@ import React, { useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { faClose, faPlus } from "@fortawesome/free-solid-svg-icons"; import useCollectionStore from "@/store/collections"; import { NewCollection } from "@/types/global"; +import { useSession } from "next-auth/react"; type Props = { toggleCollectionModal: Function; @@ -17,10 +18,15 @@ export default function AddCollection({ toggleCollectionModal }: Props) { const [newCollection, setNewCollection] = useState({ name: "", description: "", + members: [], }); + const [memberEmail, setMemberEmail] = useState(""); + const { addCollection } = useCollectionStore(); + const session = useSession(); + const submitCollection = async () => { console.log(newCollection); @@ -29,6 +35,14 @@ export default function AddCollection({ toggleCollectionModal }: Props) { if (response) toggleCollectionModal(); }; + const getUserByEmail = async (email: string) => { + const response = await fetch(`/api/routes/users?email=${email}`); + + const data = await response.json(); + + return data.response; + }; + return (

New Collection

@@ -59,6 +73,176 @@ export default function AddCollection({ toggleCollectionModal }: Props) { />
+
+ +
+

Members

+ { + setMemberEmail(e.target.value); + }} + onKeyDown={async (e) => { + const checkIfMemberAlreadyExists = newCollection.members.find( + (e) => e.email === memberEmail + ); + + const ownerEmail = session.data?.user.email; + + if ( + e.key === "Enter" && + // no duplicate members + !checkIfMemberAlreadyExists && + // member can't be empty + memberEmail.trim() !== "" && + // member can't be the owner + memberEmail.trim() !== ownerEmail + ) { + // Lookup, get data/err, list ... + const user = await getUserByEmail(memberEmail.trim()); + + if (user.email) { + const newMember = { + name: user.name, + email: user.email, + canCreate: false, + canUpdate: false, + canDelete: false, + }; + + setNewCollection({ + ...newCollection, + members: [...newCollection.members, newMember], + }); + + setMemberEmail(""); + } + } + }} + type="text" + placeholder="Email (Optional)" + className="w-56 sm:w-96 rounded-md p-3 border-sky-100 border-solid border text-sm outline-none focus:border-sky-500 duration-100" + /> +
+ + {newCollection.members[0] ? ( +

+ (All Members will have Read access to this collection.) +

+ ) : null} + + {newCollection.members.map((e, i) => { + return ( +
+
+

{e.name}

+

{e.email}

+
+
+ { + const updatedMembers = newCollection.members.filter( + (member) => { + return member.email !== e.email; + } + ); + setNewCollection({ + ...newCollection, + members: updatedMembers, + }); + }} + /> +

Permissions

+

(Click to toggle.)

+ + + + + + +
+
+ ); + })} +
. import { prisma } from "@/lib/api/db"; -import { Collection } from "@prisma/client"; +import { NewCollection } from "@/types/global"; import { existsSync, mkdirSync } from "fs"; -export default async function (collection: Collection, userId: number) { +export default async function (collection: NewCollection, userId: number) { if (!collection || collection.name.trim() === "") return { response: "Please enter a valid collection.", @@ -41,6 +41,14 @@ export default async function (collection: Collection, userId: number) { }, name: collection.name, description: collection.description, + members: { + create: collection.members.map((e) => ({ + user: { connect: { email: e.email } }, + canCreate: e.canCreate, + canUpdate: e.canUpdate, + canDelete: e.canDelete, + })), + }, }, }); diff --git a/lib/api/controllers/links/deleteLink.ts b/lib/api/controllers/links/deleteLink.ts index e96458c..12342b5 100644 --- a/lib/api/controllers/links/deleteLink.ts +++ b/lib/api/controllers/links/deleteLink.ts @@ -7,15 +7,12 @@ import { prisma } from "@/lib/api/db"; import { ExtendedLink } from "@/types/global"; import fs from "fs"; import { Link, UsersAndCollections } from "@prisma/client"; -import hasAccessToCollection from "@/lib/api/hasAccessToCollection"; +import getPermission from "@/lib/api/getPermission"; export default async function (link: ExtendedLink, userId: number) { if (!link) return { response: "Please choose a valid link.", status: 401 }; - const collectionIsAccessible = await hasAccessToCollection( - userId, - link.collectionId - ); + const collectionIsAccessible = await getPermission(userId, link.collectionId); 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 093e9cf..26eb2dc 100644 --- a/lib/api/controllers/links/postLink.ts +++ b/lib/api/controllers/links/postLink.ts @@ -9,7 +9,7 @@ import getTitle from "../../getTitle"; import archive from "../../archive"; import { Link, UsersAndCollections } from "@prisma/client"; import AES from "crypto-js/aes"; -import hasAccessToCollection from "@/lib/api/hasAccessToCollection"; +import getPermission from "@/lib/api/getPermission"; export default async function (link: ExtendedLink, userId: number) { link.collection.name = link.collection.name.trim(); @@ -21,7 +21,7 @@ export default async function (link: ExtendedLink, userId: number) { } if (link.collection.ownerId) { - const collectionIsAccessible = await hasAccessToCollection( + const collectionIsAccessible = await getPermission( userId, link.collection.id ); diff --git a/lib/api/controllers/links/updateLink.ts b/lib/api/controllers/links/updateLink.ts index 1a7e53d..a1cfd23 100644 --- a/lib/api/controllers/links/updateLink.ts +++ b/lib/api/controllers/links/updateLink.ts @@ -6,15 +6,12 @@ import { prisma } from "@/lib/api/db"; import { ExtendedLink } from "@/types/global"; import { Link, UsersAndCollections } from "@prisma/client"; -import hasAccessToCollection from "@/lib/api/hasAccessToCollection"; +import getPermission from "@/lib/api/getPermission"; export default async function (link: ExtendedLink, userId: number) { if (!link) return { response: "Please choose a valid link.", status: 401 }; - const collectionIsAccessible = await hasAccessToCollection( - userId, - link.collectionId - ); + const collectionIsAccessible = await getPermission(userId, link.collectionId); const memberHasAccess = collectionIsAccessible?.members.some( (e: UsersAndCollections) => e.userId === userId && e.canUpdate diff --git a/lib/api/controllers/users/getUsers.ts b/lib/api/controllers/users/getUsers.ts new file mode 100644 index 0000000..f1780a4 --- /dev/null +++ b/lib/api/controllers/users/getUsers.ts @@ -0,0 +1,22 @@ +// Copyright (C) 2022-present Daniel31x13 +// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3. +// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// You should have received a copy of the GNU General Public License along with this program. If not, see . + +import { prisma } from "@/lib/api/db"; + +export default async function (email: string) { + const user = await prisma.user.findUnique({ + where: { + email, + }, + }); + + const unsensitiveUserInfo = { + name: user?.name, + email: user?.email, + createdAt: user?.createdAt, + }; + + return { response: unsensitiveUserInfo || null, status: 200 }; +} diff --git a/lib/api/hasAccessToCollection.ts b/lib/api/getPermission.ts similarity index 100% rename from lib/api/hasAccessToCollection.ts rename to lib/api/getPermission.ts diff --git a/pages/api/archives/[...params].ts b/pages/api/archives/[...params].ts index 5f2d188..317a05f 100644 --- a/pages/api/archives/[...params].ts +++ b/pages/api/archives/[...params].ts @@ -10,7 +10,7 @@ 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"; +import getPermission from "@/lib/api/getPermission"; export default async function (req: NextApiRequest, res: NextApiResponse) { if (!req.query.params) @@ -23,7 +23,7 @@ export default async function (req: NextApiRequest, res: NextApiResponse) { if (!session?.user?.email) return res.status(401).json({ response: "You must be logged in." }); - const collectionIsAccessible = await hasAccessToCollection( + const collectionIsAccessible = await getPermission( session.user.id, Number(collectionId) ); diff --git a/pages/api/routes/tags/index.ts b/pages/api/routes/tags/index.ts index b929edf..c14d9c9 100644 --- a/pages/api/routes/tags/index.ts +++ b/pages/api/routes/tags/index.ts @@ -8,14 +8,7 @@ import { getServerSession } from "next-auth/next"; import { authOptions } from "pages/api/auth/[...nextauth]"; import getTags from "@/lib/api/controllers/tags/getTags"; -type Data = { - response: object[] | string; -}; - -export default async function ( - req: NextApiRequest, - res: NextApiResponse -) { +export default async function (req: NextApiRequest, res: NextApiResponse) { const session = await getServerSession(req, res, authOptions); if (!session?.user?.email) { diff --git a/pages/api/routes/users/index.ts b/pages/api/routes/users/index.ts new file mode 100644 index 0000000..b3359ea --- /dev/null +++ b/pages/api/routes/users/index.ts @@ -0,0 +1,23 @@ +// Copyright (C) 2022-present Daniel31x13 +// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3. +// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// You should have received a copy of the GNU General Public License along with this program. If not, see . + +import type { NextApiRequest, NextApiResponse } from "next"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "pages/api/auth/[...nextauth]"; +import getUsers from "@/lib/api/controllers/users/getUsers"; + +export default async function (req: NextApiRequest, res: NextApiResponse) { + const session = await getServerSession(req, res, authOptions); + + if (!session?.user?.email) { + return res.status(401).json({ response: "You must be logged in." }); + } + + // get unsensitive user info by email + if (req.method === "GET") { + const users = await getUsers(req.query.email as string); + return res.status(users.status).json({ response: users.response }); + } +} diff --git a/pages/register.tsx b/pages/register.tsx index f39e8c1..e9fae86 100644 --- a/pages/register.tsx +++ b/pages/register.tsx @@ -24,7 +24,6 @@ export default function () { async function registerUser() { let success: boolean = false; - console.log(form); if (form.name != "" && form.email != "" && form.password != "") { await fetch("/api/auth/register", { diff --git a/store/collections.ts b/store/collections.ts index bcf4639..ee39c9c 100644 --- a/store/collections.ts +++ b/store/collections.ts @@ -5,13 +5,13 @@ import { create } from "zustand"; import { Collection } from "@prisma/client"; -import { NewCollection } from "@/types/global"; +import { ExtendedCollection, NewCollection } from "@/types/global"; type CollectionStore = { - collections: Collection[]; + collections: ExtendedCollection[]; setCollections: () => void; addCollection: (body: NewCollection) => Promise; - updateCollection: (collection: Collection) => void; + // updateCollection: (collection: Collection) => void; removeCollection: (collectionId: number) => void; }; @@ -44,12 +44,12 @@ const useCollectionStore = create()((set) => ({ return response.ok; }, - updateCollection: (collection) => - set((state) => ({ - collections: state.collections.map((c) => - c.id === collection.id ? collection : c - ), - })), + // updateCollection: (collection) => + // set((state) => ({ + // collections: state.collections.map((c) => + // c.id === collection.id ? collection : c + // ), + // })), removeCollection: (collectionId) => { set((state) => ({ collections: state.collections.filter((c) => c.id !== collectionId), diff --git a/store/links.ts b/store/links.ts index 35d57b1..68e9f51 100644 --- a/store/links.ts +++ b/store/links.ts @@ -79,6 +79,8 @@ const useLinkStore = create()((set) => ({ const data = await response.json(); + console.log(data); + if (response.ok) { set((state) => ({ links: state.links.filter((e) => e.id !== link.id), diff --git a/types/global.ts b/types/global.ts index d352910..21500a9 100644 --- a/types/global.ts +++ b/types/global.ts @@ -3,7 +3,7 @@ // This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // You should have received a copy of the GNU General Public License along with this program. If not, see . -import { Collection, Link, Tag } from "@prisma/client"; +import { Collection, Link, Tag, UsersAndCollections } from "@prisma/client"; export interface ExtendedLink extends Link { tags: Tag[]; @@ -24,4 +24,15 @@ export interface NewLink { export interface NewCollection { name: string; description: string; + members: { + name: string; + email: string; + canCreate: boolean; + canUpdate: boolean; + canDelete: boolean; + }[]; +} + +export interface ExtendedCollection extends Collection { + members: UsersAndCollections[]; }