chore: merge main

This commit is contained in:
Catalin Documenso
2025-06-24 10:49:08 +03:00
787 changed files with 55308 additions and 42985 deletions

View File

@ -20,11 +20,11 @@
"@ts-rest/core": "^3.30.5",
"@ts-rest/open-api": "^3.33.0",
"@ts-rest/serverless": "^3.30.5",
"@types/swagger-ui-react": "^4.18.3",
"@types/swagger-ui-react": "^5.18.0",
"luxon": "^3.4.0",
"superjson": "^1.13.1",
"swagger-ui-react": "^5.11.0",
"swagger-ui-react": "^5.21.0",
"ts-pattern": "^5.0.5",
"zod": "3.24.1"
}
}
}

View File

@ -11,13 +11,12 @@ import {
ZDeleteDocumentMutationSchema,
ZDeleteFieldMutationSchema,
ZDeleteRecipientMutationSchema,
ZDownloadDocumentQuerySchema,
ZDownloadDocumentSuccessfulSchema,
ZFindTeamMembersResponseSchema,
ZGenerateDocumentFromTemplateMutationResponseSchema,
ZGenerateDocumentFromTemplateMutationSchema,
ZGetDocumentsQuerySchema,
ZGetTemplatesQuerySchema,
ZInviteTeamMemberMutationSchema,
ZNoBodyMutationSchema,
ZResendDocumentForSigningMutationSchema,
ZSendDocumentForSigningMutationSchema,
@ -28,17 +27,13 @@ import {
ZSuccessfulGetDocumentResponseSchema,
ZSuccessfulGetTemplateResponseSchema,
ZSuccessfulGetTemplatesResponseSchema,
ZSuccessfulInviteTeamMemberResponseSchema,
ZSuccessfulRecipientResponseSchema,
ZSuccessfulRemoveTeamMemberResponseSchema,
ZSuccessfulResendDocumentResponseSchema,
ZSuccessfulResponseSchema,
ZSuccessfulSigningResponseSchema,
ZSuccessfulUpdateTeamMemberResponseSchema,
ZUnsuccessfulResponseSchema,
ZUpdateFieldMutationSchema,
ZUpdateRecipientMutationSchema,
ZUpdateTeamMemberMutationSchema,
} from './schema';
const c = initContract();
@ -71,6 +66,7 @@ export const ApiContractV1 = c.router(
downloadSignedDocument: {
method: 'GET',
path: '/api/v1/documents/:id/download',
query: ZDownloadDocumentQuerySchema,
responses: {
200: ZDownloadDocumentSuccessfulSchema,
401: ZUnsuccessfulResponseSchema,
@ -282,61 +278,6 @@ export const ApiContractV1 = c.router(
},
summary: 'Delete a field from a document',
},
findTeamMembers: {
method: 'GET',
path: '/api/v1/team/:id/members',
responses: {
200: ZFindTeamMembersResponseSchema,
400: ZUnsuccessfulResponseSchema,
401: ZUnsuccessfulResponseSchema,
404: ZUnsuccessfulResponseSchema,
500: ZUnsuccessfulResponseSchema,
},
summary: 'List team members',
},
inviteTeamMember: {
method: 'POST',
path: '/api/v1/team/:id/members/invite',
body: ZInviteTeamMemberMutationSchema,
responses: {
200: ZSuccessfulInviteTeamMemberResponseSchema,
400: ZUnsuccessfulResponseSchema,
401: ZUnsuccessfulResponseSchema,
404: ZUnsuccessfulResponseSchema,
500: ZUnsuccessfulResponseSchema,
},
summary: 'Invite a member to a team',
},
updateTeamMember: {
method: 'PUT',
path: '/api/v1/team/:id/members/:memberId',
body: ZUpdateTeamMemberMutationSchema,
responses: {
200: ZSuccessfulUpdateTeamMemberResponseSchema,
400: ZUnsuccessfulResponseSchema,
401: ZUnsuccessfulResponseSchema,
404: ZUnsuccessfulResponseSchema,
500: ZUnsuccessfulResponseSchema,
},
summary: 'Update a team member',
},
removeTeamMember: {
method: 'DELETE',
path: '/api/v1/team/:id/members/:memberId',
body: null,
responses: {
200: ZSuccessfulRemoveTeamMemberResponseSchema,
400: ZUnsuccessfulResponseSchema,
401: ZUnsuccessfulResponseSchema,
404: ZUnsuccessfulResponseSchema,
500: ZUnsuccessfulResponseSchema,
},
summary: 'Remove a member from a team',
},
},
{
baseHeaders: ZAuthorizationHeadersSchema,

View File

@ -1,5 +1,5 @@
import type { Prisma } from '@prisma/client';
import { DocumentDataType, SigningStatus, TeamMemberRole } from '@prisma/client';
import { DocumentDataType, SigningStatus } from '@prisma/client';
import { tsr } from '@ts-rest/serverless/fetch';
import { match } from 'ts-pattern';
@ -27,9 +27,7 @@ import { deleteRecipient } from '@documenso/lib/server-only/recipient/delete-rec
import { getRecipientByIdV1Api } from '@documenso/lib/server-only/recipient/get-recipient-by-id-v1-api';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients';
import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
import { createTeamMemberInvites } from '@documenso/lib/server-only/team/create-team-member-invites';
import { deleteTeamMembers } from '@documenso/lib/server-only/team/delete-team-members';
import { updateDocumentRecipients } from '@documenso/lib/server-only/recipient/update-document-recipients';
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { createDocumentFromTemplateLegacy } from '@documenso/lib/server-only/template/create-document-from-template-legacy';
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
@ -52,6 +50,7 @@ import {
} from '@documenso/lib/universal/upload/server-actions';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { ApiContractV1 } from './contract';
@ -142,6 +141,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
downloadSignedDocument: authenticatedMiddleware(async (args, user, team) => {
const { id: documentId } = args.params;
const { downloadOriginalDocument } = args.query;
try {
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
@ -177,7 +177,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
};
}
if (!isDocumentCompleted(document.status)) {
if (!downloadOriginalDocument && !isDocumentCompleted(document.status)) {
return {
status: 400,
body: {
@ -186,7 +186,9 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
};
}
const { url } = await getPresignGetUrl(document.documentData.data);
const { url } = await getPresignGetUrl(
downloadOriginalDocument ? document.documentData.initialData : document.documentData.data,
);
return {
status: 200,
@ -255,7 +257,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
};
}
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
const { remaining } = await getServerLimits({ userId: user.id, teamId: team.id });
if (remaining.documents <= 0) {
return {
@ -464,7 +466,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { body, params } = args;
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
const { remaining } = await getServerLimits({ userId: user.id, teamId: team?.id });
if (remaining.documents <= 0) {
return {
@ -562,7 +564,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { body, params } = args;
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
const { remaining } = await getServerLimits({ userId: user.id, teamId: team?.id });
if (remaining.documents <= 0) {
return {
@ -818,7 +820,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
name,
role,
signingOrder,
actionAuth: authOptions?.actionAuth ?? null,
actionAuth: authOptions?.actionAuth ?? [],
},
],
requestMetadata: metadata,
@ -876,18 +878,24 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
};
}
const updatedRecipient = await updateRecipient({
documentId: Number(documentId),
recipientId: Number(recipientId),
const updatedRecipient = await updateDocumentRecipients({
userId: user.id,
teamId: team?.id,
email,
name,
role,
signingOrder,
actionAuth: authOptions?.actionAuth,
requestMetadata: metadata.requestMetadata,
}).catch(() => null);
teamId: team.id,
documentId: Number(documentId),
recipients: [
{
id: Number(recipientId),
email,
name,
role,
signingOrder,
actionAuth: authOptions?.actionAuth ?? [],
},
],
requestMetadata: metadata,
})
.then(({ recipients }) => recipients[0])
.catch(null);
if (!updatedRecipient) {
return {
@ -970,17 +978,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
select: { id: true, status: true },
where: {
id: Number(documentId),
...(team?.id
? {
team: {
id: team.id,
members: { some: { userId: user.id } },
},
}
: {
userId: user.id,
teamId: null,
}),
team: buildTeamWhereQuery({ teamId: team.id, userId: user.id }),
},
});
@ -1230,6 +1228,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,
teamId: team.id,
});
if (!document) {
@ -1254,7 +1253,6 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
userId: user.id,
teamId: team?.id,
fieldId: Number(fieldId),
documentId: Number(documentId),
}).catch(() => null);
if (!field) {
@ -1319,272 +1317,6 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
},
};
}),
findTeamMembers: authenticatedMiddleware(async (args, user, team) => {
const { id: teamId } = args.params;
if (team?.id !== Number(teamId)) {
return {
status: 403,
body: {
message: 'You are not authorized to perform actions against this team.',
},
};
}
const self = await prisma.teamMember.findFirst({
where: {
userId: user.id,
teamId: team.id,
},
});
if (self?.role !== TeamMemberRole.ADMIN) {
return {
status: 403,
body: {
message: 'You are not authorized to perform actions against this team.',
},
};
}
const members = await prisma.teamMember.findMany({
where: {
teamId: team.id,
},
include: {
user: true,
},
});
return {
status: 200,
body: {
members: members.map((member) => ({
id: member.id,
email: member.user.email,
role: member.role,
})),
},
};
}),
inviteTeamMember: authenticatedMiddleware(async (args, user, team) => {
const { id: teamId } = args.params;
const { email, role } = args.body;
if (team?.id !== Number(teamId)) {
return {
status: 403,
body: {
message: 'You are not authorized to perform actions against this team.',
},
};
}
const self = await prisma.teamMember.findFirst({
where: {
userId: user.id,
teamId: team.id,
},
});
if (self?.role !== TeamMemberRole.ADMIN) {
return {
status: 403,
body: {
message: 'You are not authorized to perform actions against this team.',
},
};
}
const hasAlreadyBeenInvited = await prisma.teamMember.findFirst({
where: {
teamId: team.id,
user: {
email,
},
},
});
if (hasAlreadyBeenInvited) {
return {
status: 400,
body: {
message: 'This user has already been invited to the team',
},
};
}
await createTeamMemberInvites({
userId: user.id,
userName: user.name ?? '',
teamId: team.id,
invitations: [
{
email,
role,
},
],
});
return {
status: 200,
body: {
message: 'An invite has been sent to the member',
},
};
}),
updateTeamMember: authenticatedMiddleware(async (args, user, team) => {
const { id: teamId, memberId } = args.params;
const { role } = args.body;
if (team?.id !== Number(teamId)) {
return {
status: 403,
body: {
message: 'You are not authorized to perform actions against this team.',
},
};
}
const self = await prisma.teamMember.findFirst({
where: {
userId: user.id,
teamId: team.id,
},
});
if (self?.role !== TeamMemberRole.ADMIN) {
return {
status: 403,
body: {
message: 'You are not authorized to perform actions against this team.',
},
};
}
const member = await prisma.teamMember.findFirst({
where: {
id: Number(memberId),
teamId: team.id,
},
});
if (!member) {
return {
status: 404,
body: {
message: 'The provided member id does not exist.',
},
};
}
const updatedMember = await prisma.teamMember.update({
where: {
id: member.id,
},
data: {
role,
},
include: {
user: true,
},
});
return {
status: 200,
body: {
id: updatedMember.id,
email: updatedMember.user.email,
role: updatedMember.role,
},
};
}),
removeTeamMember: authenticatedMiddleware(async (args, user, team) => {
const { id: teamId, memberId } = args.params;
if (team?.id !== Number(teamId)) {
return {
status: 403,
body: {
message: 'You are not authorized to perform actions against this team.',
},
};
}
const self = await prisma.teamMember.findFirst({
where: {
userId: user.id,
teamId: team.id,
},
});
if (self?.role !== TeamMemberRole.ADMIN) {
return {
status: 403,
body: {
message: 'You are not authorized to perform actions against this team.',
},
};
}
const member = await prisma.teamMember.findFirst({
where: {
id: Number(memberId),
teamId: Number(teamId),
},
include: {
user: true,
},
});
if (!member) {
return {
status: 404,
body: {
message: 'Member not found',
},
};
}
if (team.ownerUserId === member.userId) {
return {
status: 403,
body: {
message: 'You cannot remove the owner of the team',
},
};
}
if (member.userId === user.id) {
return {
status: 403,
body: {
message: 'You cannot remove yourself from the team',
},
};
}
await deleteTeamMembers({
userId: user.id,
teamId: team.id,
teamMemberIds: [member.id],
});
return {
status: 200,
body: {
id: member.id,
email: member.user.email,
role: member.role,
},
};
}),
});
const updateDocument = async ({
@ -1596,26 +1328,13 @@ const updateDocument = async ({
documentId: number;
data: Prisma.DocumentUpdateInput;
userId: number;
teamId?: number;
teamId: number;
}) => {
return await prisma.document.update({
where: {
id: documentId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
}),
userId,
team: buildTeamWhereQuery({ teamId, userId }),
},
data: {
...data,

View File

@ -25,8 +25,8 @@ export const authenticatedMiddleware = <
>(
handler: (
args: T & { req: TsRestRequest },
user: User,
team: Team | null | undefined,
user: Pick<User, 'id' | 'email' | 'name' | 'disabled'>,
team: Team,
options: { metadata: ApiRequestMetadata },
) => Promise<R>,
) => {

View File

@ -8,7 +8,6 @@ import {
RecipientRole,
SendStatus,
SigningStatus,
TeamMemberRole,
TemplateType,
} from '@prisma/client';
import { z } from 'zod';
@ -120,6 +119,15 @@ export const ZUploadDocumentSuccessfulSchema = z.object({
key: z.string(),
});
export const ZDownloadDocumentQuerySchema = z.object({
downloadOriginalDocument: z
.preprocess((val) => String(val) === 'true' || String(val) === '1', z.boolean())
.optional()
.default(false),
});
export type TDownloadDocumentQuerySchema = z.infer<typeof ZDownloadDocumentQuerySchema>;
export const ZDownloadDocumentSuccessfulSchema = z.object({
downloadUrl: z.string(),
});
@ -169,8 +177,16 @@ export const ZCreateDocumentMutationSchema = z.object({
.default({}),
authOptions: z
.object({
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
globalAccessAuth: z
.union([ZDocumentAccessAuthTypesSchema, z.array(ZDocumentAccessAuthTypesSchema)])
.transform((val) => (Array.isArray(val) ? val : [val]))
.optional()
.default([]),
globalActionAuth: z
.union([ZDocumentActionAuthTypesSchema, z.array(ZDocumentActionAuthTypesSchema)])
.transform((val) => (Array.isArray(val) ? val : [val]))
.optional()
.default([]),
})
.optional()
.openapi({
@ -236,8 +252,16 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
.optional(),
authOptions: z
.object({
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
globalAccessAuth: z
.union([ZDocumentAccessAuthTypesSchema, z.array(ZDocumentAccessAuthTypesSchema)])
.transform((val) => (Array.isArray(val) ? val : [val]))
.optional()
.default([]),
globalActionAuth: z
.union([ZDocumentActionAuthTypesSchema, z.array(ZDocumentActionAuthTypesSchema)])
.transform((val) => (Array.isArray(val) ? val : [val]))
.optional()
.default([]),
})
.optional(),
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
@ -309,8 +333,16 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
.optional(),
authOptions: z
.object({
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
globalAccessAuth: z
.union([ZDocumentAccessAuthTypesSchema, z.array(ZDocumentAccessAuthTypesSchema)])
.transform((val) => (Array.isArray(val) ? val : [val]))
.optional()
.default([]),
globalActionAuth: z
.union([ZDocumentActionAuthTypesSchema, z.array(ZDocumentActionAuthTypesSchema)])
.transform((val) => (Array.isArray(val) ? val : [val]))
.optional()
.default([]),
})
.optional(),
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
@ -349,7 +381,11 @@ export const ZCreateRecipientMutationSchema = z.object({
signingOrder: z.number().nullish(),
authOptions: z
.object({
actionAuth: ZRecipientActionAuthTypesSchema.optional(),
actionAuth: z
.union([ZRecipientActionAuthTypesSchema, z.array(ZRecipientActionAuthTypesSchema)])
.transform((val) => (Array.isArray(val) ? val : [val]))
.optional()
.default([]),
})
.optional()
.openapi({
@ -599,41 +635,3 @@ export const ZGetTemplatesQuerySchema = z.object({
page: z.coerce.number().min(1).optional().default(1),
perPage: z.coerce.number().min(1).optional().default(1),
});
export const ZFindTeamMembersResponseSchema = z.object({
members: z.array(
z.object({
id: z.number(),
email: z.string().email(),
role: z.nativeEnum(TeamMemberRole),
}),
),
});
export const ZInviteTeamMemberMutationSchema = z.object({
email: z
.string()
.email()
.transform((email) => email.toLowerCase()),
role: z.nativeEnum(TeamMemberRole).optional().default(TeamMemberRole.MEMBER),
});
export const ZSuccessfulInviteTeamMemberResponseSchema = z.object({
message: z.string(),
});
export const ZUpdateTeamMemberMutationSchema = z.object({
role: z.nativeEnum(TeamMemberRole),
});
export const ZSuccessfulUpdateTeamMemberResponseSchema = z.object({
id: z.number(),
email: z.string().email(),
role: z.nativeEnum(TeamMemberRole),
});
export const ZSuccessfulRemoveTeamMemberResponseSchema = z.object({
id: z.number(),
email: z.string().email(),
role: z.nativeEnum(TeamMemberRole),
});