Merge branch 'main' into feat/add-runtime-env
This commit is contained in:
@@ -6,13 +6,18 @@ import { ZLimitsResponseSchema } from './schema';
|
||||
|
||||
export type GetLimitsOptions = {
|
||||
headers?: Record<string, string>;
|
||||
teamId?: number | null;
|
||||
};
|
||||
|
||||
export const getLimits = async ({ headers }: GetLimitsOptions = {}) => {
|
||||
export const getLimits = async ({ headers, teamId }: GetLimitsOptions = {}) => {
|
||||
const requestHeaders = headers ?? {};
|
||||
|
||||
const url = new URL('/api/limits', APP_BASE_URL() ?? 'http://localhost:3000');
|
||||
|
||||
if (teamId) {
|
||||
requestHeaders['team-id'] = teamId.toString();
|
||||
}
|
||||
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
...requestHeaders,
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { TLimitsSchema } from './schema';
|
||||
import type { TLimitsSchema } from './schema';
|
||||
|
||||
export const FREE_PLAN_LIMITS: TLimitsSchema = {
|
||||
documents: 5,
|
||||
recipients: 10,
|
||||
};
|
||||
|
||||
export const TEAM_PLAN_LIMITS: TLimitsSchema = {
|
||||
documents: Infinity,
|
||||
recipients: Infinity,
|
||||
};
|
||||
|
||||
export const SELFHOSTED_PLAN_LIMITS: TLimitsSchema = {
|
||||
documents: Infinity,
|
||||
recipients: Infinity,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { getToken } from 'next-auth/jwt';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { ERROR_CODES } from './errors';
|
||||
import { TLimitsErrorResponseSchema, TLimitsResponseSchema } from './schema';
|
||||
import type { TLimitsErrorResponseSchema, TLimitsResponseSchema } from './schema';
|
||||
import { getServerLimits } from './server';
|
||||
|
||||
export const limitsHandler = async (
|
||||
@@ -14,7 +14,19 @@ export const limitsHandler = async (
|
||||
try {
|
||||
const token = await getToken({ req });
|
||||
|
||||
const limits = await getServerLimits({ email: token?.email });
|
||||
const rawTeamId = req.headers['team-id'];
|
||||
|
||||
let teamId: number | null = null;
|
||||
|
||||
if (typeof rawTeamId === 'string' && !isNaN(parseInt(rawTeamId, 10))) {
|
||||
teamId = parseInt(rawTeamId, 10);
|
||||
}
|
||||
|
||||
if (!teamId && rawTeamId) {
|
||||
throw new Error(ERROR_CODES.INVALID_TEAM_ID);
|
||||
}
|
||||
|
||||
const limits = await getServerLimits({ email: token?.email, teamId });
|
||||
|
||||
return res.status(200).json(limits);
|
||||
} catch (err) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { equals } from 'remeda';
|
||||
|
||||
import { getLimits } from '../client';
|
||||
import { FREE_PLAN_LIMITS } from '../constants';
|
||||
import { TLimitsResponseSchema } from '../schema';
|
||||
import type { TLimitsResponseSchema } from '../schema';
|
||||
|
||||
export type LimitsContextValue = TLimitsResponseSchema;
|
||||
|
||||
@@ -24,19 +24,22 @@ export const useLimits = () => {
|
||||
|
||||
export type LimitsProviderProps = {
|
||||
initialValue?: LimitsContextValue;
|
||||
teamId?: number;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const LimitsProvider = ({ initialValue, children }: LimitsProviderProps) => {
|
||||
const defaultValue: TLimitsResponseSchema = {
|
||||
export const LimitsProvider = ({
|
||||
initialValue = {
|
||||
quota: FREE_PLAN_LIMITS,
|
||||
remaining: FREE_PLAN_LIMITS,
|
||||
};
|
||||
|
||||
const [limits, setLimits] = useState(() => initialValue ?? defaultValue);
|
||||
},
|
||||
teamId,
|
||||
children,
|
||||
}: LimitsProviderProps) => {
|
||||
const [limits, setLimits] = useState(() => initialValue);
|
||||
|
||||
const refreshLimits = async () => {
|
||||
const newLimits = await getLimits();
|
||||
const newLimits = await getLimits({ teamId });
|
||||
|
||||
setLimits((oldLimits) => {
|
||||
if (equals(oldLimits, newLimits)) {
|
||||
|
||||
@@ -3,16 +3,22 @@
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
import { getLimits } from '../client';
|
||||
import type { LimitsContextValue } from './client';
|
||||
import { LimitsProvider as ClientLimitsProvider } from './client';
|
||||
|
||||
export type LimitsProviderProps = {
|
||||
children?: React.ReactNode;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const LimitsProvider = async ({ children }: LimitsProviderProps) => {
|
||||
export const LimitsProvider = async ({ children, teamId }: LimitsProviderProps) => {
|
||||
const requestHeaders = Object.fromEntries(headers().entries());
|
||||
|
||||
const limits = await getLimits({ headers: requestHeaders });
|
||||
const limits: LimitsContextValue = await getLimits({ headers: requestHeaders, teamId });
|
||||
|
||||
return <ClientLimitsProvider initialValue={limits}>{children}</ClientLimitsProvider>;
|
||||
return (
|
||||
<ClientLimitsProvider initialValue={limits} teamId={teamId}>
|
||||
{children}
|
||||
</ClientLimitsProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { getFlag } from '@documenso/lib/universal/get-feature-flag';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { SubscriptionStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { getPricesByType } from '../stripe/get-prices-by-type';
|
||||
import { FREE_PLAN_LIMITS, SELFHOSTED_PLAN_LIMITS } from './constants';
|
||||
import { getPricesByPlan } from '../stripe/get-prices-by-plan';
|
||||
import { FREE_PLAN_LIMITS, SELFHOSTED_PLAN_LIMITS, TEAM_PLAN_LIMITS } from './constants';
|
||||
import { ERROR_CODES } from './errors';
|
||||
import { ZLimitsSchema } from './schema';
|
||||
|
||||
export type GetServerLimitsOptions = {
|
||||
email?: string | null;
|
||||
teamId?: number | null;
|
||||
};
|
||||
|
||||
export const getServerLimits = async ({ email }: GetServerLimitsOptions) => {
|
||||
const isBillingEnabled = await getFlag('app_billing');
|
||||
|
||||
if (!isBillingEnabled) {
|
||||
export const getServerLimits = async ({ email, teamId }: GetServerLimitsOptions) => {
|
||||
if (!IS_BILLING_ENABLED) {
|
||||
return {
|
||||
quota: SELFHOSTED_PLAN_LIMITS,
|
||||
remaining: SELFHOSTED_PLAN_LIMITS,
|
||||
@@ -27,6 +27,14 @@ export const getServerLimits = async ({ email }: GetServerLimitsOptions) => {
|
||||
throw new Error(ERROR_CODES.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
return teamId ? handleTeamLimits({ email, teamId }) : handleUserLimits({ email });
|
||||
};
|
||||
|
||||
type HandleUserLimitsOptions = {
|
||||
email: string;
|
||||
};
|
||||
|
||||
const handleUserLimits = async ({ email }: HandleUserLimitsOptions) => {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email,
|
||||
@@ -48,10 +56,10 @@ export const getServerLimits = async ({ email }: GetServerLimitsOptions) => {
|
||||
);
|
||||
|
||||
if (activeSubscriptions.length > 0) {
|
||||
const individualPrices = await getPricesByType('individual');
|
||||
const communityPlanPrices = await getPricesByPlan(STRIPE_PLAN_TYPE.COMMUNITY);
|
||||
|
||||
for (const subscription of activeSubscriptions) {
|
||||
const price = individualPrices.find((price) => price.id === subscription.priceId);
|
||||
const price = communityPlanPrices.find((price) => price.id === subscription.priceId);
|
||||
if (!price || typeof price.product === 'string' || price.product.deleted) {
|
||||
continue;
|
||||
}
|
||||
@@ -71,6 +79,7 @@ export const getServerLimits = async ({ email }: GetServerLimitsOptions) => {
|
||||
const documents = await prisma.document.count({
|
||||
where: {
|
||||
userId: user.id,
|
||||
teamId: null,
|
||||
createdAt: {
|
||||
gte: DateTime.utc().startOf('month').toJSDate(),
|
||||
},
|
||||
@@ -84,3 +93,50 @@ export const getServerLimits = async ({ email }: GetServerLimitsOptions) => {
|
||||
remaining,
|
||||
};
|
||||
};
|
||||
|
||||
type HandleTeamLimitsOptions = {
|
||||
email: string;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
const handleTeamLimits = async ({ email, teamId }: HandleTeamLimitsOptions) => {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
user: {
|
||||
email,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
subscription: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new Error('Team not found');
|
||||
}
|
||||
|
||||
const { subscription } = team;
|
||||
|
||||
if (subscription && subscription.status === SubscriptionStatus.INACTIVE) {
|
||||
return {
|
||||
quota: {
|
||||
documents: 0,
|
||||
recipients: 0,
|
||||
},
|
||||
remaining: {
|
||||
documents: 0,
|
||||
recipients: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
quota: structuredClone(TEAM_PLAN_LIMITS),
|
||||
remaining: structuredClone(TEAM_PLAN_LIMITS),
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user