Merge branch 'main' into feat/add-runtime-env

This commit is contained in:
Lucas Smith
2024-02-08 22:06:59 +11:00
committed by GitHub
294 changed files with 15355 additions and 1451 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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)) {

View File

@@ -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>
);
};

View File

@@ -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),
};
};