Skip to content

Commit

Permalink
Merge pull request #408 from mfts/feat/subscription-plan
Browse files Browse the repository at this point in the history
feat: check team limits
  • Loading branch information
mfts authored May 12, 2024
2 parents fc6cd71 + ed8f2dc commit ee39a50
Show file tree
Hide file tree
Showing 13 changed files with 240 additions and 23 deletions.
7 changes: 5 additions & 2 deletions components/datarooms/add-dataroom-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,13 @@ export function AddDataroomModal({ children }: { children?: React.ReactNode }) {
};

// If the team is on a free plan, show the upgrade modal
if (plan === "free") {
if (plan === "free" || plan === "pro") {
if (children) {
return (
<UpgradePlanModal clickedPlan="Pro" trigger={"add_dataroom_overview"}>
<UpgradePlanModal
clickedPlan="Data Rooms"
trigger={"add_dataroom_overview"}
>
{children}
</UpgradePlanModal>
);
Expand Down
40 changes: 40 additions & 0 deletions ee/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
The Papermark Commercial License (the “Commercial License”)
Copyright (c) 2024-present Papermark, Inc

With regard to the Papermark Software:

This software and associated documentation files (the "Software") may only be
used in production, if you (and any entity that you represent) have agreed to,
and are in compliance with, an agreement governing
the use of the Software, as mutually agreed by you and Papermark, Inc ("Papermark"),
and otherwise have a valid Papermark Enterprise Edition subscription ("Commercial Subscription").
Subject to the foregoing sentence, you are free to modify this Software and publish patches to the Software.
You agree that Papermark and/or its licensors (as applicable) retain all right, title and interest in
and to all such modifications and/or patches, and all such modifications and/or
patches may only be used, copied, modified, displayed, distributed, or otherwise
exploited with a valid Commercial Subscription for the correct number of hosts.
Notwithstanding the foregoing, you may copy and modify the Software for development
and testing purposes, without requiring a subscription. You agree that Papermark and/or
its licensors (as applicable) retain all right, title and interest in and to all such
modifications. You are not granted any other rights beyond what is expressly stated herein.
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
and/or sell the Software.

This Commercial License applies only to the part of this Software that is not distributed under
the AGPLv3 license. Any part of this Software distributed under the MIT license or which
is served client-side as an image, font, cascading stylesheet (CSS), file which produces
or is compiled, arranged, augmented, or combined into client-side JavaScript, in whole or
in part, is copyrighted under the AGPLv3 license. The full text of this Commercial License shall
be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

For all third party components incorporated into the Papermark Software, those
components are licensed under the original license provided by the owner of the
applicable component.
32 changes: 32 additions & 0 deletions ee/limits/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth";
import { CustomUser } from "@/lib/types";
import { getLimits } from "@/ee/limits/server";
import { authOptions } from "@/pages/api/auth/[...nextauth]";

export default async function handle(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method === "GET") {
// GET /api/:teamId/limits
const session = await getServerSession(req, res, authOptions);
if (!session) {
return res.status(401).end("Unauthorized");
}

const { teamId } = req.query as { teamId: string };
const userId = (session.user as CustomUser).id;

try {
const limits = await getLimits({ teamId, userId });

return res.status(200).json(limits);
} catch (error) {
return res.status(500).json((error as Error).message);
}
} else {
res.setHeader("Allow", ["GET"]);
return res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
49 changes: 49 additions & 0 deletions ee/limits/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import prisma from "@/lib/prisma";
import { z } from "zod";

export async function getLimits({
teamId,
userId,
}: {
teamId: string;
userId: string;
}) {
const team = await prisma.team.findUnique({
where: {
id: teamId,
users: {
some: {
userId: userId,
},
},
},
select: {
plan: true,
limits: true,
},
});

if (!team) {
throw new Error("Team not found");
}

// parse the limits json with zod and return the limits
// {datarooms: 1, users: 1, domains: 1, customDomainOnPro: boolean, customDomainInDataroom: boolean}

const configSchema = z.object({
datarooms: z.number(),
users: z.number(),
domains: z.number(),
customDomainOnPro: z.boolean(),
customDomainInDataroom: z.boolean(),
});

try {
const parsedData = configSchema.parse(team.limits);

return parsedData;
} catch (error) {
// if no limits set, then return null and don't block the team
return null;
}
}
30 changes: 30 additions & 0 deletions ee/limits/swr-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useTeam } from "@/context/team-context";
import useSWR from "swr";
import { fetcher } from "@/lib/utils";

export type LimitProps = {
datarooms: number;
users: number;
domains: number;
customDomainOnPro: boolean;
customDomainInDataroom: boolean;
};

export function useLimits() {
const teamInfo = useTeam();
const teamId = teamInfo?.currentTeam?.id;

const { data, error } = useSWR<LimitProps | null>(
teamId && `/api/teams/${teamId}/limits`,
fetcher,
{
dedupingInterval: 30000,
},
);

return {
limits: data,
error,
loading: !data && !error,
};
}
3 changes: 3 additions & 0 deletions lib/swr/use-limits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { useLimits } from "@/ee/limits/swr-handler";

export default useLimits;
16 changes: 16 additions & 0 deletions pages/api/teams/[teamId]/datarooms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { CustomUser } from "@/lib/types";
import slugify from "@sindresorhus/slugify";
import { newId } from "@/lib/id-helper";
import { getLimits } from "@/ee/limits/server";

export default async function handle(
req: NextApiRequest,
Expand Down Expand Up @@ -87,6 +88,21 @@ export default async function handle(
return res.status(401).end("Unauthorized");
}

// Limits: Check if the user has reached the limit of datarooms in the team
const dataroomCount = await prisma.dataroom.count({
where: {
teamId: teamId,
},
});

const limits = await getLimits({ teamId, userId });

if (limits && dataroomCount >= limits.datarooms) {
return res
.status(403)
.json({ message: "You have reached the limit of datarooms" });
}

const pId = newId("dataroom");

const dataroom = await prisma.dataroom.create({
Expand Down
23 changes: 21 additions & 2 deletions pages/api/teams/[teamId]/invite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { errorhandler } from "@/lib/errorHandler";
import { sendTeammateInviteEmail } from "@/lib/emails/send-teammate-invite";
import { newId } from "@/lib/id-helper";
import { hashToken } from "@/lib/api/auth/token";
import { getLimits } from "@/ee/limits/server";

export default async function handle(
req: NextApiRequest,
Expand Down Expand Up @@ -47,9 +48,14 @@ export default async function handle(
},
});

if (!team) {
res.status(404).json("Team not found");
return;
}

// check that the user is admin of the team, otherwise return 403
const teamUsers = team?.users;
const isUserAdmin = teamUsers?.some(
const teamUsers = team.users;
const isUserAdmin = teamUsers.some(
(user) =>
user.role === "ADMIN" &&
user.userId === (session.user as CustomUser).id,
Expand All @@ -59,6 +65,19 @@ export default async function handle(
return;
}

// Check if the user has reached the limit of users in the team
const limits = await getLimits({
teamId,
userId: (session.user as CustomUser).id,
});

if (limits && teamUsers.length >= limits.users) {
res
.status(403)
.json("You have reached the limit of users in your team");
return;
}

// check if user is already in the team
const isExistingMember = teamUsers?.some(
(user) => user.user.email === email,
Expand Down
3 changes: 3 additions & 0 deletions pages/api/teams/[teamId]/limits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import limitsHandler from "@/ee/limits/handler";

export default limitsHandler;
46 changes: 29 additions & 17 deletions pages/datarooms/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,24 @@ import {
import { Separator } from "@/components/ui/separator";
import { usePlan } from "@/lib/swr/use-billing";
import useDatarooms from "@/lib/swr/use-datarooms";
import useLimits from "@/lib/swr/use-limits";
import { daysLeft } from "@/lib/utils";
import { PlusIcon } from "lucide-react";
import Link from "next/link";

export default function DataroomsPage() {
const { datarooms } = useDatarooms();
const { plan, trial } = usePlan();
const { limits } = useLimits();

const numDatarooms = datarooms?.length ?? 1;
const limitDatarooms = limits?.datarooms ?? 0;

const isBusiness = plan === "business";
const isDatarooms = plan === "datarooms";
const isTrialDatarooms = trial === "drtrial";
const canCreateUnlimitedDatarooms =
isDatarooms || (isBusiness && numDatarooms < limitDatarooms);

return (
<AppLayout>
Expand All @@ -35,32 +46,24 @@ export default function DataroomsPage() {
</p>
</div>
<div className="flex items-center gap-x-1">
{plan !== "business" &&
plan !== "datarooms" &&
trial !== "drtrial" ? (
<DataroomTrialModal>
{isBusiness && !canCreateUnlimitedDatarooms ? (
<UpgradePlanModal clickedPlan="Data Rooms" trigger="datarooms">
<Button
className="flex-1 text-left group flex gap-x-3 items-center justify-start px-3"
title="Add New Document"
>
<span>Start Data Room Trial</span>
<span>Upgrade to Create Dataroom</span>
</Button>
</DataroomTrialModal>
) : datarooms &&
trial === "drtrial" &&
plan !== "business" &&
plan !== "datarooms" ? (
</UpgradePlanModal>
) : isTrialDatarooms && datarooms && !isBusiness && !isDatarooms ? (
<div className="flex items-center gap-x-4">
<div className="text-sm text-destructive ">
<span className="">Dataroom Trial:</span>{" "}
<div className="text-sm text-destructive">
<span>Dataroom Trial: </span>
<span className="font-medium">
{daysLeft(new Date(datarooms[0].createdAt), 7)} days left
</span>
</div>
<UpgradePlanModal
clickedPlan={"Business"}
trigger={"datarooms"}
>
<UpgradePlanModal clickedPlan="Business" trigger="datarooms">
<Button
className="flex-1 text-left group flex gap-x-3 items-center justify-start px-3"
title="Add New Document"
Expand All @@ -69,7 +72,7 @@ export default function DataroomsPage() {
</Button>
</UpgradePlanModal>
</div>
) : (
) : isBusiness || isDatarooms ? (
<AddDataroomModal>
<Button
className="flex-1 text-left group flex gap-x-3 items-center justify-start px-3"
Expand All @@ -79,6 +82,15 @@ export default function DataroomsPage() {
<span>Create New Dataroom</span>
</Button>
</AddDataroomModal>
) : (
<DataroomTrialModal>
<Button
className="flex-1 text-left group flex gap-x-3 items-center justify-start px-3"
title="Add New Document"
>
<span>Start Data Room Trial</span>
</Button>
</DataroomTrialModal>
)}
</div>
</section>
Expand Down
7 changes: 6 additions & 1 deletion pages/settings/people.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { usePlan } from "@/lib/swr/use-billing";
import { useInvitations } from "@/lib/swr/use-invitations";
import { UpgradePlanModal } from "@/components/billing/upgrade-plan-modal";
import { useAnalytics } from "@/lib/analytics";
import useLimits from "@/lib/swr/use-limits";

export default function Billing() {
const [isTeamMemberInviteModalOpen, setTeamMemberInviteModalOpen] =
Expand All @@ -36,13 +37,16 @@ export default function Billing() {
const { team, loading } = useGetTeam()!;
const teamInfo = useTeam();
const { plan: userPlan } = usePlan();
const { limits } = useLimits();
const { teams } = useTeams();
const analytics = useAnalytics();

const { invitations } = useInvitations();

const router = useRouter();

const numUsers = (team && team.users.length) ?? 1;

const getUserDocumentCount = (userId: string) => {
const documents = team?.documents.filter(
(document) => document.owner.id === userId,
Expand Down Expand Up @@ -185,7 +189,8 @@ export default function Billing() {
Teammates that have access to this project.
</p>
</div>
{userPlan !== "free" ? (
{userPlan !== "free" &&
(limits === null || (limits && limits.users >= numUsers)) ? (
<AddTeamMembers
open={isTeamMemberInviteModalOpen}
setOpen={setTeamMemberInviteModalOpen}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Team" ADD COLUMN "limits" JSONB;

4 changes: 3 additions & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,13 @@ model Team {
datarooms Dataroom[]
plan String @default("free")
stripeId String? @unique // Stripe subscription / customer ID
stripeId String? @unique // Stripe customer ID
subscriptionId String? @unique // Stripe subscription ID
startsAt DateTime? // Stripe subscription start date
endsAt DateTime? // Stripe subscription end date
limits Json? // Plan limits // {datarooms: 1, users: 1, domains: 1, customDomainOnPro: boolean, customDomainInDataroom: boolean}
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Expand Down

0 comments on commit ee39a50

Please sign in to comment.