diff --git a/.env.sample b/.env.sample index b6eebf3..4e1966e 100644 --- a/.env.sample +++ b/.env.sample @@ -20,14 +20,5 @@ NEXT_PUBLIC_EMAIL_PROVIDER= EMAIL_FROM= EMAIL_SERVER= -# Stripe settings (You don't need these, it's for the cloud instance payments) -NEXT_PUBLIC_STRIPE_IS_ACTIVE= -STRIPE_SECRET_KEY= -MONTHLY_PRICE_ID= -YEARLY_PRICE_ID= -NEXT_PUBLIC_TRIAL_PERIOD_DAYS= -NEXT_PUBLIC_STRIPE_BILLING_PORTAL_URL= -BASE_URL=http://localhost:3000 - # Docker postgres settings POSTGRES_PASSWORD= diff --git a/README.md b/README.md index 7840caa..a830530 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ We highly recommend that you don't use the old version because it is no longer m ## Features - 📸 Auto capture a screenshot and a PDF of each link. -- 🏛️ Send your webpage to Wayback Machine archive.org for a snapshot. +- 🏛️ Send your webpage to Wayback Machine ([archive.org](https://archive.org)) for a snapshot. (Optional) - 📂 Organize links by collection, name, description and multiple tags. - 👥 Collaborate on gathering links in a collection. - 🔐 Customize the permissions of each member. diff --git a/components/CollectionCard.tsx b/components/CollectionCard.tsx index a2ffa49..63f08c4 100644 --- a/components/CollectionCard.tsx +++ b/components/CollectionCard.tsx @@ -68,7 +68,7 @@ export default function CollectionCard({ collection, className }: Props) { return ( ); diff --git a/components/LinkCard.tsx b/components/LinkCard.tsx index 0658ea7..b50299d 100644 --- a/components/LinkCard.tsx +++ b/components/LinkCard.tsx @@ -87,7 +87,7 @@ export default function LinkCard({ link, count, className }: Props) { const deleteLink = async () => { const load = toast.loading("Deleting..."); - const response = await removeLink(link); + const response = await removeLink(link.id as number); toast.dismiss(load); diff --git a/components/Modal/Collection/TeamManagement.tsx b/components/Modal/Collection/TeamManagement.tsx index 633e0b9..b163681 100644 --- a/components/Modal/Collection/TeamManagement.tsx +++ b/components/Modal/Collection/TeamManagement.tsx @@ -58,7 +58,7 @@ export default function TeamManagement({ useEffect(() => { const fetchOwner = async () => { - const owner = await getPublicUserData({ id: collection.ownerId }); + const owner = await getPublicUserData(collection.ownerId as number); setCollectionOwner(owner); }; @@ -238,7 +238,7 @@ export default function TeamManagement({ )}
@@ -425,7 +425,7 @@ export default function TeamManagement({ >
diff --git a/components/Modal/Link/LinkDetails.tsx b/components/Modal/Link/LinkDetails.tsx index 4b870d8..e593e09 100644 --- a/components/Modal/Link/LinkDetails.tsx +++ b/components/Modal/Link/LinkDetails.tsx @@ -106,7 +106,7 @@ export default function LinkDetails({ link, isOwnerOrMod }: Props) { }, [colorPalette, theme]); const handleDownload = (format: "png" | "pdf") => { - const path = `/api/archives/${link.collection.id}/${link.id}.${format}`; + const path = `/api/v1/archives/${link.collection.id}/${link.id}.${format}`; fetch(path) .then((response) => { if (response.ok) { @@ -250,7 +250,7 @@ export default function LinkDetails({ link, isOwnerOrMod }: Props) {
@@ -283,7 +283,7 @@ export default function LinkDetails({ link, isOwnerOrMod }: Props) {
diff --git a/hooks/useLinks.tsx b/hooks/useLinks.tsx index 25d65b2..7f8d84e 100644 --- a/hooks/useLinks.tsx +++ b/hooks/useLinks.tsx @@ -33,7 +33,7 @@ export default function useLinks( const encodedData = encodeURIComponent(JSON.stringify(requestBody)); const response = await fetch( - `/api/links?body=${encodeURIComponent(encodedData)}` + `/api/v1/links?body=${encodeURIComponent(encodedData)}` ); const data = await response.json(); diff --git a/lib/api/controllers/collections/deleteCollection.ts b/lib/api/controllers/collections/collectionId/deleteCollectionById.ts similarity index 91% rename from lib/api/controllers/collections/deleteCollection.ts rename to lib/api/controllers/collections/collectionId/deleteCollectionById.ts index c42ba52..792f9fa 100644 --- a/lib/api/controllers/collections/deleteCollection.ts +++ b/lib/api/controllers/collections/collectionId/deleteCollectionById.ts @@ -4,15 +4,16 @@ import { Collection, UsersAndCollections } from "@prisma/client"; import removeFolder from "@/lib/api/storage/removeFolder"; export default async function deleteCollection( - collection: { id: number }, - userId: number + userId: number, + collectionId: number ) { - const collectionId = collection.id; - if (!collectionId) return { response: "Please choose a valid collection.", status: 401 }; - const collectionIsAccessible = (await getPermission(userId, collectionId)) as + const collectionIsAccessible = (await getPermission({ + userId, + collectionId, + })) as | (Collection & { members: UsersAndCollections[]; }) diff --git a/lib/api/controllers/collections/updateCollection.ts b/lib/api/controllers/collections/collectionId/updateCollectionById.ts similarity index 77% rename from lib/api/controllers/collections/updateCollection.ts rename to lib/api/controllers/collections/collectionId/updateCollectionById.ts index 7cafedd..076bb08 100644 --- a/lib/api/controllers/collections/updateCollection.ts +++ b/lib/api/controllers/collections/collectionId/updateCollectionById.ts @@ -4,16 +4,17 @@ import getPermission from "@/lib/api/getPermission"; import { Collection, UsersAndCollections } from "@prisma/client"; export default async function updateCollection( - collection: CollectionIncludingMembersAndLinkCount, - userId: number + userId: number, + collectionId: number, + data: CollectionIncludingMembersAndLinkCount ) { - if (!collection.id) + if (!collectionId) return { response: "Please choose a valid collection.", status: 401 }; - const collectionIsAccessible = (await getPermission( + const collectionIsAccessible = (await getPermission({ userId, - collection.id - )) as + collectionId, + })) as | (Collection & { members: UsersAndCollections[]; }) @@ -26,23 +27,23 @@ export default async function updateCollection( await prisma.usersAndCollections.deleteMany({ where: { collection: { - id: collection.id, + id: collectionId, }, }, }); return await prisma.collection.update({ where: { - id: collection.id, + id: collectionId, }, data: { - name: collection.name.trim(), - description: collection.description, - color: collection.color, - isPublic: collection.isPublic, + name: data.name.trim(), + description: data.description, + color: data.color, + isPublic: data.isPublic, members: { - create: collection.members.map((e) => ({ + create: data.members.map((e) => ({ user: { connect: { id: e.user.id || e.userId } }, canCreate: e.canCreate, canUpdate: e.canUpdate, diff --git a/lib/api/controllers/links/deleteLink.ts b/lib/api/controllers/links/linkId/deleteLinkById.ts similarity index 56% rename from lib/api/controllers/links/deleteLink.ts rename to lib/api/controllers/links/linkId/deleteLinkById.ts index cddfe28..b66b850 100644 --- a/lib/api/controllers/links/deleteLink.ts +++ b/lib/api/controllers/links/linkId/deleteLinkById.ts @@ -1,20 +1,12 @@ import { prisma } from "@/lib/api/db"; -import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import { Collection, Link, UsersAndCollections } from "@prisma/client"; import getPermission from "@/lib/api/getPermission"; import removeFile from "@/lib/api/storage/removeFile"; -export default async function deleteLink( - link: LinkIncludingShortenedCollectionAndTags, - userId: number -) { - if (!link || !link.collectionId) - return { response: "Please choose a valid link.", status: 401 }; +export default async function deleteLink(userId: number, linkId: number) { + if (!linkId) return { response: "Please choose a valid link.", status: 401 }; - const collectionIsAccessible = (await getPermission( - userId, - link.collectionId - )) as + const collectionIsAccessible = (await getPermission({ userId, linkId })) as | (Collection & { members: UsersAndCollections[]; }) @@ -29,12 +21,16 @@ export default async function deleteLink( const deleteLink: Link = await prisma.link.delete({ where: { - id: link.id, + id: linkId, }, }); - removeFile({ filePath: `archives/${link.collectionId}/${link.id}.pdf` }); - removeFile({ filePath: `archives/${link.collectionId}/${link.id}.png` }); + removeFile({ + filePath: `archives/${collectionIsAccessible?.id}/${linkId}.pdf`, + }); + removeFile({ + filePath: `archives/${collectionIsAccessible?.id}/${linkId}.png`, + }); return { response: deleteLink, status: 200 }; } diff --git a/lib/api/controllers/links/updateLink.ts b/lib/api/controllers/links/linkId/updateLinkById.ts similarity index 59% rename from lib/api/controllers/links/updateLink.ts rename to lib/api/controllers/links/linkId/updateLinkById.ts index 13a9c53..9ed6464 100644 --- a/lib/api/controllers/links/updateLink.ts +++ b/lib/api/controllers/links/linkId/updateLinkById.ts @@ -5,40 +5,32 @@ import getPermission from "@/lib/api/getPermission"; import moveFile from "@/lib/api/storage/moveFile"; export default async function updateLink( - link: LinkIncludingShortenedCollectionAndTags, - userId: number + userId: number, + linkId: number, + data: LinkIncludingShortenedCollectionAndTags ) { - console.log(link); - if (!link || !link.collection.id) + if (!data || !data.collection.id) return { response: "Please choose a valid link and collection.", status: 401, }; - const targetLink = (await getPermission( - userId, - link.collection.id, - link.id - )) as - | (Link & { - collection: Collection & { - members: UsersAndCollections[]; - }; + const collectionIsAccessible = (await getPermission({ userId, linkId })) as + | (Collection & { + members: UsersAndCollections[]; }) | null; - const memberHasAccess = targetLink?.collection.members.some( + const memberHasAccess = collectionIsAccessible?.members.some( (e: UsersAndCollections) => e.userId === userId && e.canUpdate ); const isCollectionOwner = - targetLink?.collection.ownerId === link.collection.ownerId && - link.collection.ownerId === userId; + collectionIsAccessible?.ownerId === data.collection.ownerId && + data.collection.ownerId === userId; const unauthorizedSwitchCollection = - !isCollectionOwner && targetLink?.collection.id !== link.collection.id; - - console.log(isCollectionOwner); + !isCollectionOwner && collectionIsAccessible?.id !== data.collection.id; // Makes sure collection members (non-owners) cannot move a link to/from a collection. if (unauthorizedSwitchCollection) @@ -46,7 +38,7 @@ export default async function updateLink( response: "You can't move a link to/from a collection you don't own.", status: 401, }; - else if (targetLink?.collection.ownerId !== userId && !memberHasAccess) + else if (collectionIsAccessible?.ownerId !== userId && !memberHasAccess) return { response: "Collection is not accessible.", status: 401, @@ -54,37 +46,37 @@ export default async function updateLink( else { const updatedLink = await prisma.link.update({ where: { - id: link.id, + id: linkId, }, data: { - name: link.name, - description: link.description, + name: data.name, + description: data.description, collection: { connect: { - id: link.collection.id, + id: data.collection.id, }, }, tags: { set: [], - connectOrCreate: link.tags.map((tag) => ({ + connectOrCreate: data.tags.map((tag) => ({ where: { name_ownerId: { name: tag.name, - ownerId: link.collection.ownerId, + ownerId: data.collection.ownerId, }, }, create: { name: tag.name, owner: { connect: { - id: link.collection.ownerId, + id: data.collection.ownerId, }, }, }, })), }, pinnedBy: - link?.pinnedBy && link.pinnedBy[0] + data?.pinnedBy && data.pinnedBy[0] ? { connect: { id: userId } } : { disconnect: { id: userId } }, }, @@ -100,15 +92,15 @@ export default async function updateLink( }, }); - if (targetLink?.collection.id !== link.collection.id) { + if (collectionIsAccessible?.id !== data.collection.id) { await moveFile( - `archives/${targetLink?.collection.id}/${link.id}.pdf`, - `archives/${link.collection.id}/${link.id}.pdf` + `archives/${collectionIsAccessible?.id}/${linkId}.pdf`, + `archives/${data.collection.id}/${linkId}.pdf` ); await moveFile( - `archives/${targetLink?.collection.id}/${link.id}.png`, - `archives/${link.collection.id}/${link.id}.png` + `archives/${collectionIsAccessible?.id}/${linkId}.png`, + `archives/${data.collection.id}/${linkId}.png` ); } diff --git a/lib/api/controllers/links/postLink.ts b/lib/api/controllers/links/postLink.ts index 33b4ba2..5830040 100644 --- a/lib/api/controllers/links/postLink.ts +++ b/lib/api/controllers/links/postLink.ts @@ -27,10 +27,10 @@ export default async function postLink( link.collection.name = link.collection.name.trim(); if (link.collection.id) { - const collectionIsAccessible = (await getPermission( + const collectionIsAccessible = (await getPermission({ userId, - link.collection.id - )) as + collectionId: link.collection.id, + })) as | (Collection & { members: UsersAndCollections[]; }) diff --git a/lib/api/controllers/tags/tagId/updeteTagById.ts b/lib/api/controllers/tags/tagId/updeteTagById.ts new file mode 100644 index 0000000..c477475 --- /dev/null +++ b/lib/api/controllers/tags/tagId/updeteTagById.ts @@ -0,0 +1,47 @@ +import { prisma } from "@/lib/api/db"; +import { Tag } from "@prisma/client"; + +export default async function updateTag( + userId: number, + tagId: number, + data: Tag +) { + if (!tagId || !data.name) + return { response: "Please choose a valid name for the tag.", status: 401 }; + + const tagNameIsTaken = await prisma.tag.findFirst({ + where: { + ownerId: userId, + name: data.name, + }, + }); + + if (tagNameIsTaken) + return { + response: "Tag names should be unique.", + status: 400, + }; + + const targetTag = await prisma.tag.findUnique({ + where: { + id: tagId, + }, + }); + + if (targetTag?.ownerId !== userId) + return { + response: "Permission denied.", + status: 401, + }; + + const updatedTag = await prisma.tag.update({ + where: { + id: tagId, + }, + data: { + name: data.name, + }, + }); + + return { response: updatedTag, status: 200 }; +} diff --git a/lib/api/controllers/users/getUsers.ts b/lib/api/controllers/users/getUsers.ts deleted file mode 100644 index 9d752a6..0000000 --- a/lib/api/controllers/users/getUsers.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { prisma } from "@/lib/api/db"; - -export default async function getUser({ - params, - isSelf, - username, -}: { - params: { - lookupUsername?: string; - lookupId?: number; - }; - isSelf: boolean; - username: string; -}) { - const user = await prisma.user.findUnique({ - where: { - id: params.lookupId, - username: params.lookupUsername?.toLowerCase(), - }, - include: { - whitelistedUsers: { - select: { - username: true - } - } - } - }); - - if (!user) return { response: "User not found.", status: 404 }; - - const whitelistedUsernames = user.whitelistedUsers?.map(usernames => usernames.username); - - if ( - !isSelf && - user?.isPrivate && - !whitelistedUsernames.includes(username.toLowerCase()) - ) { - return { response: "This profile is private.", status: 401 }; - } - - const { password, ...lessSensitiveInfo } = user; - - const data = isSelf - ? // If user is requesting its own data - {...lessSensitiveInfo, whitelistedUsers: whitelistedUsernames} - : { - // If user is requesting someone elses data - id: lessSensitiveInfo.id, - name: lessSensitiveInfo.name, - username: lessSensitiveInfo.username, - }; - - return { response: data || null, status: 200 }; -} diff --git a/pages/api/auth/register.ts b/lib/api/controllers/users/postUser.ts similarity index 78% rename from pages/api/auth/register.ts rename to lib/api/controllers/users/postUser.ts index 7247765..5025d4e 100644 --- a/pages/api/auth/register.ts +++ b/lib/api/controllers/users/postUser.ts @@ -16,7 +16,7 @@ interface User { password: string; } -export default async function Index( +export default async function postUser( req: NextApiRequest, res: NextApiResponse ) { @@ -35,8 +35,16 @@ export default async function Index( .status(400) .json({ response: "Please fill out all the fields." }); - const checkUsername = RegExp("^[a-z0-9_-]{3,31}$"); + // Check email (if enabled) + const checkEmail = + /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; + if (emailEnabled && !checkEmail.test(body.email?.toLowerCase() || "")) + return res.status(400).json({ + response: "Please enter a valid email.", + }); + // Check username (if email was disabled) + const checkUsername = RegExp("^[a-z0-9_-]{3,31}$"); if (!emailEnabled && !checkUsername.test(body.username?.toLowerCase() || "")) return res.status(400).json({ response: @@ -47,7 +55,6 @@ export default async function Index( where: emailEnabled ? { email: body.email?.toLowerCase(), - emailVerified: { not: null }, } : { username: (body.username as string).toLowerCase(), @@ -72,8 +79,8 @@ export default async function Index( return res.status(201).json({ response: "User successfully created." }); } else if (checkIfUserExists) { - return res - .status(400) - .json({ response: "Username and/or Email already exists." }); + return res.status(400).json({ + response: `${emailEnabled ? "Email" : "Username"} already exists.`, + }); } } diff --git a/lib/api/controllers/users/userId/getPublicUserById.ts b/lib/api/controllers/users/userId/getPublicUserById.ts new file mode 100644 index 0000000..0d00af0 --- /dev/null +++ b/lib/api/controllers/users/userId/getPublicUserById.ts @@ -0,0 +1,48 @@ +import { prisma } from "@/lib/api/db"; + +export default async function getUser( + targetId: number | string, + isId: boolean, + requestingUsername?: string +) { + const user = await prisma.user.findUnique({ + where: isId + ? { + id: Number(targetId) as number, + } + : { + username: targetId as string, + }, + include: { + whitelistedUsers: { + select: { + username: true, + }, + }, + }, + }); + + if (!user) + return { response: "User not found or profile is private.", status: 404 }; + + const whitelistedUsernames = user.whitelistedUsers?.map( + (usernames) => usernames.username + ); + + if ( + user?.isPrivate && + (!requestingUsername || + !whitelistedUsernames.includes(requestingUsername?.toLowerCase())) + ) { + return { response: "User not found or profile is private.", status: 404 }; + } + + const { password, ...lessSensitiveInfo } = user; + + const data = { + name: lessSensitiveInfo.name, + username: lessSensitiveInfo.username, + }; + + return { response: data, status: 200 }; +} diff --git a/lib/api/controllers/users/userId/getUserById.ts b/lib/api/controllers/users/userId/getUserById.ts new file mode 100644 index 0000000..2b29540 --- /dev/null +++ b/lib/api/controllers/users/userId/getUserById.ts @@ -0,0 +1,32 @@ +import { prisma } from "@/lib/api/db"; + +export default async function getUser(userId: number) { + const user = await prisma.user.findUnique({ + where: { + id: userId, + }, + include: { + whitelistedUsers: { + select: { + username: true, + }, + }, + }, + }); + + if (!user) + return { response: "User not found or profile is private.", status: 404 }; + + const whitelistedUsernames = user.whitelistedUsers?.map( + (usernames) => usernames.username + ); + + const { password, ...lessSensitiveInfo } = user; + + const data = { + ...lessSensitiveInfo, + whitelistedUsers: whitelistedUsernames, + }; + + return { response: data, status: 200 }; +} diff --git a/lib/api/controllers/users/updateUser.ts b/lib/api/controllers/users/userId/updateUserById.ts similarity index 80% rename from lib/api/controllers/users/updateUser.ts rename to lib/api/controllers/users/userId/updateUserById.ts index 6bb4c22..b2229e3 100644 --- a/lib/api/controllers/users/updateUser.ts +++ b/lib/api/controllers/users/userId/updateUserById.ts @@ -10,20 +10,20 @@ const emailEnabled = process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false; export default async function updateUser( - user: AccountSettings, sessionUser: { id: number; username: string; email: string; isSubscriber: boolean; - } + }, + data: AccountSettings ) { - if (emailEnabled && !user.email) + if (emailEnabled && !data.email) return { response: "Email invalid.", status: 400, }; - else if (!user.username) + else if (!data.username) return { response: "Username invalid.", status: 400, @@ -31,7 +31,7 @@ export default async function updateUser( const checkUsername = RegExp("^[a-z0-9_-]{3,31}$"); - if (!checkUsername.test(user.username.toLowerCase())) + if (!checkUsername.test(data.username.toLowerCase())) return { response: "Username has to be between 3-30 characters, no spaces and special characters are allowed.", @@ -44,15 +44,15 @@ export default async function updateUser( OR: emailEnabled ? [ { - username: user.username.toLowerCase(), + username: data.username.toLowerCase(), }, { - email: user.email?.toLowerCase(), + email: data.email?.toLowerCase(), }, ] : [ { - username: user.username.toLowerCase(), + username: data.username.toLowerCase(), }, ], }, @@ -66,10 +66,10 @@ export default async function updateUser( // Avatar Settings - const profilePic = user.profilePic; + const profilePic = data.profilePic; if (profilePic.startsWith("data:image/jpeg;base64")) { - if (user.profilePic.length < 1572864) { + if (data.profilePic.length < 1572864) { try { const base64Data = profilePic.replace(/^data:image\/jpeg;base64,/, ""); @@ -97,22 +97,22 @@ export default async function updateUser( // Other settings const saltRounds = 10; - const newHashedPassword = bcrypt.hashSync(user.newPassword || "", saltRounds); + const newHashedPassword = bcrypt.hashSync(data.newPassword || "", saltRounds); const updatedUser = await prisma.user.update({ where: { id: sessionUser.id, }, data: { - name: user.name, - username: user.username.toLowerCase(), - email: user.email?.toLowerCase(), - isPrivate: user.isPrivate, - archiveAsScreenshot: user.archiveAsScreenshot, - archiveAsPDF: user.archiveAsPDF, - archiveAsWaybackMachine: user.archiveAsWaybackMachine, + name: data.name, + username: data.username.toLowerCase(), + email: data.email?.toLowerCase(), + isPrivate: data.isPrivate, + archiveAsScreenshot: data.archiveAsScreenshot, + archiveAsPDF: data.archiveAsPDF, + archiveAsWaybackMachine: data.archiveAsWaybackMachine, password: - user.newPassword && user.newPassword !== "" + data.newPassword && data.newPassword !== "" ? newHashedPassword : undefined, }, @@ -124,11 +124,11 @@ export default async function updateUser( const { whitelistedUsers, password, ...userInfo } = updatedUser; // If user.whitelistedUsers is not provided, we will assume the whitelistedUsers should be removed - const newWhitelistedUsernames: string[] = user.whitelistedUsers || []; + const newWhitelistedUsernames: string[] = data.whitelistedUsers || []; // Get the current whitelisted usernames const currentWhitelistedUsernames: string[] = whitelistedUsers.map( - (user) => user.username + (data) => data.username ); // Find the usernames to be deleted (present in current but not in new) @@ -164,17 +164,17 @@ export default async function updateUser( const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY; - if (STRIPE_SECRET_KEY && emailEnabled && sessionUser.email !== user.email) + if (STRIPE_SECRET_KEY && emailEnabled && sessionUser.email !== data.email) await updateCustomerEmail( STRIPE_SECRET_KEY, sessionUser.email, - user.email as string + data.email as string ); const response: Omit = { ...userInfo, whitelistedUsers: newWhitelistedUsernames, - profilePic: `/api/avatar/${userInfo.id}?${Date.now()}`, + profilePic: `/api/v1/avatar/${userInfo.id}?${Date.now()}`, }; return { response, status: 200 }; diff --git a/lib/api/getPermission.ts b/lib/api/getPermission.ts index f76b470..3d1b2bc 100644 --- a/lib/api/getPermission.ts +++ b/lib/api/getPermission.ts @@ -1,24 +1,30 @@ import { prisma } from "@/lib/api/db"; -export default async function getPermission( - userId: number, - collectionId: number, - linkId?: number -) { +type Props = { + userId: number; + collectionId?: number; + linkId?: number; +}; + +export default async function getPermission({ + userId, + collectionId, + linkId, +}: Props) { if (linkId) { - const link = await prisma.link.findUnique({ + const check = await prisma.collection.findFirst({ where: { - id: linkId, - }, - include: { - collection: { - include: { members: true }, + links: { + some: { + id: linkId, + }, }, }, + include: { members: true }, }); - return link; - } else { + return check; + } else if (collectionId) { const check = await prisma.collection.findFirst({ where: { AND: { diff --git a/lib/client/addMemberToCollection.ts b/lib/client/addMemberToCollection.ts index 630c303..af0db51 100644 --- a/lib/client/addMemberToCollection.ts +++ b/lib/client/addMemberToCollection.ts @@ -22,9 +22,7 @@ const addMemberToCollection = async ( memberUsername.trim().toLowerCase() !== ownerUsername.toLowerCase() ) { // Lookup, get data/err, list ... - const user = await getPublicUserData({ - username: memberUsername.trim().toLowerCase(), - }); + const user = await getPublicUserData(memberUsername.trim().toLowerCase()); if (user.username) { setMember({ diff --git a/lib/client/getPublicCollectionData.ts b/lib/client/getPublicCollectionData.ts index d997eb7..b86c173 100644 --- a/lib/client/getPublicCollectionData.ts +++ b/lib/client/getPublicCollectionData.ts @@ -17,7 +17,7 @@ const getPublicCollectionData = async ( const encodedData = encodeURIComponent(JSON.stringify(requestBody)); const res = await fetch( - "/api/public/collections?body=" + encodeURIComponent(encodedData) + "/api/v1/public/collections?body=" + encodeURIComponent(encodedData) ); const data = await res.json(); diff --git a/lib/client/getPublicUserData.ts b/lib/client/getPublicUserData.ts index 4c65da3..20bd1e9 100644 --- a/lib/client/getPublicUserData.ts +++ b/lib/client/getPublicUserData.ts @@ -1,17 +1,7 @@ import { toast } from "react-hot-toast"; -export default async function getPublicUserData({ - username, - id, -}: { - username?: string; - id?: number; -}) { - const response = await fetch( - `/api/users?id=${id}&${ - username ? `username=${username?.toLowerCase()}` : undefined - }` - ); +export default async function getPublicUserData(id: number | string) { + const response = await fetch(`/api/v1/users/${id}`); const data = await response.json(); diff --git a/pages/_app.tsx b/pages/_app.tsx index f5966b4..69d8b6f 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -22,7 +22,7 @@ export default function App({ }, []); return ( - + Linkwarden diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts deleted file mode 100644 index cd310bf..0000000 --- a/pages/api/users/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -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"; -import updateUser from "@/lib/api/controllers/users/updateUser"; - -export default async function users(req: NextApiRequest, res: NextApiResponse) { - const session = await getServerSession(req, res, authOptions); - - if (!session?.user.id) { - return res.status(401).json({ response: "You must be logged in." }); - } else if (session?.user?.isSubscriber === false) - res.status(401).json({ - response: - "You are not a subscriber, feel free to reach out to us at support@linkwarden.app in case of any issues.", - }); - - const lookupUsername = (req.query.username as string) || undefined; - const lookupId = Number(req.query.id) || undefined; - const isSelf = - session.user.username === lookupUsername || session.user.id === lookupId - ? true - : false; - - if (req.method === "GET") { - const users = await getUsers({ - params: { - lookupUsername, - lookupId, - }, - isSelf, - username: session.user.username, - }); - return res.status(users.status).json({ response: users.response }); - } else if (req.method === "PUT") { - const updated = await updateUser(req.body, session.user); - return res.status(updated.status).json({ response: updated.response }); - } -} diff --git a/pages/api/archives/[...params].ts b/pages/api/v1/archives/[...params].ts similarity index 86% rename from pages/api/archives/[...params].ts rename to pages/api/v1/archives/[...params].ts index 3e659a6..814e5ee 100644 --- a/pages/api/archives/[...params].ts +++ b/pages/api/v1/archives/[...params].ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { getServerSession } from "next-auth/next"; -import { authOptions } from "pages/api/auth/[...nextauth]"; +import { authOptions } from "@/pages/api/v1/auth/[...nextauth]"; import getPermission from "@/lib/api/getPermission"; import readFile from "@/lib/api/storage/readFile"; @@ -21,10 +21,10 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) { "You are not a subscriber, feel free to reach out to us at support@linkwarden.app in case of any issues.", }); - const collectionIsAccessible = await getPermission( - session.user.id, - Number(collectionId) - ); + const collectionIsAccessible = await getPermission({ + userId: session.user.id, + collectionId: Number(collectionId), + }); if (!collectionIsAccessible) return res diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/v1/auth/[...nextauth].ts similarity index 96% rename from pages/api/auth/[...nextauth].ts rename to pages/api/v1/auth/[...nextauth].ts index 42f9b85..1bde92d 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/v1/auth/[...nextauth].ts @@ -88,8 +88,7 @@ export const authOptions: AuthOptions = { return session; }, - // Using the `...rest` parameter to be able to narrow down the type based on `trigger` - async jwt({ token, trigger, session, user }) { + async jwt({ token, trigger, user }) { const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY; const NEXT_PUBLIC_TRIAL_PERIOD_DAYS = diff --git a/pages/api/avatar/[id].ts b/pages/api/v1/avatar/[id].ts similarity index 96% rename from pages/api/avatar/[id].ts rename to pages/api/v1/avatar/[id].ts index a006dbf..c92b98b 100644 --- a/pages/api/avatar/[id].ts +++ b/pages/api/v1/avatar/[id].ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { getServerSession } from "next-auth/next"; -import { authOptions } from "pages/api/auth/[...nextauth]"; +import { authOptions } from "@/pages/api/v1/auth/[...nextauth]"; import { prisma } from "@/lib/api/db"; import readFile from "@/lib/api/storage/readFile"; diff --git a/pages/api/v1/collections/[id].ts b/pages/api/v1/collections/[id].ts new file mode 100644 index 0000000..3ac289c --- /dev/null +++ b/pages/api/v1/collections/[id].ts @@ -0,0 +1,35 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/pages/api/v1/auth/[...nextauth]"; +import updateCollectionById from "@/lib/api/controllers/collections/collectionId/updateCollectionById"; +import deleteCollectionById from "@/lib/api/controllers/collections/collectionId/deleteCollectionById"; + +export default async function collections( + req: NextApiRequest, + res: NextApiResponse +) { + const session = await getServerSession(req, res, authOptions); + + if (!session?.user?.id) { + return res.status(401).json({ response: "You must be logged in." }); + } else if (session?.user?.isSubscriber === false) + res.status(401).json({ + response: + "You are not a subscriber, feel free to reach out to us at support@linkwarden.app in case of any issues.", + }); + + if (req.method === "PUT") { + const updated = await updateCollectionById( + session.user.id, + Number(req.query.id) as number, + req.body + ); + return res.status(updated.status).json({ response: updated.response }); + } else if (req.method === "DELETE") { + const deleted = await deleteCollectionById( + session.user.id, + Number(req.query.id) as number + ); + return res.status(deleted.status).json({ response: deleted.response }); + } +} diff --git a/pages/api/collections/index.ts b/pages/api/v1/collections/index.ts similarity index 65% rename from pages/api/collections/index.ts rename to pages/api/v1/collections/index.ts index 2132add..73cb45e 100644 --- a/pages/api/collections/index.ts +++ b/pages/api/v1/collections/index.ts @@ -1,10 +1,8 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { getServerSession } from "next-auth/next"; -import { authOptions } from "@/pages/api/auth/[...nextauth]"; +import { authOptions } from "@/pages/api/v1/auth/[...nextauth]"; import getCollections from "@/lib/api/controllers/collections/getCollections"; import postCollection from "@/lib/api/controllers/collections/postCollection"; -import updateCollection from "@/lib/api/controllers/collections/updateCollection"; -import deleteCollection from "@/lib/api/controllers/collections/deleteCollection"; export default async function collections( req: NextApiRequest, @@ -30,11 +28,5 @@ export default async function collections( return res .status(newCollection.status) .json({ response: newCollection.response }); - } else if (req.method === "PUT") { - const updated = await updateCollection(req.body, session.user.id); - return res.status(updated.status).json({ response: updated.response }); - } else if (req.method === "DELETE") { - const deleted = await deleteCollection(req.body, session.user.id); - return res.status(deleted.status).json({ response: deleted.response }); } } diff --git a/pages/api/v1/getToken.ts b/pages/api/v1/getToken.ts new file mode 100644 index 0000000..c767075 --- /dev/null +++ b/pages/api/v1/getToken.ts @@ -0,0 +1,16 @@ +// For future... +// import { getToken } from "next-auth/jwt"; + +// export default async (req, res) => { +// // If you don't have NEXTAUTH_SECRET set, you will have to pass your secret as `secret` to `getToken` +// console.log({ req }); +// const token = await getToken({ req, raw: true }); +// if (token) { +// // Signed in +// console.log("JSON Web Token", JSON.stringify(token, null, 2)); +// } else { +// // Not Signed in +// res.status(401); +// } +// res.end(); +// }; diff --git a/pages/api/v1/links/[id].ts b/pages/api/v1/links/[id].ts new file mode 100644 index 0000000..9847f2a --- /dev/null +++ b/pages/api/v1/links/[id].ts @@ -0,0 +1,33 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/pages/api/v1/auth/[...nextauth]"; +import deleteLinkById from "@/lib/api/controllers/links/linkId/deleteLinkById"; +import updateLinkById from "@/lib/api/controllers/links/linkId/updateLinkById"; + +export default async function links(req: NextApiRequest, res: NextApiResponse) { + const session = await getServerSession(req, res, authOptions); + + if (!session?.user?.id) { + return res.status(401).json({ response: "You must be logged in." }); + } else if (session?.user?.isSubscriber === false) + res.status(401).json({ + response: + "You are not a subscriber, feel free to reach out to us at support@linkwarden.app in case of any issues.", + }); + + if (req.method === "PUT") { + const updated = await updateLinkById( + session.user.id, + Number(req.query.id), + req.body + ); + return res.status(updated.status).json({ + response: updated.response, + }); + } else if (req.method === "DELETE") { + const deleted = await deleteLinkById(session.user.id, Number(req.query.id)); + return res.status(deleted.status).json({ + response: deleted.response, + }); + } +} diff --git a/pages/api/links/index.ts b/pages/api/v1/links/index.ts similarity index 65% rename from pages/api/links/index.ts rename to pages/api/v1/links/index.ts index eb5be9d..6eab9af 100644 --- a/pages/api/links/index.ts +++ b/pages/api/v1/links/index.ts @@ -1,10 +1,8 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { getServerSession } from "next-auth/next"; -import { authOptions } from "@/pages/api/auth/[...nextauth]"; +import { authOptions } from "@/pages/api/v1/auth/[...nextauth]"; import getLinks from "@/lib/api/controllers/links/getLinks"; import postLink from "@/lib/api/controllers/links/postLink"; -import deleteLink from "@/lib/api/controllers/links/deleteLink"; -import updateLink from "@/lib/api/controllers/links/updateLink"; export default async function links(req: NextApiRequest, res: NextApiResponse) { const session = await getServerSession(req, res, authOptions); @@ -25,15 +23,5 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) { return res.status(newlink.status).json({ response: newlink.response, }); - } else if (req.method === "PUT") { - const updated = await updateLink(req.body, session.user.id); - return res.status(updated.status).json({ - response: updated.response, - }); - } else if (req.method === "DELETE") { - const deleted = await deleteLink(req.body, session.user.id); - return res.status(deleted.status).json({ - response: deleted.response, - }); } } diff --git a/pages/api/migration/index.ts b/pages/api/v1/migration/index.ts similarity index 96% rename from pages/api/migration/index.ts rename to pages/api/v1/migration/index.ts index aeeb499..a881e48 100644 --- a/pages/api/migration/index.ts +++ b/pages/api/v1/migration/index.ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { getServerSession } from "next-auth/next"; -import { authOptions } from "@/pages/api/auth/[...nextauth]"; +import { authOptions } from "@/pages/api/v1/auth/[...nextauth]"; import exportData from "@/lib/api/controllers/migration/exportData"; import importFromHTMLFile from "@/lib/api/controllers/migration/importFromHTMLFile"; import importFromLinkwarden from "@/lib/api/controllers/migration/importFromLinkwarden"; diff --git a/pages/api/payment/index.ts b/pages/api/v1/payment/index.ts similarity index 95% rename from pages/api/payment/index.ts rename to pages/api/v1/payment/index.ts index e62ac4a..23e0673 100644 --- a/pages/api/payment/index.ts +++ b/pages/api/v1/payment/index.ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { getServerSession } from "next-auth/next"; -import { authOptions } from "@/pages/api/auth/[...nextauth]"; +import { authOptions } from "@/pages/api/v1/auth/[...nextauth]"; import paymentCheckout from "@/lib/api/paymentCheckout"; import { Plan } from "@/types/global"; diff --git a/pages/api/public/collections.ts b/pages/api/v1/public/collections.ts similarity index 100% rename from pages/api/public/collections.ts rename to pages/api/v1/public/collections.ts diff --git a/pages/api/v1/tags/[id].ts b/pages/api/v1/tags/[id].ts new file mode 100644 index 0000000..22b355f --- /dev/null +++ b/pages/api/v1/tags/[id].ts @@ -0,0 +1,23 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/pages/api/v1/auth/[...nextauth]"; +import updateTag from "@/lib/api/controllers/tags/tagId/updeteTagById"; + +export default async function tags(req: NextApiRequest, res: NextApiResponse) { + const session = await getServerSession(req, res, authOptions); + + if (!session?.user?.username) { + return res.status(401).json({ response: "You must be logged in." }); + } else if (session?.user?.isSubscriber === false) + res.status(401).json({ + response: + "You are not a subscriber, feel free to reach out to us at support@linkwarden.app in case of any issues.", + }); + + const tagId = Number(req.query.id); + + if (req.method === "PUT") { + const tags = await updateTag(session.user.id, tagId, req.body); + return res.status(tags.status).json({ response: tags.response }); + } +} diff --git a/pages/api/tags/index.ts b/pages/api/v1/tags/index.ts similarity index 92% rename from pages/api/tags/index.ts rename to pages/api/v1/tags/index.ts index 5841ace..efaf7b2 100644 --- a/pages/api/tags/index.ts +++ b/pages/api/v1/tags/index.ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { getServerSession } from "next-auth/next"; -import { authOptions } from "@/pages/api/auth/[...nextauth]"; +import { authOptions } from "@/pages/api/v1/auth/[...nextauth]"; import getTags from "@/lib/api/controllers/tags/getTags"; export default async function tags(req: NextApiRequest, res: NextApiResponse) { diff --git a/pages/api/v1/users/[id].ts b/pages/api/v1/users/[id].ts new file mode 100644 index 0000000..112ceff --- /dev/null +++ b/pages/api/v1/users/[id].ts @@ -0,0 +1,40 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/pages/api/v1/auth/[...nextauth]"; +import getUserById from "@/lib/api/controllers/users/userId/getUserById"; +import getPublicUserById from "@/lib/api/controllers/users/userId/getPublicUserById"; +import updateUserById from "@/lib/api/controllers/users/userId/updateUserById"; + +export default async function users(req: NextApiRequest, res: NextApiResponse) { + const session = await getServerSession(req, res, authOptions); + const userId = session?.user.id; + const username = session?.user.username; + + const lookupId = req.query.id as string; + const isSelf = + userId === Number(lookupId) || username === lookupId ? true : false; + + // Check if "lookupId" is the user "id" or their "username" + const isId = lookupId.split("").every((e) => Number.isInteger(parseInt(e))); + + if (req.method === "GET" && !isSelf) { + const users = await getPublicUserById(lookupId, isId, username); + return res.status(users.status).json({ response: users.response }); + } + + if (!userId) { + return res.status(401).json({ response: "You must be logged in." }); + } else if (session?.user?.isSubscriber === false) + res.status(401).json({ + response: + "You are not a subscriber, feel free to reach out to us at support@linkwarden.app in case of any issues.", + }); + + if (req.method === "GET") { + const users = await getUserById(session.user.id); + return res.status(users.status).json({ response: users.response }); + } else if (req.method === "PUT") { + const updated = await updateUserById(session.user, req.body); + return res.status(updated.status).json({ response: updated.response }); + } +} diff --git a/pages/api/v1/users/index.ts b/pages/api/v1/users/index.ts new file mode 100644 index 0000000..3af7bf8 --- /dev/null +++ b/pages/api/v1/users/index.ts @@ -0,0 +1,9 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import postUser from "@/lib/api/controllers/users/postUser"; + +export default async function users(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "POST") { + const response = await postUser(req, res); + return response; + } +} diff --git a/pages/collections/[id].tsx b/pages/collections/[id].tsx index d3f6f56..632ab4e 100644 --- a/pages/collections/[id].tsx +++ b/pages/collections/[id].tsx @@ -104,7 +104,7 @@ export default function Index() { return ( ); diff --git a/pages/register.tsx b/pages/register.tsx index 75ba525..c279e97 100644 --- a/pages/register.tsx +++ b/pages/register.tsx @@ -59,7 +59,7 @@ export default function Register() { const load = toast.loading("Creating Account..."); - const response = await fetch("/api/auth/register", { + const response = await fetch("/api/v1/users", { body: JSON.stringify(request), headers: { "Content-Type": "application/json", diff --git a/pages/settings/account.tsx b/pages/settings/account.tsx index 9cd52b6..1ee8c2a 100644 --- a/pages/settings/account.tsx +++ b/pages/settings/account.tsx @@ -128,7 +128,7 @@ export default function Account() { data: request, }; - const response = await fetch("/api/migration", { + const response = await fetch("/api/v1/migration", { method: "POST", body: JSON.stringify(body), }); @@ -333,7 +333,7 @@ export default function Account() {

Download your data instantly.

- +
Export Data
diff --git a/pages/subscribe.tsx b/pages/subscribe.tsx index b8b478a..56ef8f6 100644 --- a/pages/subscribe.tsx +++ b/pages/subscribe.tsx @@ -20,7 +20,7 @@ export default function Subscribe() { const redirectionToast = toast.loading("Redirecting to Stripe..."); - const res = await fetch("/api/payment?plan=" + plan); + const res = await fetch("/api/v1/payment?plan=" + plan); const data = await res.json(); router.push(data.response); diff --git a/pages/tags/[id].tsx b/pages/tags/[id].tsx index 4acd069..1804a75 100644 --- a/pages/tags/[id].tsx +++ b/pages/tags/[id].tsx @@ -1,25 +1,38 @@ import LinkCard from "@/components/LinkCard"; import useLinkStore from "@/store/links"; -import { faHashtag, faSort } from "@fortawesome/free-solid-svg-icons"; +import { + faCheck, + faEllipsis, + faHashtag, + faSort, + faXmark, +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; +import { FormEvent, useEffect, useState } from "react"; import MainLayout from "@/layouts/MainLayout"; import { Tag } from "@prisma/client"; import useTagStore from "@/store/tags"; import SortDropdown from "@/components/SortDropdown"; import { Sort } from "@/types/global"; import useLinks from "@/hooks/useLinks"; +import Dropdown from "@/components/Dropdown"; +import { toast } from "react-hot-toast"; export default function Index() { const router = useRouter(); const { links } = useLinkStore(); - const { tags } = useTagStore(); + const { tags, updateTag } = useTagStore(); const [sortDropdown, setSortDropdown] = useState(false); const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); + const [expandDropdown, setExpandDropdown] = useState(false); + + const [renameTag, setRenameTag] = useState(false); + const [newTagName, setNewTagName] = useState(); + const [activeTag, setActiveTag] = useState(); useLinks({ tagId: Number(router.query.id), sort: sortBy }); @@ -28,19 +41,130 @@ export default function Index() { setActiveTag(tags.find((e) => e.id === Number(router.query.id))); }, [router, tags]); + useEffect(() => { + setNewTagName(activeTag?.name); + }, [activeTag]); + + const [submitLoader, setSubmitLoader] = useState(false); + + const cancelUpdateTag = async () => { + setNewTagName(activeTag?.name); + setRenameTag(false); + }; + + const submit = async (e?: FormEvent) => { + e?.preventDefault(); + + if (activeTag?.name === newTagName) return setRenameTag(false); + else if (newTagName === "") { + return cancelUpdateTag(); + } + + setSubmitLoader(true); + + const load = toast.loading("Applying..."); + + let response; + + if (activeTag && newTagName) + response = await updateTag({ + ...activeTag, + name: newTagName, + }); + + toast.dismiss(load); + + if (response?.ok) { + toast.success("Tag Renamed!"); + } else toast.error(response?.data as string); + setSubmitLoader(false); + setRenameTag(false); + }; + return (
-
+
-

- {activeTag?.name} -

+ {renameTag ? ( + <> +
+ setNewTagName(e.target.value)} + /> +
submit()} + id="expand-dropdown" + className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1" + > + +
+
cancelUpdateTag()} + id="expand-dropdown" + className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1" + > + +
+
+ + ) : ( + <> +

+ {activeTag?.name} +

+
+
setExpandDropdown(!expandDropdown)} + id="expand-dropdown" + className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1" + > + +
+ + {expandDropdown ? ( + { + setRenameTag(true); + setExpandDropdown(false); + }, + }, + ]} + onClickOutside={(e: Event) => { + const target = e.target as HTMLInputElement; + if (target.id !== "expand-dropdown") + setExpandDropdown(false); + }} + className="absolute top-8 left-0 w-36" + /> + ) : null} +
+ + )}
diff --git a/store/account.ts b/store/account.ts index 4b7e9b6..c57d71e 100644 --- a/store/account.ts +++ b/store/account.ts @@ -15,16 +15,16 @@ type AccountStore = { const useAccountStore = create()((set) => ({ account: {} as AccountSettings, setAccount: async (id) => { - const response = await fetch(`/api/users?id=${id}`); + const response = await fetch(`/api/v1/users/${id}`); const data = await response.json(); - const profilePic = `/api/avatar/${data.response.id}?${Date.now()}`; + const profilePic = `/api/v1/avatar/${data.response.id}?${Date.now()}`; if (response.ok) set({ account: { ...data.response, profilePic } }); }, updateAccount: async (user) => { - const response = await fetch("/api/users", { + const response = await fetch(`/api/v1/users/${user.id}`, { method: "PUT", body: JSON.stringify(user), headers: { diff --git a/store/collections.ts b/store/collections.ts index 0fade5f..5c78b52 100644 --- a/store/collections.ts +++ b/store/collections.ts @@ -22,14 +22,14 @@ type CollectionStore = { const useCollectionStore = create()((set) => ({ collections: [], setCollections: async () => { - const response = await fetch("/api/collections"); + const response = await fetch("/api/v1/collections"); const data = await response.json(); if (response.ok) set({ collections: data.response }); }, addCollection: async (body) => { - const response = await fetch("/api/collections", { + const response = await fetch("/api/v1/collections", { body: JSON.stringify(body), headers: { "Content-Type": "application/json", @@ -47,7 +47,7 @@ const useCollectionStore = create()((set) => ({ return { ok: response.ok, data: data.response }; }, updateCollection: async (collection) => { - const response = await fetch("/api/collections", { + const response = await fetch(`/api/v1/collections/${collection.id}`, { body: JSON.stringify(collection), headers: { "Content-Type": "application/json", @@ -66,9 +66,8 @@ const useCollectionStore = create()((set) => ({ return { ok: response.ok, data: data.response }; }, - removeCollection: async (id) => { - const response = await fetch("/api/collections", { - body: JSON.stringify({ id }), + removeCollection: async (collectionId) => { + const response = await fetch(`/api/v1/collections/${collectionId}`, { headers: { "Content-Type": "application/json", }, @@ -79,7 +78,7 @@ const useCollectionStore = create()((set) => ({ if (response.ok) { set((state) => ({ - collections: state.collections.filter((e) => e.id !== id), + collections: state.collections.filter((e) => e.id !== collectionId), })); useTagStore.getState().setTags(); } diff --git a/store/links.ts b/store/links.ts index ff25603..d6a13e4 100644 --- a/store/links.ts +++ b/store/links.ts @@ -20,9 +20,7 @@ type LinkStore = { updateLink: ( link: LinkIncludingShortenedCollectionAndTags ) => Promise; - removeLink: ( - link: LinkIncludingShortenedCollectionAndTags - ) => Promise; + removeLink: (linkId: number) => Promise; resetLinks: () => void; }; @@ -38,7 +36,7 @@ const useLinkStore = create()((set) => ({ })); }, addLink: async (body) => { - const response = await fetch("/api/links", { + const response = await fetch("/api/v1/links", { body: JSON.stringify(body), headers: { "Content-Type": "application/json", @@ -59,7 +57,7 @@ const useLinkStore = create()((set) => ({ return { ok: response.ok, data: data.response }; }, updateLink: async (link) => { - const response = await fetch("/api/links", { + const response = await fetch(`/api/v1/links/${link.id}`, { body: JSON.stringify(link), headers: { "Content-Type": "application/json", @@ -81,9 +79,8 @@ const useLinkStore = create()((set) => ({ return { ok: response.ok, data: data.response }; }, - removeLink: async (link) => { - const response = await fetch("/api/links", { - body: JSON.stringify(link), + removeLink: async (linkId) => { + const response = await fetch(`/api/v1/links/${linkId}`, { headers: { "Content-Type": "application/json", }, @@ -94,7 +91,7 @@ const useLinkStore = create()((set) => ({ if (response.ok) { set((state) => ({ - links: state.links.filter((e) => e.id !== link.id), + links: state.links.filter((e) => e.id !== linkId), })); useTagStore.getState().setTags(); } diff --git a/store/tags.ts b/store/tags.ts index f833aa0..26552bf 100644 --- a/store/tags.ts +++ b/store/tags.ts @@ -1,20 +1,47 @@ import { create } from "zustand"; import { Tag } from "@prisma/client"; +type ResponseObject = { + ok: boolean; + data: object | string; +}; + type TagStore = { tags: Tag[]; setTags: () => void; + updateTag: (tag: Tag) => Promise; }; const useTagStore = create()((set) => ({ tags: [], setTags: async () => { - const response = await fetch("/api/tags"); + const response = await fetch("/api/v1/tags"); const data = await response.json(); if (response.ok) set({ tags: data.response }); }, + updateTag: async (tag) => { + const response = await fetch(`/api/v1/tags/${tag.id}`, { + body: JSON.stringify(tag), + headers: { + "Content-Type": "application/json", + }, + method: "PUT", + }); + + const data = await response.json(); + + if (response.ok) { + set((state) => ({ + tags: state.tags.map((e) => + e.id === data.response.id ? data.response : e + ), + })); + } + + return { ok: response.ok, data: data.response }; + }, })); export default useTagStore;