diff --git a/lib/api/controllers/collections/postCollection.ts b/lib/api/controllers/collections/postCollection.ts index c09291f..c6ae03e 100644 --- a/lib/api/controllers/collections/postCollection.ts +++ b/lib/api/controllers/collections/postCollection.ts @@ -44,11 +44,6 @@ export default async function postCollection( const newCollection = await prisma.collection.create({ data: { - owner: { - connect: { - id: userId, - }, - }, name: collection.name.trim(), description: collection.description, color: collection.color, @@ -61,6 +56,16 @@ export default async function postCollection( }, } : undefined, + owner: { + connect: { + id: userId, + }, + }, + createdBy: { + connect: { + id: userId, + }, + }, }, include: { _count: { diff --git a/lib/api/controllers/links/postLink.ts b/lib/api/controllers/links/postLink.ts index b620ee1..faf87ce 100644 --- a/lib/api/controllers/links/postLink.ts +++ b/lib/api/controllers/links/postLink.ts @@ -6,8 +6,7 @@ import { PostLinkSchema, PostLinkSchemaType, } from "@/lib/shared/schemaValidation"; - -const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000; +import { hasPassedLimit } from "../../verifyCapacity"; export default async function postLink( body: PostLinkSchemaType, @@ -59,19 +58,14 @@ export default async function postLink( }; } - const numberOfLinksTheUserHas = await prisma.link.count({ - where: { - collection: { - ownerId: linkCollection.ownerId, - }, - }, - }); + const hasTooManyLinks = await hasPassedLimit(userId, 1); - if (numberOfLinksTheUserHas > MAX_LINKS_PER_USER) + if (hasTooManyLinks) { return { - response: `Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.`, + response: `Your subscription have reached the maximum number of links allowed.`, status: 400, }; + } const { title, headers } = await fetchTitleAndHeaders(link.url || ""); @@ -98,6 +92,11 @@ export default async function postLink( name, description: link.description, type: linkType, + createdBy: { + connect: { + id: userId, + }, + }, collection: { connect: { id: linkCollection.id, diff --git a/lib/api/controllers/migration/importFromHTMLFile.ts b/lib/api/controllers/migration/importFromHTMLFile.ts index 2103806..b3b3913 100644 --- a/lib/api/controllers/migration/importFromHTMLFile.ts +++ b/lib/api/controllers/migration/importFromHTMLFile.ts @@ -2,8 +2,7 @@ import { prisma } from "@/lib/api/db"; import createFolder from "@/lib/api/storage/createFolder"; import { JSDOM } from "jsdom"; import { parse, Node, Element, TextNode } from "himalaya"; - -const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000; +import { hasPassedLimit } from "../../verifyCapacity"; export default async function importFromHTMLFile( userId: number, @@ -20,19 +19,14 @@ export default async function importFromHTMLFile( const bookmarks = document.querySelectorAll("A"); const totalImports = bookmarks.length; - const numberOfLinksTheUserHas = await prisma.link.count({ - where: { - collection: { - ownerId: userId, - }, - }, - }); + const hasTooManyLinks = await hasPassedLimit(userId, totalImports); - if (totalImports + numberOfLinksTheUserHas > MAX_LINKS_PER_USER) + if (hasTooManyLinks) { return { - response: `Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.`, + response: `Your subscription have reached the maximum number of links allowed.`, status: 400, }; + } const jsonData = parse(document.documentElement.outerHTML); @@ -183,6 +177,11 @@ const createCollection = async ( id: userId, }, }, + createdBy: { + connect: { + id: userId, + }, + }, }, }); @@ -222,28 +221,27 @@ const createLink = async ( url, description, collectionId, + createdById: userId, tags: tags && tags[0] ? { connectOrCreate: tags.map((tag: string) => { - return ( - { - where: { - name_ownerId: { - name: tag.trim(), - ownerId: userId, - }, - }, - create: { + return { + where: { + name_ownerId: { name: tag.trim(), - owner: { - connect: { - id: userId, - }, + ownerId: userId, + }, + }, + create: { + name: tag.trim(), + owner: { + connect: { + id: userId, }, }, - } || undefined - ); + }, + }; }), } : undefined, diff --git a/lib/api/controllers/migration/importFromLinkwarden.ts b/lib/api/controllers/migration/importFromLinkwarden.ts index ed948d3..2ef1b52 100644 --- a/lib/api/controllers/migration/importFromLinkwarden.ts +++ b/lib/api/controllers/migration/importFromLinkwarden.ts @@ -1,8 +1,7 @@ import { prisma } from "@/lib/api/db"; import { Backup } from "@/types/global"; import createFolder from "@/lib/api/storage/createFolder"; - -const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000; +import { hasPassedLimit } from "../../verifyCapacity"; export default async function importFromLinkwarden( userId: number, @@ -16,19 +15,14 @@ export default async function importFromLinkwarden( totalImports += collection.links.length; }); - const numberOfLinksTheUserHas = await prisma.link.count({ - where: { - collection: { - ownerId: userId, - }, - }, - }); + const hasTooManyLinks = await hasPassedLimit(userId, totalImports); - if (totalImports + numberOfLinksTheUserHas > MAX_LINKS_PER_USER) + if (hasTooManyLinks) { return { - response: `Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.`, + response: `Your subscription have reached the maximum number of links allowed.`, status: 400, }; + } await prisma .$transaction( @@ -47,6 +41,11 @@ export default async function importFromLinkwarden( name: e.name?.trim().slice(0, 254), description: e.description?.trim().slice(0, 254), color: e.color?.trim().slice(0, 50), + createdBy: { + connect: { + id: userId, + }, + }, }, }); @@ -72,6 +71,11 @@ export default async function importFromLinkwarden( id: newCollection.id, }, }, + createdBy: { + connect: { + id: userId, + }, + }, // Import Tags tags: { connectOrCreate: link.tags.map((tag) => ({ diff --git a/lib/api/controllers/migration/importFromWallabag.ts b/lib/api/controllers/migration/importFromWallabag.ts index a1f6a0b..c82df85 100644 --- a/lib/api/controllers/migration/importFromWallabag.ts +++ b/lib/api/controllers/migration/importFromWallabag.ts @@ -1,7 +1,6 @@ import { prisma } from "@/lib/api/db"; import createFolder from "@/lib/api/storage/createFolder"; - -const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000; +import { hasPassedLimit } from "../../verifyCapacity"; type WallabagBackup = { is_archived: number; @@ -36,19 +35,14 @@ export default async function importFromWallabag( let totalImports = backup.length; - const numberOfLinksTheUserHas = await prisma.link.count({ - where: { - collection: { - ownerId: userId, - }, - }, - }); + const hasTooManyLinks = await hasPassedLimit(userId, totalImports); - if (totalImports + numberOfLinksTheUserHas > MAX_LINKS_PER_USER) + if (hasTooManyLinks) { return { - response: `Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.`, + response: `Your subscription have reached the maximum number of links allowed.`, status: 400, }; + } await prisma .$transaction( @@ -61,6 +55,11 @@ export default async function importFromWallabag( }, }, name: "Imports", + createdBy: { + connect: { + id: userId, + }, + }, }, }); @@ -89,6 +88,11 @@ export default async function importFromWallabag( id: newCollection.id, }, }, + createdBy: { + connect: { + id: userId, + }, + }, tags: link.tags && link.tags[0] ? { diff --git a/lib/api/setLinkCollection.ts b/lib/api/setLinkCollection.ts index c26de99..f5270a7 100644 --- a/lib/api/setLinkCollection.ts +++ b/lib/api/setLinkCollection.ts @@ -45,6 +45,7 @@ const setLinkCollection = async (link: PostLinkSchemaType, userId: number) => { data: { name: link.collection.name.trim(), ownerId: userId, + createdById: userId, }, }); @@ -78,6 +79,7 @@ const setLinkCollection = async (link: PostLinkSchemaType, userId: number) => { name: "Unorganized", ownerId: userId, parentId: null, + createdById: userId, }, }); } diff --git a/lib/api/verifyCapacity.ts b/lib/api/verifyCapacity.ts new file mode 100644 index 0000000..bf41b42 --- /dev/null +++ b/lib/api/verifyCapacity.ts @@ -0,0 +1,68 @@ +import { prisma } from "./db"; + +const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000; +const stripeEnabled = process.env.NEXT_PUBLIC_STRIPE === "true"; + +export const hasPassedLimit = async ( + userId: number, + numberOfImports: number +) => { + if (!stripeEnabled) { + return false; + } + + const user = await prisma.user.findUnique({ + where: { id: userId }, + include: { + parentSubscription: true, + subscriptions: true, + }, + }); + + if (!user) { + return true; + } + + if ( + user.parentSubscription || + (user.subscriptions && user.subscriptions?.quantity > 1) + ) { + const subscription = user.parentSubscription || user.subscriptions; + + if (!subscription) { + return true; + } + + // Calculate the total allowed links for the organization + const totalCapacity = subscription.quantity * MAX_LINKS_PER_USER; + + const totalLinks = await prisma.link.count({ + where: { + createdBy: { + OR: [ + { + parentSubscriptionId: subscription.id || undefined, + }, + { + subscriptions: { + id: subscription.id || undefined, + }, + }, + ], + }, + }, + }); + + return totalCapacity - (numberOfImports + totalLinks) < 0; + } else { + const totalLinks = await prisma.link.count({ + where: { + createdBy: { + id: userId, + }, + }, + }); + + return MAX_LINKS_PER_USER - (numberOfImports + totalLinks) < 0; + } +}; diff --git a/prisma/migrations/20241026161909_assign_createdby_to_collection_owners_and_make_field_required/migration.sql b/prisma/migrations/20241026161909_assign_createdby_to_collection_owners_and_make_field_required/migration.sql new file mode 100644 index 0000000..08748b2 --- /dev/null +++ b/prisma/migrations/20241026161909_assign_createdby_to_collection_owners_and_make_field_required/migration.sql @@ -0,0 +1,37 @@ +/* + Warnings: + + - Made the column `createdById` on table `Collection` required. This step will fail if there are existing NULL values in that column. + - Made the column `createdById` on table `Link` required. This step will fail if there are existing NULL values in that column. + +*/ + +-- Update the Link table to set the createdBy based on the Collection's ownerId. +UPDATE "Link" +SET "createdById" = ( + SELECT "ownerId" + FROM "Collection" + WHERE "Collection"."id" = "Link"."collectionId" +); + +-- Set createdBy to ownerId for existing records +UPDATE "Collection" +SET "createdById" = "ownerId"; + +-- DropForeignKey +ALTER TABLE "Collection" DROP CONSTRAINT "Collection_createdById_fkey"; + +-- DropForeignKey +ALTER TABLE "Link" DROP CONSTRAINT "Link_createdById_fkey"; + +-- AlterTable +ALTER TABLE "Collection" ALTER COLUMN "createdById" SET NOT NULL; + +-- AlterTable +ALTER TABLE "Link" ALTER COLUMN "createdById" SET NOT NULL; + +-- AddForeignKey +ALTER TABLE "Collection" ADD CONSTRAINT "Collection_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Link" ADD CONSTRAINT "Link_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ce4ca12..d06b717 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -115,8 +115,8 @@ model Collection { ownerId Int members UsersAndCollections[] teamId Int? - createdBy User? @relation("CreatedCollections", fields: [createdById], references: [id]) - createdById Int? + createdBy User @relation("CreatedCollections", fields: [createdById], references: [id]) + createdById Int links Link[] createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt @@ -145,8 +145,8 @@ model Link { type String @default("url") description String @default("") pinnedBy User[] @relation("PinnedLinks") - createdBy User? @relation("CreatedLinks", fields: [createdById], references: [id]) - createdById Int? + createdBy User @relation("CreatedLinks", fields: [createdById], references: [id]) + createdById Int collection Collection @relation(fields: [collectionId], references: [id]) collectionId Int tags Tag[]