mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 01:15:49 +10:00
Merge branch 'main' into feat/document-file-conversion
Resolved conflicts:
- create-document-data.ts: merged initialData + originalData/originalMimeType
- files.helpers.ts: kept both imports
- envelope-drop-zone-wrapper.tsx: adopted buildDropzoneRejectionDescription
- create-envelope-items.ts: adopted UNSAFE_createEnvelopeItems
- create-envelope.ts: integrated convertToPdfIfNeeded into createEnvelopeRouteCaller
Extended putPdfFileServerSide to accept { initialData, originalData,
originalMimeType } options; updated seal-document.handler call site.
This commit is contained in:
@@ -38,19 +38,19 @@ export const createStripeCustomerRoute = adminProcedure
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const stripeCustomer = await createCustomer({
|
||||
name: organisation.name,
|
||||
email: organisation.owner.email,
|
||||
});
|
||||
// Create Stripe customer outside a transaction to avoid holding a
|
||||
// connection open during the external API call.
|
||||
const stripeCustomer = await createCustomer({
|
||||
name: organisation.name,
|
||||
email: organisation.owner.email,
|
||||
});
|
||||
|
||||
await tx.organisation.update({
|
||||
where: {
|
||||
id: organisationId,
|
||||
},
|
||||
data: {
|
||||
customerId: stripeCustomer.id,
|
||||
},
|
||||
});
|
||||
await prisma.organisation.update({
|
||||
where: {
|
||||
id: organisationId,
|
||||
},
|
||||
data: {
|
||||
customerId: stripeCustomer.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { PDF_SIZE_A4_72PPI } from '@documenso/lib/constants/pdf';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { generateAuditLogPdf } from '@documenso/lib/server-only/pdf/generate-audit-log-pdf';
|
||||
import { unsafeBuildEnvelopeIdQuery } from '@documenso/lib/utils/envelope';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import {
|
||||
ZAdminDownloadDocumentAuditLogsRequestSchema,
|
||||
ZAdminDownloadDocumentAuditLogsResponseSchema,
|
||||
} from './download-document-audit-logs.types';
|
||||
|
||||
export const downloadDocumentAuditLogsRoute = adminProcedure
|
||||
.input(ZAdminDownloadDocumentAuditLogsRequestSchema)
|
||||
.output(ZAdminDownloadDocumentAuditLogsResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { envelopeId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
envelopeId,
|
||||
},
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: unsafeBuildEnvelopeIdQuery(
|
||||
{
|
||||
type: 'envelopeId',
|
||||
id: envelopeId,
|
||||
},
|
||||
EnvelopeType.DOCUMENT,
|
||||
),
|
||||
include: {
|
||||
documentMeta: true,
|
||||
envelopeItems: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope not found',
|
||||
});
|
||||
}
|
||||
|
||||
const auditLogPdf = await generateAuditLogPdf({
|
||||
envelope,
|
||||
recipients: envelope.recipients,
|
||||
fields: envelope.fields,
|
||||
language: envelope.documentMeta.language,
|
||||
envelopeOwner: {
|
||||
email: envelope.user.email,
|
||||
name: envelope.user.name || '',
|
||||
},
|
||||
envelopeItems: envelope.envelopeItems.map((item) => item.title),
|
||||
pageWidth: PDF_SIZE_A4_72PPI.width,
|
||||
pageHeight: PDF_SIZE_A4_72PPI.height,
|
||||
});
|
||||
|
||||
const result = await auditLogPdf.save();
|
||||
|
||||
const base64 = Buffer.from(result).toString('base64');
|
||||
|
||||
return {
|
||||
data: base64,
|
||||
envelopeTitle: envelope.title,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZAdminDownloadDocumentAuditLogsRequestSchema = z.object({
|
||||
envelopeId: z.string(),
|
||||
});
|
||||
|
||||
export const ZAdminDownloadDocumentAuditLogsResponseSchema = z.object({
|
||||
data: z.string(),
|
||||
envelopeTitle: z.string(),
|
||||
});
|
||||
|
||||
export type TAdminDownloadDocumentAuditLogsRequest = z.infer<
|
||||
typeof ZAdminDownloadDocumentAuditLogsRequestSchema
|
||||
>;
|
||||
export type TAdminDownloadDocumentAuditLogsResponse = z.infer<
|
||||
typeof ZAdminDownloadDocumentAuditLogsResponseSchema
|
||||
>;
|
||||
@@ -120,14 +120,16 @@ export const findAdminOrganisations = async ({
|
||||
};
|
||||
}
|
||||
|
||||
const orderBy: Prisma.OrganisationOrderByWithRelationInput[] = query
|
||||
? [{ subscription: { status: 'asc' } }, { name: 'asc' }]
|
||||
: [{ createdAt: 'desc' }];
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.organisation.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
orderBy,
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
import type { FindResultResponse } from '@documenso/lib/types/search-params';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import {
|
||||
ZFindEmailDomainsRequestSchema,
|
||||
ZFindEmailDomainsResponseSchema,
|
||||
} from './find-email-domains.types';
|
||||
|
||||
export const findEmailDomainsRoute = adminProcedure
|
||||
.input(ZFindEmailDomainsRequestSchema)
|
||||
.output(ZFindEmailDomainsResponseSchema)
|
||||
.query(async ({ input }) => {
|
||||
const { query, page, perPage, status } = input;
|
||||
|
||||
return await findEmailDomains({ query, page, perPage, status });
|
||||
});
|
||||
|
||||
type FindEmailDomainsOptions = {
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
status?: 'PENDING' | 'ACTIVE';
|
||||
};
|
||||
|
||||
const findEmailDomains = async ({
|
||||
query,
|
||||
page = 1,
|
||||
perPage = 20,
|
||||
status,
|
||||
}: FindEmailDomainsOptions) => {
|
||||
const whereClause: Prisma.EmailDomainWhereInput = {};
|
||||
|
||||
if (query) {
|
||||
whereClause.OR = [
|
||||
{
|
||||
domain: {
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
},
|
||||
{
|
||||
organisation: {
|
||||
name: {
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereClause.status = status;
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.emailDomain.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
domain: true,
|
||||
status: true,
|
||||
selector: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
lastVerifiedAt: true,
|
||||
organisation: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
emails: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.emailDomain.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
data,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import EmailDomainStatusSchema from '@documenso/prisma/generated/zod/inputTypeSchemas/EmailDomainStatusSchema';
|
||||
import EmailDomainSchema from '@documenso/prisma/generated/zod/modelSchema/EmailDomainSchema';
|
||||
import OrganisationSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationSchema';
|
||||
|
||||
export const ZFindEmailDomainsRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
status: EmailDomainStatusSchema.optional(),
|
||||
});
|
||||
|
||||
export const ZFindEmailDomainsResponseSchema = ZFindResultResponse.extend({
|
||||
data: EmailDomainSchema.pick({
|
||||
id: true,
|
||||
domain: true,
|
||||
status: true,
|
||||
selector: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
lastVerifiedAt: true,
|
||||
})
|
||||
.extend({
|
||||
organisation: OrganisationSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
}),
|
||||
_count: z.object({
|
||||
emails: z.number(),
|
||||
}),
|
||||
})
|
||||
.array(),
|
||||
});
|
||||
|
||||
export type TFindEmailDomainsRequest = z.infer<typeof ZFindEmailDomainsRequestSchema>;
|
||||
export type TFindEmailDomainsResponse = z.infer<typeof ZFindEmailDomainsResponseSchema>;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { adminFindUnsealedDocuments } from '@documenso/lib/server-only/admin/admin-find-unsealed-documents';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import {
|
||||
ZFindUnsealedDocumentsRequestSchema,
|
||||
ZFindUnsealedDocumentsResponseSchema,
|
||||
} from './find-unsealed-documents.types';
|
||||
|
||||
export const findUnsealedDocumentsRoute = adminProcedure
|
||||
.input(ZFindUnsealedDocumentsRequestSchema)
|
||||
.output(ZFindUnsealedDocumentsResponseSchema)
|
||||
.query(async ({ input }) => {
|
||||
const { page, perPage } = input;
|
||||
|
||||
return await adminFindUnsealedDocuments({ page, perPage });
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
|
||||
export const ZFindUnsealedDocumentsRequestSchema = ZFindSearchParamsSchema.pick({
|
||||
page: true,
|
||||
perPage: true,
|
||||
}).extend({
|
||||
perPage: z.number().optional().default(20),
|
||||
});
|
||||
|
||||
export const ZAdminUnsealedDocumentSchema = z.object({
|
||||
id: z.string(),
|
||||
secondaryId: z.string(),
|
||||
title: z.string(),
|
||||
status: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
userId: z.number(),
|
||||
teamId: z.number(),
|
||||
ownerName: z.string().nullable(),
|
||||
ownerEmail: z.string(),
|
||||
lastSignedAt: z.date().nullable(),
|
||||
});
|
||||
|
||||
export const ZFindUnsealedDocumentsResponseSchema = ZFindResultResponse.extend({
|
||||
data: ZAdminUnsealedDocumentSchema.array(),
|
||||
});
|
||||
|
||||
export type TFindUnsealedDocumentsRequest = z.infer<typeof ZFindUnsealedDocumentsRequestSchema>;
|
||||
export type TFindUnsealedDocumentsResponse = z.infer<typeof ZFindUnsealedDocumentsResponseSchema>;
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
import type { FindResultResponse } from '@documenso/lib/types/search-params';
|
||||
import { getHighestTeamRoleInGroup } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import { ZFindUserTeamsRequestSchema, ZFindUserTeamsResponseSchema } from './find-user-teams.types';
|
||||
|
||||
export const findUserTeamsRoute = adminProcedure
|
||||
.input(ZFindUserTeamsRequestSchema)
|
||||
.output(ZFindUserTeamsResponseSchema)
|
||||
.query(async ({ input }) => {
|
||||
const { userId, query, page, perPage } = input;
|
||||
|
||||
return await findUserTeams({
|
||||
userId,
|
||||
query,
|
||||
page,
|
||||
perPage,
|
||||
});
|
||||
});
|
||||
|
||||
type FindUserTeamsOptions = {
|
||||
userId: number;
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
};
|
||||
|
||||
const findUserTeams = async ({ userId, query, page = 1, perPage = 10 }: FindUserTeamsOptions) => {
|
||||
const whereClause: Prisma.TeamWhereInput = {
|
||||
teamGroups: {
|
||||
some: {
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (query && query.length > 0) {
|
||||
whereClause.name = {
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
};
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.team.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
include: {
|
||||
organisation: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
teamGroups: {
|
||||
where: {
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.team.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
const mappedData = data.map((team) => ({
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
url: team.url,
|
||||
createdAt: team.createdAt,
|
||||
teamRole: getHighestTeamRoleInGroup(team.teamGroups),
|
||||
organisation: team.organisation,
|
||||
}));
|
||||
|
||||
return {
|
||||
data: mappedData,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<typeof mappedData>;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { TeamMemberRoleSchema } from '@documenso/prisma/generated/zod/inputTypeSchemas/TeamMemberRoleSchema';
|
||||
import OrganisationSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationSchema';
|
||||
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
|
||||
export const ZFindUserTeamsRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
userId: z.number(),
|
||||
});
|
||||
|
||||
export const ZFindUserTeamsResponseSchema = ZFindResultResponse.extend({
|
||||
data: TeamSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
createdAt: true,
|
||||
})
|
||||
.extend({
|
||||
teamRole: TeamMemberRoleSchema,
|
||||
organisation: OrganisationSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
}),
|
||||
})
|
||||
.array(),
|
||||
});
|
||||
|
||||
export type TFindUserTeamsRequest = z.infer<typeof ZFindUserTeamsRequestSchema>;
|
||||
export type TFindUserTeamsResponse = z.infer<typeof ZFindUserTeamsResponseSchema>;
|
||||
@@ -0,0 +1,131 @@
|
||||
import { OrganisationMemberInviteStatus } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getHighestOrganisationRoleInGroup } from '@documenso/lib/utils/organisations';
|
||||
import { getHighestTeamRoleInGroup } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import { ZGetAdminTeamRequestSchema, ZGetAdminTeamResponseSchema } from './get-admin-team.types';
|
||||
|
||||
export const getAdminTeamRoute = adminProcedure
|
||||
.input(ZGetAdminTeamRequestSchema)
|
||||
.output(ZGetAdminTeamResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { teamId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
const team = await prisma.team.findUnique({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
include: {
|
||||
organisation: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
ownerUserId: true,
|
||||
},
|
||||
},
|
||||
teamEmail: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team not found',
|
||||
});
|
||||
}
|
||||
|
||||
const [teamMembers, pendingInvites] = await Promise.all([
|
||||
prisma.organisationMember.findMany({
|
||||
where: {
|
||||
organisationId: team.organisationId,
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
group: {
|
||||
teamGroups: {
|
||||
some: {
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
createdAt: true,
|
||||
organisationGroupMembers: {
|
||||
include: {
|
||||
group: {
|
||||
include: {
|
||||
teamGroups: {
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
// Invites are organisation-scoped in the schema (no team relation), so this is intentionally
|
||||
// all pending invites for the team's parent organisation.
|
||||
prisma.organisationMemberInvite.findMany({
|
||||
where: {
|
||||
organisationId: team.organisationId,
|
||||
status: OrganisationMemberInviteStatus.PENDING,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
createdAt: true,
|
||||
organisationRole: true,
|
||||
status: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const mappedTeamMembers = teamMembers.map((teamMember) => {
|
||||
const groups = teamMember.organisationGroupMembers.map(({ group }) => group);
|
||||
|
||||
return {
|
||||
id: teamMember.id,
|
||||
userId: teamMember.userId,
|
||||
createdAt: teamMember.createdAt,
|
||||
user: teamMember.user,
|
||||
teamRole: getHighestTeamRoleInGroup(groups.flatMap((group) => group.teamGroups)),
|
||||
organisationRole: getHighestOrganisationRoleInGroup(groups),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...team,
|
||||
memberCount: mappedTeamMembers.length,
|
||||
teamMembers: mappedTeamMembers,
|
||||
pendingInvites,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { OrganisationMemberRoleSchema } from '@documenso/prisma/generated/zod/inputTypeSchemas/OrganisationMemberRoleSchema';
|
||||
import { TeamMemberRoleSchema } from '@documenso/prisma/generated/zod/inputTypeSchemas/TeamMemberRoleSchema';
|
||||
import OrganisationMemberInviteSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationMemberInviteSchema';
|
||||
import OrganisationMemberSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationMemberSchema';
|
||||
import OrganisationSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationSchema';
|
||||
import TeamEmailSchema from '@documenso/prisma/generated/zod/modelSchema/TeamEmailSchema';
|
||||
import TeamGlobalSettingsSchema from '@documenso/prisma/generated/zod/modelSchema/TeamGlobalSettingsSchema';
|
||||
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
import UserSchema from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
|
||||
|
||||
export const ZGetAdminTeamRequestSchema = z.object({
|
||||
teamId: z.number().min(1),
|
||||
});
|
||||
|
||||
export const ZGetAdminTeamResponseSchema = TeamSchema.extend({
|
||||
organisation: OrganisationSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
ownerUserId: true,
|
||||
}),
|
||||
teamEmail: TeamEmailSchema.nullable(),
|
||||
teamGlobalSettings: TeamGlobalSettingsSchema.nullable(),
|
||||
memberCount: z.number(),
|
||||
teamMembers: OrganisationMemberSchema.pick({
|
||||
id: true,
|
||||
userId: true,
|
||||
createdAt: true,
|
||||
})
|
||||
.extend({
|
||||
user: UserSchema.pick({
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
}),
|
||||
teamRole: TeamMemberRoleSchema,
|
||||
organisationRole: OrganisationMemberRoleSchema,
|
||||
})
|
||||
.array(),
|
||||
pendingInvites: OrganisationMemberInviteSchema.pick({
|
||||
id: true,
|
||||
email: true,
|
||||
createdAt: true,
|
||||
organisationRole: true,
|
||||
status: true,
|
||||
}).array(),
|
||||
});
|
||||
|
||||
export type TGetAdminTeamRequest = z.infer<typeof ZGetAdminTeamRequestSchema>;
|
||||
export type TGetAdminTeamResponse = z.infer<typeof ZGetAdminTeamResponseSchema>;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetEmailDomainRequestSchema,
|
||||
ZGetEmailDomainResponseSchema,
|
||||
} from './get-email-domain.types';
|
||||
|
||||
export const getEmailDomainRoute = adminProcedure
|
||||
.input(ZGetEmailDomainRequestSchema)
|
||||
.output(ZGetEmailDomainResponseSchema)
|
||||
.query(async ({ input }) => {
|
||||
const { emailDomainId } = input;
|
||||
|
||||
const emailDomain = await prisma.emailDomain.findUnique({
|
||||
where: {
|
||||
id: emailDomainId,
|
||||
},
|
||||
omit: {
|
||||
privateKey: true,
|
||||
},
|
||||
include: {
|
||||
organisation: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
emails: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!emailDomain) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Email domain not found',
|
||||
});
|
||||
}
|
||||
|
||||
return emailDomain;
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZOrganisationEmailLiteSchema } from '@documenso/lib/types/organisation-email';
|
||||
import EmailDomainSchema from '@documenso/prisma/generated/zod/modelSchema/EmailDomainSchema';
|
||||
import OrganisationSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationSchema';
|
||||
|
||||
export const ZGetEmailDomainRequestSchema = z.object({
|
||||
emailDomainId: z.string(),
|
||||
});
|
||||
|
||||
export const ZGetEmailDomainResponseSchema = EmailDomainSchema.pick({
|
||||
id: true,
|
||||
domain: true,
|
||||
status: true,
|
||||
selector: true,
|
||||
publicKey: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
lastVerifiedAt: true,
|
||||
}).extend({
|
||||
organisation: OrganisationSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
}),
|
||||
emails: ZOrganisationEmailLiteSchema.array(),
|
||||
});
|
||||
|
||||
export type TGetEmailDomainRequest = z.infer<typeof ZGetEmailDomainRequestSchema>;
|
||||
export type TGetEmailDomainResponse = z.infer<typeof ZGetEmailDomainResponseSchema>;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { reregisterEmailDomain } from '@documenso/ee/server-only/lib/reregister-email-domain';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import {
|
||||
ZReregisterEmailDomainRequestSchema,
|
||||
ZReregisterEmailDomainResponseSchema,
|
||||
} from './reregister-email-domain.types';
|
||||
|
||||
export const reregisterEmailDomainRoute = adminProcedure
|
||||
.input(ZReregisterEmailDomainRequestSchema)
|
||||
.output(ZReregisterEmailDomainResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { emailDomainId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
emailDomainId,
|
||||
},
|
||||
});
|
||||
|
||||
await reregisterEmailDomain({ emailDomainId });
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZReregisterEmailDomainRequestSchema = z.object({
|
||||
emailDomainId: z.string(),
|
||||
});
|
||||
|
||||
export const ZReregisterEmailDomainResponseSchema = z.void();
|
||||
|
||||
export type TReregisterEmailDomainRequest = z.infer<typeof ZReregisterEmailDomainRequestSchema>;
|
||||
export type TReregisterEmailDomainResponse = z.infer<typeof ZReregisterEmailDomainResponseSchema>;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { LicenseClient } from '@documenso/lib/server-only/license/license-client';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import { ZResyncLicenseRequestSchema, ZResyncLicenseResponseSchema } from './resync-license.types';
|
||||
|
||||
export const resyncLicenseRoute = adminProcedure
|
||||
.input(ZResyncLicenseRequestSchema)
|
||||
.output(ZResyncLicenseResponseSchema)
|
||||
.mutation(async () => {
|
||||
const client = LicenseClient.getInstance();
|
||||
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
|
||||
await client.resync();
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZResyncLicenseRequestSchema = z.void();
|
||||
|
||||
export const ZResyncLicenseResponseSchema = z.void();
|
||||
|
||||
export type TResyncLicenseRequest = z.infer<typeof ZResyncLicenseRequestSchema>;
|
||||
export type TResyncLicenseResponse = z.infer<typeof ZResyncLicenseResponseSchema>;
|
||||
@@ -6,17 +6,26 @@ import { deleteDocumentRoute } from './delete-document';
|
||||
import { deleteSubscriptionClaimRoute } from './delete-subscription-claim';
|
||||
import { deleteUserRoute } from './delete-user';
|
||||
import { disableUserRoute } from './disable-user';
|
||||
import { downloadDocumentAuditLogsRoute } from './download-document-audit-logs';
|
||||
import { enableUserRoute } from './enable-user';
|
||||
import { findAdminOrganisationsRoute } from './find-admin-organisations';
|
||||
import { findDocumentAuditLogsRoute } from './find-document-audit-logs';
|
||||
import { findDocumentJobsRoute } from './find-document-jobs';
|
||||
import { findDocumentsRoute } from './find-documents';
|
||||
import { findEmailDomainsRoute } from './find-email-domains';
|
||||
import { findSubscriptionClaimsRoute } from './find-subscription-claims';
|
||||
import { findUnsealedDocumentsRoute } from './find-unsealed-documents';
|
||||
import { findUserTeamsRoute } from './find-user-teams';
|
||||
import { getAdminOrganisationRoute } from './get-admin-organisation';
|
||||
import { getAdminTeamRoute } from './get-admin-team';
|
||||
import { getEmailDomainRoute } from './get-email-domain';
|
||||
import { getUserRoute } from './get-user';
|
||||
import { promoteMemberToOwnerRoute } from './promote-member-to-owner';
|
||||
import { reregisterEmailDomainRoute } from './reregister-email-domain';
|
||||
import { resealDocumentRoute } from './reseal-document';
|
||||
import { resetTwoFactorRoute } from './reset-two-factor-authentication';
|
||||
import { resyncLicenseRoute } from './resync-license';
|
||||
import { swapOrganisationSubscriptionRoute } from './swap-organisation-subscription';
|
||||
import { updateAdminOrganisationRoute } from './update-admin-organisation';
|
||||
import { updateOrganisationMemberRoleRoute } from './update-organisation-member-role';
|
||||
import { updateRecipientRoute } from './update-recipient';
|
||||
@@ -30,6 +39,7 @@ export const adminRouter = router({
|
||||
get: getAdminOrganisationRoute,
|
||||
create: createAdminOrganisationRoute,
|
||||
update: updateAdminOrganisationRoute,
|
||||
swapSubscription: swapOrganisationSubscriptionRoute,
|
||||
},
|
||||
organisationMember: {
|
||||
promoteToOwner: promoteMemberToOwnerRoute,
|
||||
@@ -44,6 +54,9 @@ export const adminRouter = router({
|
||||
stripe: {
|
||||
createCustomer: createStripeCustomerRoute,
|
||||
},
|
||||
license: {
|
||||
resync: resyncLicenseRoute,
|
||||
},
|
||||
user: {
|
||||
get: getUserRoute,
|
||||
update: updateUserRoute,
|
||||
@@ -51,16 +64,27 @@ export const adminRouter = router({
|
||||
enable: enableUserRoute,
|
||||
disable: disableUserRoute,
|
||||
resetTwoFactor: resetTwoFactorRoute,
|
||||
findTeams: findUserTeamsRoute,
|
||||
},
|
||||
document: {
|
||||
find: findDocumentsRoute,
|
||||
findUnsealed: findUnsealedDocumentsRoute,
|
||||
delete: deleteDocumentRoute,
|
||||
reseal: resealDocumentRoute,
|
||||
findJobs: findDocumentJobsRoute,
|
||||
findAuditLogs: findDocumentAuditLogsRoute,
|
||||
downloadAuditLogs: downloadDocumentAuditLogsRoute,
|
||||
},
|
||||
recipient: {
|
||||
update: updateRecipientRoute,
|
||||
},
|
||||
emailDomain: {
|
||||
find: findEmailDomainsRoute,
|
||||
get: getEmailDomainRoute,
|
||||
reregister: reregisterEmailDomainRoute,
|
||||
},
|
||||
team: {
|
||||
get: getAdminTeamRoute,
|
||||
},
|
||||
updateSiteSetting: updateSiteSettingRoute,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { SubscriptionStatus } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { createOrganisationClaimUpsertData } from '@documenso/lib/server-only/organisation/create-organisation';
|
||||
import { INTERNAL_CLAIM_ID, internalClaims } from '@documenso/lib/types/subscription';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import {
|
||||
ZSwapOrganisationSubscriptionRequestSchema,
|
||||
ZSwapOrganisationSubscriptionResponseSchema,
|
||||
} from './swap-organisation-subscription.types';
|
||||
|
||||
export const swapOrganisationSubscriptionRoute = adminProcedure
|
||||
.input(ZSwapOrganisationSubscriptionRequestSchema)
|
||||
.output(ZSwapOrganisationSubscriptionResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { sourceOrganisationId, targetOrganisationId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
sourceOrganisationId,
|
||||
targetOrganisationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (sourceOrganisationId === targetOrganisationId) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Source and target organisations must be different',
|
||||
});
|
||||
}
|
||||
|
||||
const sourceOrg = await prisma.organisation.findUnique({
|
||||
where: { id: sourceOrganisationId },
|
||||
include: {
|
||||
subscription: true,
|
||||
organisationClaim: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!sourceOrg) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Source organisation not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!sourceOrg.subscription ||
|
||||
(sourceOrg.subscription.status !== SubscriptionStatus.ACTIVE &&
|
||||
sourceOrg.subscription.status !== SubscriptionStatus.PAST_DUE)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Source organisation does not have an active subscription',
|
||||
});
|
||||
}
|
||||
|
||||
const targetOrg = await prisma.organisation.findUnique({
|
||||
where: { id: targetOrganisationId },
|
||||
include: {
|
||||
subscription: true,
|
||||
organisationClaim: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!targetOrg) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Target organisation not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (sourceOrg.ownerUserId !== targetOrg.ownerUserId) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Both organisations must be owned by the same user',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
targetOrg.subscription &&
|
||||
(targetOrg.subscription.status === SubscriptionStatus.ACTIVE ||
|
||||
targetOrg.subscription.status === SubscriptionStatus.PAST_DUE)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Target organisation already has an active subscription',
|
||||
});
|
||||
}
|
||||
|
||||
const customerId = sourceOrg.customerId ?? sourceOrg.subscription.customerId;
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// Delete stale INACTIVE subscription on target if present.
|
||||
if (targetOrg.subscription) {
|
||||
await tx.subscription.delete({
|
||||
where: { id: targetOrg.subscription.id },
|
||||
});
|
||||
}
|
||||
|
||||
// Clear customerId on source org to avoid unique constraint violation.
|
||||
await tx.organisation.update({
|
||||
where: { id: sourceOrganisationId },
|
||||
data: { customerId: null },
|
||||
});
|
||||
|
||||
// Set customerId on target org.
|
||||
await tx.organisation.update({
|
||||
where: { id: targetOrganisationId },
|
||||
data: { customerId },
|
||||
});
|
||||
|
||||
// Move the subscription record to the target org.
|
||||
await tx.subscription.update({
|
||||
where: { id: sourceOrg.subscription!.id },
|
||||
data: { organisationId: targetOrganisationId },
|
||||
});
|
||||
|
||||
// Copy source org's claim entitlements to target org's claim.
|
||||
if (sourceOrg.organisationClaim && targetOrg.organisationClaim) {
|
||||
await tx.organisationClaim.update({
|
||||
where: { id: targetOrg.organisationClaim.id },
|
||||
data: {
|
||||
originalSubscriptionClaimId: sourceOrg.organisationClaim.originalSubscriptionClaimId,
|
||||
teamCount: sourceOrg.organisationClaim.teamCount,
|
||||
memberCount: sourceOrg.organisationClaim.memberCount,
|
||||
envelopeItemCount: sourceOrg.organisationClaim.envelopeItemCount,
|
||||
flags: sourceOrg.organisationClaim.flags,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Reset source org's claim to FREE.
|
||||
if (sourceOrg.organisationClaim) {
|
||||
await tx.organisationClaim.update({
|
||||
where: { id: sourceOrg.organisationClaim.id },
|
||||
data: {
|
||||
originalSubscriptionClaimId: INTERNAL_CLAIM_ID.FREE,
|
||||
...createOrganisationClaimUpsertData(internalClaims[INTERNAL_CLAIM_ID.FREE]),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZSwapOrganisationSubscriptionRequestSchema = z.object({
|
||||
sourceOrganisationId: z.string(),
|
||||
targetOrganisationId: z.string(),
|
||||
});
|
||||
|
||||
export const ZSwapOrganisationSubscriptionResponseSchema = z.void();
|
||||
|
||||
export type TSwapOrganisationSubscriptionRequest = z.infer<
|
||||
typeof ZSwapOrganisationSubscriptionRequestSchema
|
||||
>;
|
||||
export type TSwapOrganisationSubscriptionResponse = z.infer<
|
||||
typeof ZSwapOrganisationSubscriptionResponseSchema
|
||||
>;
|
||||
@@ -10,7 +10,7 @@ export const updateRecipientRoute = adminProcedure
|
||||
.input(ZUpdateRecipientRequestSchema)
|
||||
.output(ZUpdateRecipientResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id, name, email } = input;
|
||||
const { id, name, email, role } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
@@ -18,5 +18,5 @@ export const updateRecipientRoute = adminProcedure
|
||||
},
|
||||
});
|
||||
|
||||
await updateRecipient({ id, name, email });
|
||||
await updateRecipient({ id, name, email, role });
|
||||
});
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
|
||||
export const ZUpdateRecipientRequestSchema = z.object({
|
||||
id: z.number().min(1),
|
||||
name: z.string().optional(),
|
||||
email: z.string().email().optional(),
|
||||
email: zEmail().optional(),
|
||||
role: z.enum(['CC', 'SIGNER', 'VIEWER', 'APPROVER', 'ASSISTANT']).optional(),
|
||||
});
|
||||
|
||||
export const ZUpdateRecipientResponseSchema = z.void();
|
||||
|
||||
@@ -29,24 +29,22 @@ export const updateSubscriptionClaimRoute = adminProcedure
|
||||
|
||||
const newlyEnabledFlags = getNewTruthyFlags(existingClaim.flags, data.flags);
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.subscriptionClaim.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data,
|
||||
});
|
||||
|
||||
if (Object.keys(newlyEnabledFlags).length > 0) {
|
||||
await jobsClient.triggerJob({
|
||||
name: 'internal.backport-subscription-claims',
|
||||
payload: {
|
||||
subscriptionClaimId: id,
|
||||
flags: newlyEnabledFlags,
|
||||
},
|
||||
});
|
||||
}
|
||||
await prisma.subscriptionClaim.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data,
|
||||
});
|
||||
|
||||
if (Object.keys(newlyEnabledFlags).length > 0) {
|
||||
await jobsClient.triggerJob({
|
||||
name: 'internal.backport-subscription-claims',
|
||||
payload: {
|
||||
subscriptionClaimId: id,
|
||||
flags: newlyEnabledFlags,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function getNewTruthyFlags(
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Role } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
|
||||
export const ZUpdateUserRequestSchema = z.object({
|
||||
id: z.number().min(1),
|
||||
name: z.string().nullish(),
|
||||
email: z.string().email().optional(),
|
||||
email: zEmail().optional(),
|
||||
roles: z.array(z.nativeEnum(Role)).optional(),
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import { DateTime } from 'luxon';
|
||||
|
||||
import { TWO_FACTOR_EMAIL_EXPIRATION_MINUTES } from '@documenso/lib/server-only/2fa/email/constants';
|
||||
import { send2FATokenEmail } from '@documenso/lib/server-only/2fa/email/send-2fa-token-email';
|
||||
import { assertRateLimit } from '@documenso/lib/server-only/rate-limit/rate-limit-middleware';
|
||||
import { request2FAEmailRateLimit } from '@documenso/lib/server-only/rate-limit/rate-limits';
|
||||
import { DocumentAuth } from '@documenso/lib/types/document-auth';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
@@ -21,6 +23,13 @@ export const accessAuthRequest2FAEmailRoute = procedure
|
||||
try {
|
||||
const { token } = input;
|
||||
|
||||
const rateLimitResult = await request2FAEmailRateLimit.check({
|
||||
ip: ctx.metadata.requestMetadata.ipAddress ?? 'unknown',
|
||||
identifier: token,
|
||||
});
|
||||
|
||||
assertRateLimit(rateLimitResult);
|
||||
|
||||
const user = ctx.user;
|
||||
|
||||
// Get document and recipient by token
|
||||
|
||||
@@ -38,6 +38,7 @@ export const createDocumentTemporaryRoute = authenticatedProcedure
|
||||
meta,
|
||||
folderId,
|
||||
attachments,
|
||||
formValues,
|
||||
} = input;
|
||||
|
||||
const { remaining } = await getServerLimits({ userId: user.id, teamId });
|
||||
@@ -68,6 +69,7 @@ export const createDocumentTemporaryRoute = authenticatedProcedure
|
||||
title,
|
||||
externalId,
|
||||
visibility,
|
||||
formValues,
|
||||
globalAccessAuth,
|
||||
globalActionAuth,
|
||||
recipients: (recipients || []).map((recipient) => ({
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
|
||||
/**
|
||||
* Temporariy endpoint for V2 Beta until we allow passthrough documents on create.
|
||||
* @deprecated
|
||||
*/
|
||||
export const createDocumentTemporaryMeta: TrpcRouteMeta = {
|
||||
openapi: {
|
||||
@@ -36,6 +37,7 @@ export const createDocumentTemporaryMeta: TrpcRouteMeta = {
|
||||
description:
|
||||
'You will need to upload the PDF to the provided URL returned. Note: Once V2 API is released, this will be removed since we will allow direct uploads, instead of using an upload URL.',
|
||||
tags: ['Document'],
|
||||
deprecated: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ export const createDocumentRoute = authenticatedProcedure
|
||||
visibility,
|
||||
globalAccessAuth,
|
||||
globalActionAuth,
|
||||
formValues,
|
||||
recipients: (recipients || []).map((recipient) => ({
|
||||
...recipient,
|
||||
fields: (recipient.fields || []).map((field) => ({
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from '@documenso/lib/types/field';
|
||||
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
|
||||
import { zodFormData } from '../../utils/zod-form-data';
|
||||
import { zfdFile, zodFormData } from '../../utils/zod-form-data';
|
||||
import { ZCreateRecipientSchema } from '../recipient-router/schema';
|
||||
import type { TrpcRouteMeta } from '../trpc';
|
||||
import { ZDocumentExternalIdSchema, ZDocumentTitleSchema } from './schema';
|
||||
@@ -79,7 +79,7 @@ export const ZCreateDocumentPayloadSchema = z.object({
|
||||
|
||||
export const ZCreateDocumentRequestSchema = zodFormData({
|
||||
payload: zfd.json(ZCreateDocumentPayloadSchema),
|
||||
file: zfd.file(),
|
||||
file: zfdFile(),
|
||||
});
|
||||
|
||||
export const ZCreateDocumentResponseSchema = z.object({
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ZDocumentMetaSubjectSchema,
|
||||
ZDocumentMetaTimezoneSchema,
|
||||
} from '@documenso/lib/types/document-meta';
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
|
||||
import type { TrpcRouteMeta } from '../trpc';
|
||||
|
||||
@@ -36,7 +37,7 @@ export const ZDistributeDocumentRequestSchema = z.object({
|
||||
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
|
||||
language: ZDocumentMetaLanguageSchema.optional(),
|
||||
emailId: z.string().nullish(),
|
||||
emailReplyTo: z.string().email().nullish(),
|
||||
emailReplyTo: zEmail().nullish(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { PDF_SIZE_A4_72PPI } from '@documenso/lib/constants/pdf';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
|
||||
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
|
||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||
import { generateAuditLogPdf } from '@documenso/lib/server-only/pdf/generate-audit-log-pdf';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
@@ -42,12 +40,26 @@ export const downloadDocumentAuditLogsRoute = authenticatedProcedure
|
||||
});
|
||||
}
|
||||
|
||||
const encrypted = encryptSecondaryData({
|
||||
data: mapSecondaryIdToDocumentId(envelope.secondaryId).toString(),
|
||||
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
|
||||
const certificatePdf = await generateAuditLogPdf({
|
||||
envelope,
|
||||
recipients: envelope.recipients,
|
||||
fields: envelope.fields,
|
||||
language: envelope.documentMeta.language,
|
||||
envelopeOwner: {
|
||||
email: envelope.user.email,
|
||||
name: envelope.user.name || '',
|
||||
},
|
||||
envelopeItems: envelope.envelopeItems.map((item) => item.title),
|
||||
pageWidth: PDF_SIZE_A4_72PPI.width,
|
||||
pageHeight: PDF_SIZE_A4_72PPI.height,
|
||||
});
|
||||
|
||||
const result = await certificatePdf.save();
|
||||
|
||||
const base64 = Buffer.from(result).toString('base64');
|
||||
|
||||
return {
|
||||
url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/audit-log?d=${encrypted}`,
|
||||
data: base64,
|
||||
envelopeTitle: envelope.title,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,7 +5,8 @@ export const ZDownloadDocumentAuditLogsRequestSchema = z.object({
|
||||
});
|
||||
|
||||
export const ZDownloadDocumentAuditLogsResponseSchema = z.object({
|
||||
url: z.string(),
|
||||
data: z.string(),
|
||||
envelopeTitle: z.string(),
|
||||
});
|
||||
|
||||
export type TDownloadDocumentAuditLogsRequest = z.infer<
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
|
||||
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
|
||||
import { PDF_SIZE_A4_72PPI } from '@documenso/lib/constants/pdf';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
|
||||
import { generateCertificatePdf } from '@documenso/lib/server-only/pdf/generate-certificate-pdf';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
@@ -27,7 +26,7 @@ export const downloadDocumentCertificateRoute = authenticatedProcedure
|
||||
},
|
||||
});
|
||||
|
||||
const envelope = await getEnvelopeById({
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
@@ -37,16 +36,54 @@ export const downloadDocumentCertificateRoute = authenticatedProcedure
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: {
|
||||
include: {
|
||||
signature: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (!isDocumentCompleted(envelope.status)) {
|
||||
throw new AppError('DOCUMENT_NOT_COMPLETE');
|
||||
}
|
||||
|
||||
const encrypted = encryptSecondaryData({
|
||||
data: mapSecondaryIdToDocumentId(envelope.secondaryId).toString(),
|
||||
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
|
||||
const certificatePdf = await generateCertificatePdf({
|
||||
envelope,
|
||||
recipients: envelope.recipients,
|
||||
fields: envelope.fields,
|
||||
language: envelope.documentMeta.language,
|
||||
envelopeOwner: {
|
||||
email: envelope.user.email,
|
||||
name: envelope.user.name || '',
|
||||
},
|
||||
pageWidth: PDF_SIZE_A4_72PPI.width,
|
||||
pageHeight: PDF_SIZE_A4_72PPI.height,
|
||||
});
|
||||
|
||||
const result = await certificatePdf.save();
|
||||
|
||||
const base64 = Buffer.from(result).toString('base64');
|
||||
|
||||
return {
|
||||
url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encrypted}`,
|
||||
data: base64,
|
||||
envelopeTitle: envelope.title,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,7 +5,8 @@ export const ZDownloadDocumentCertificateRequestSchema = z.object({
|
||||
});
|
||||
|
||||
export const ZDownloadDocumentCertificateResponseSchema = z.object({
|
||||
url: z.string(),
|
||||
data: z.string(),
|
||||
envelopeTitle: z.string(),
|
||||
});
|
||||
|
||||
export type TDownloadDocumentCertificateRequest = z.infer<
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
|
||||
import type { GetStatsInput } from '@documenso/lib/server-only/document/get-stats';
|
||||
import { getStats } from '@documenso/lib/server-only/document/get-stats';
|
||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||
import { mapEnvelopesToDocumentMany } from '@documenso/lib/utils/document';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
@@ -30,28 +28,15 @@ export const findDocumentsInternalRoute = authenticatedProcedure
|
||||
folderId,
|
||||
} = input;
|
||||
|
||||
const getStatOptions: GetStatsInput = {
|
||||
user,
|
||||
period,
|
||||
search: query,
|
||||
folderId,
|
||||
};
|
||||
|
||||
if (teamId) {
|
||||
const team = await getTeamById({ userId: user.id, teamId });
|
||||
|
||||
getStatOptions.team = {
|
||||
teamId: team.id,
|
||||
teamEmail: team.teamEmail?.email,
|
||||
senderIds,
|
||||
currentTeamMemberRole: team.currentTeamRole,
|
||||
currentUserEmail: user.email,
|
||||
userId: user.id,
|
||||
};
|
||||
}
|
||||
|
||||
const [stats, documents] = await Promise.all([
|
||||
getStats(getStatOptions),
|
||||
getStats({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
period,
|
||||
search: query,
|
||||
folderId,
|
||||
senderIds,
|
||||
}),
|
||||
findDocuments({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
|
||||
@@ -38,6 +38,7 @@ export const findDocumentsRoute = authenticatedProcedure
|
||||
perPage,
|
||||
folderId,
|
||||
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
|
||||
useWindowedCount: false,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { DocumentVisibility } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const DOCUMENT_TITLE_MAX_LENGTH = 255;
|
||||
|
||||
export const ZDocumentTitleSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(255)
|
||||
.max(DOCUMENT_TITLE_MAX_LENGTH)
|
||||
.describe('The title of the document.');
|
||||
|
||||
export const ZDocumentExternalIdSchema = z
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZSearchDocumentRequestSchema = z.object({
|
||||
query: z.string(),
|
||||
query: z.string().trim().min(1).max(1024),
|
||||
});
|
||||
|
||||
export const ZSearchDocumentResponseSchema = z
|
||||
|
||||
@@ -5,14 +5,14 @@ import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZUpdateDocumentRequestSchema,
|
||||
ZUpdateDocumentResponseSchema,
|
||||
updateDocumentMeta,
|
||||
} from './update-document.types';
|
||||
import { updateDocumentMeta as updateDocumentTrpcMeta } from './update-document.types';
|
||||
|
||||
/**
|
||||
* Public route.
|
||||
*/
|
||||
export const updateDocumentRoute = authenticatedProcedure
|
||||
.meta(updateDocumentTrpcMeta)
|
||||
.meta(updateDocumentMeta)
|
||||
.input(ZUpdateDocumentRequestSchema)
|
||||
.output(ZUpdateDocumentResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { router } from '../trpc';
|
||||
import { createEmbeddingDocumentRoute } from './create-embedding-document';
|
||||
import { createEmbeddingEnvelopeRoute } from './create-embedding-envelope';
|
||||
import { createEmbeddingPresignTokenRoute } from './create-embedding-presign-token';
|
||||
import { createEmbeddingTemplateRoute } from './create-embedding-template';
|
||||
import { getMultiSignDocumentRoute } from './get-multi-sign-document';
|
||||
import { updateEmbeddingDocumentRoute } from './update-embedding-document';
|
||||
import { updateEmbeddingEnvelopeRoute } from './update-embedding-envelope';
|
||||
import { updateEmbeddingTemplateRoute } from './update-embedding-template';
|
||||
import { verifyEmbeddingPresignTokenRoute } from './verify-embedding-presign-token';
|
||||
|
||||
export const embeddingPresignRouter = router({
|
||||
createEmbeddingPresignToken: createEmbeddingPresignTokenRoute,
|
||||
verifyEmbeddingPresignToken: verifyEmbeddingPresignTokenRoute,
|
||||
createEmbeddingEnvelope: createEmbeddingEnvelopeRoute,
|
||||
createEmbeddingDocument: createEmbeddingDocumentRoute,
|
||||
createEmbeddingTemplate: createEmbeddingTemplateRoute,
|
||||
updateEmbeddingEnvelope: updateEmbeddingEnvelopeRoute,
|
||||
updateEmbeddingDocument: updateEmbeddingDocumentRoute,
|
||||
updateEmbeddingTemplate: updateEmbeddingTemplateRoute,
|
||||
// applyMultiSignSignature: applyMultiSignSignatureRoute,
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
ZFieldWidthSchema,
|
||||
} from '@documenso/lib/types/field';
|
||||
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
import { DocumentSigningOrder } from '@documenso/prisma/generated/types';
|
||||
|
||||
@@ -33,7 +34,7 @@ export const ZCreateEmbeddingDocumentRequestSchema = z.object({
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
id: z.number().optional(),
|
||||
email: z.string().email(),
|
||||
email: zEmail(),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
@@ -41,7 +42,6 @@ export const ZCreateEmbeddingDocumentRequestSchema = z.object({
|
||||
// Search: "map<any>" to find it
|
||||
fields: ZFieldAndMetaSchema.and(
|
||||
z.object({
|
||||
id: z.number().optional(),
|
||||
pageNumber: ZFieldPageNumberSchema,
|
||||
pageX: ZFieldPageXSchema,
|
||||
pageY: ZFieldPageYSchema,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||
|
||||
import { createEnvelopeRouteCaller } from '../envelope-router/create-envelope';
|
||||
import { procedure } from '../trpc';
|
||||
import {
|
||||
ZCreateEmbeddingEnvelopeRequestSchema,
|
||||
ZCreateEmbeddingEnvelopeResponseSchema,
|
||||
} from './create-embedding-envelope.types';
|
||||
|
||||
export const createEmbeddingEnvelopeRoute = procedure
|
||||
.input(ZCreateEmbeddingEnvelopeRequestSchema)
|
||||
.output(ZCreateEmbeddingEnvelopeResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { req } = ctx;
|
||||
|
||||
const authorizationHeader = req.headers.get('authorization');
|
||||
|
||||
const [presignToken] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0);
|
||||
|
||||
if (!presignToken) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'No presign token provided',
|
||||
});
|
||||
}
|
||||
|
||||
const apiToken = await verifyEmbeddingPresignToken({ token: presignToken });
|
||||
|
||||
const { userId, teamId } = apiToken;
|
||||
|
||||
return await createEnvelopeRouteCaller({
|
||||
userId,
|
||||
teamId,
|
||||
input,
|
||||
options: {
|
||||
// Default recipients should be added on the frontend automatically for embeds.
|
||||
bypassDefaultRecipients: true,
|
||||
},
|
||||
apiRequestMetadata: ctx.metadata,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import {
|
||||
ZCreateEnvelopeRequestSchema,
|
||||
ZCreateEnvelopeResponseSchema,
|
||||
} from '../envelope-router/create-envelope.types';
|
||||
|
||||
export const ZCreateEmbeddingEnvelopeRequestSchema = ZCreateEnvelopeRequestSchema;
|
||||
|
||||
export const ZCreateEmbeddingEnvelopeResponseSchema = ZCreateEnvelopeResponseSchema;
|
||||
@@ -24,7 +24,9 @@ export const ZCreateEmbeddingPresignTokenRequestSchema = z.object({
|
||||
scope: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Resource restriction. Example: documentId:1, templateId:2'),
|
||||
.describe(
|
||||
'Resource restriction. V1 embeds only support documentId:1, templateId:2. V2 embeds only support envelopeId:envelope_123',
|
||||
),
|
||||
});
|
||||
|
||||
export const ZCreateEmbeddingPresignTokenResponseSchema = z.object({
|
||||
|
||||
@@ -39,7 +39,6 @@ export const ZCreateEmbeddingTemplateRequestSchema = z.object({
|
||||
// Search: "map<any>" to find it
|
||||
fields: ZFieldAndMetaSchema.and(
|
||||
z.object({
|
||||
id: z.number().optional(),
|
||||
pageNumber: ZFieldPageNumberSchema,
|
||||
pageX: ZFieldPageXSchema,
|
||||
pageY: ZFieldPageYSchema,
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
ZFieldWidthSchema,
|
||||
} from '@documenso/lib/types/field';
|
||||
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
import { DocumentSigningOrder, RecipientRole } from '@documenso/prisma/generated/types';
|
||||
|
||||
import { ZDocumentExternalIdSchema, ZDocumentTitleSchema } from '../document-router/schema';
|
||||
@@ -32,7 +33,7 @@ export const ZUpdateEmbeddingDocumentRequestSchema = z.object({
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
id: z.number().optional(),
|
||||
email: z.string().email(),
|
||||
email: zEmail(),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
|
||||
@@ -0,0 +1,517 @@
|
||||
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
import pMap from 'p-map';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||
import { UNSAFE_createEnvelopeItems } from '@documenso/lib/server-only/envelope-item/create-envelope-items';
|
||||
import { UNSAFE_deleteEnvelopeItem } from '@documenso/lib/server-only/envelope-item/delete-envelope-item';
|
||||
import { UNSAFE_replaceEnvelopeItemPdf } from '@documenso/lib/server-only/envelope-item/replace-envelope-item-pdf';
|
||||
import { UNSAFE_updateEnvelopeItems } from '@documenso/lib/server-only/envelope-item/update-envelope-items';
|
||||
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
|
||||
import { updateEnvelope } from '@documenso/lib/server-only/envelope/update-envelope';
|
||||
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
|
||||
import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-fields-for-template';
|
||||
import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients';
|
||||
import { setTemplateRecipients } from '@documenso/lib/server-only/recipient/set-template-recipients';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { PRESIGNED_ENVELOPE_ITEM_ID_PREFIX } from '@documenso/lib/utils/embed-config';
|
||||
import { getEnvelopeItemPermissions } from '@documenso/lib/utils/envelope';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { procedure } from '../trpc';
|
||||
import {
|
||||
ZUpdateEmbeddingEnvelopeRequestSchema,
|
||||
ZUpdateEmbeddingEnvelopeResponseSchema,
|
||||
} from './update-embedding-envelope.types';
|
||||
|
||||
export const updateEmbeddingEnvelopeRoute = procedure
|
||||
.input(ZUpdateEmbeddingEnvelopeRequestSchema)
|
||||
.output(ZUpdateEmbeddingEnvelopeResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { payload, files } = input;
|
||||
const { envelopeId, data, meta } = payload;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
envelopeId,
|
||||
},
|
||||
});
|
||||
|
||||
const authorizationHeader = ctx.req.headers.get('authorization');
|
||||
|
||||
const [presignToken] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0);
|
||||
|
||||
if (!presignToken) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'No presign token provided',
|
||||
});
|
||||
}
|
||||
|
||||
const apiToken = await verifyEmbeddingPresignToken({
|
||||
token: presignToken,
|
||||
scope: `envelopeId:${envelopeId}`,
|
||||
});
|
||||
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelopeId,
|
||||
},
|
||||
type: null, // Allow updating both documents and templates.
|
||||
userId: apiToken.userId,
|
||||
teamId: apiToken.teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
envelopeItems: true,
|
||||
team: {
|
||||
select: {
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
recipients: true,
|
||||
envelopeAttachments: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (envelope.status === DocumentStatus.COMPLETED) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Cannot modify completed envelope',
|
||||
});
|
||||
}
|
||||
|
||||
// Step 1: Update the envelope items.
|
||||
const envelopeItemsToUpdate: EnvelopeItemUpdateOptions[] = [];
|
||||
const envelopeItemsToCreate: EnvelopeItemCreateOptions[] = [];
|
||||
const envelopeItemsToReplace: EnvelopeItemReplaceOptions[] = [];
|
||||
|
||||
// Sort and group envelope items to update, create, and replace.
|
||||
data.envelopeItems.forEach((item) => {
|
||||
const isNewEnvelopeItem = item.id.startsWith(PRESIGNED_ENVELOPE_ITEM_ID_PREFIX);
|
||||
|
||||
// Handle existing envelope items.
|
||||
if (!isNewEnvelopeItem) {
|
||||
const envelopeItem = envelope.envelopeItems.find(
|
||||
(envelopeItem) => envelopeItem.id === item.id,
|
||||
);
|
||||
|
||||
if (!envelopeItem) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope item not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Check if this existing item has a replacement file.
|
||||
if (item.replaceFileIndex !== undefined) {
|
||||
const replaceFile = files[item.replaceFileIndex];
|
||||
|
||||
if (!replaceFile) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid replace file index',
|
||||
});
|
||||
}
|
||||
|
||||
envelopeItemsToReplace.push({
|
||||
envelopeItemId: envelopeItem.id,
|
||||
oldDocumentDataId: envelopeItem.documentDataId,
|
||||
title: item.title,
|
||||
order: item.order,
|
||||
file: replaceFile,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const hasEnvelopeItemChanged =
|
||||
envelopeItem.title !== item.title || envelopeItem.order !== item.order;
|
||||
|
||||
if (hasEnvelopeItemChanged) {
|
||||
envelopeItemsToUpdate.push({
|
||||
envelopeItemId: envelopeItem.id,
|
||||
title: item.title,
|
||||
order: item.order,
|
||||
});
|
||||
}
|
||||
|
||||
// Return to continue loop.
|
||||
return;
|
||||
}
|
||||
|
||||
const newEnvelopeItemFile = item.index !== undefined ? files[item.index] : undefined;
|
||||
|
||||
if (!newEnvelopeItemFile) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid envelope item index',
|
||||
});
|
||||
}
|
||||
|
||||
// Handle not yet uploaded envelope items.
|
||||
envelopeItemsToCreate.push({
|
||||
embeddedEnvelopeItemId: item.id,
|
||||
title: item.title,
|
||||
order: item.order,
|
||||
file: newEnvelopeItemFile,
|
||||
});
|
||||
});
|
||||
|
||||
// Delete envelope items that have been removed from the payload.
|
||||
const envelopeItemIdsToDelete = envelope.envelopeItems
|
||||
.filter((item) => !data.envelopeItems.some((i) => i.id === item.id))
|
||||
.map((item) => item.id);
|
||||
|
||||
const willEnvelopeItemsBeModified =
|
||||
envelopeItemIdsToDelete.length > 0 ||
|
||||
envelopeItemsToCreate.length > 0 ||
|
||||
envelopeItemsToUpdate.length > 0 ||
|
||||
envelopeItemsToReplace.length > 0;
|
||||
|
||||
const organisationClaim = envelope.team.organisation.organisationClaim;
|
||||
const resultingEnvelopeItemCount =
|
||||
envelope.envelopeItems.length - envelopeItemIdsToDelete.length + envelopeItemsToCreate.length;
|
||||
|
||||
if (resultingEnvelopeItemCount > organisationClaim.envelopeItemCount) {
|
||||
throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', {
|
||||
message: `You cannot upload more than ${organisationClaim.envelopeItemCount} envelope items`,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
// Should be safe to use stale envelope.recipients since only signed or sent
|
||||
// recipients affect the outcome.
|
||||
if (willEnvelopeItemsBeModified) {
|
||||
const permissions = getEnvelopeItemPermissions(envelope, envelope.recipients);
|
||||
|
||||
const hasFileChange = envelopeItemIdsToDelete.length > 0 || envelopeItemsToCreate.length > 0;
|
||||
|
||||
const hasOrderChange = envelopeItemsToUpdate.some((item) => {
|
||||
const existing = envelope.envelopeItems.find((e) => e.id === item.envelopeItemId);
|
||||
|
||||
return !existing || item.order !== existing.order;
|
||||
});
|
||||
|
||||
const hasTitleChange = envelopeItemsToUpdate.some((item) => item.title !== undefined);
|
||||
|
||||
if (hasFileChange && !permissions.canFileBeChanged) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Envelope item files are not editable',
|
||||
});
|
||||
}
|
||||
|
||||
if (hasOrderChange && !permissions.canOrderBeChanged) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Envelope item order is not editable',
|
||||
});
|
||||
}
|
||||
|
||||
if (hasTitleChange && !permissions.canTitleBeChanged) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Envelope item title is not editable',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (envelopeItemIdsToDelete.length > 0) {
|
||||
await pMap(
|
||||
envelopeItemIdsToDelete,
|
||||
async (envelopeItemId) => {
|
||||
await UNSAFE_deleteEnvelopeItem({
|
||||
envelopeId: envelope.id,
|
||||
envelopeItemId,
|
||||
user: apiToken.user,
|
||||
apiRequestMetadata: ctx.metadata,
|
||||
});
|
||||
},
|
||||
{ concurrency: 2 },
|
||||
);
|
||||
}
|
||||
|
||||
// Mapping for the client side embedded prefix envelope item IDs to the real envelope item IDs.
|
||||
const embeddedEnvelopeItemIdMapping: Record<string, string> = {};
|
||||
|
||||
// Create new envelope items.
|
||||
if (envelopeItemsToCreate.length > 0) {
|
||||
const createdEnvelopeItems = await UNSAFE_createEnvelopeItems({
|
||||
files: envelopeItemsToCreate.map((item) => ({
|
||||
clientId: item.embeddedEnvelopeItemId,
|
||||
file: item.file,
|
||||
orderOverride: item.order,
|
||||
})),
|
||||
envelope: {
|
||||
...envelope,
|
||||
// Purposefully putting empty recipients here since placeholders should automatically injected on the client side for
|
||||
// embedded purposes. Todo: Embeds - (Not implemeneted yet)
|
||||
recipients: [],
|
||||
},
|
||||
user: {
|
||||
id: apiToken.user.id,
|
||||
name: apiToken.user.name,
|
||||
email: apiToken.user.email,
|
||||
},
|
||||
apiRequestMetadata: ctx.metadata,
|
||||
});
|
||||
|
||||
// Build the map from the envelope item order.
|
||||
createdEnvelopeItems.forEach((item) => {
|
||||
if (!item.clientId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Client ID not found',
|
||||
});
|
||||
}
|
||||
|
||||
embeddedEnvelopeItemIdMapping[item.clientId] = item.id;
|
||||
});
|
||||
}
|
||||
|
||||
if (envelopeItemsToUpdate.length > 0) {
|
||||
await UNSAFE_updateEnvelopeItems({
|
||||
envelopeId: envelope.id,
|
||||
envelopeType: envelope.type,
|
||||
existingEnvelopeItems: envelope.envelopeItems,
|
||||
data: envelopeItemsToUpdate,
|
||||
user: {
|
||||
name: apiToken.user.name,
|
||||
email: apiToken.user.email,
|
||||
},
|
||||
apiRequestMetadata: ctx.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
// Replace PDFs for existing envelope items without creating placeholder fields
|
||||
// field cleanup is handled in later steps.
|
||||
if (envelopeItemsToReplace.length > 0) {
|
||||
await pMap(
|
||||
envelopeItemsToReplace,
|
||||
async (item) => {
|
||||
await UNSAFE_replaceEnvelopeItemPdf({
|
||||
envelope,
|
||||
recipients: [],
|
||||
envelopeItemId: item.envelopeItemId,
|
||||
oldDocumentDataId: item.oldDocumentDataId,
|
||||
data: {
|
||||
title: item.title,
|
||||
order: item.order,
|
||||
file: item.file,
|
||||
},
|
||||
user: apiToken.user,
|
||||
apiRequestMetadata: ctx.metadata,
|
||||
});
|
||||
},
|
||||
{ concurrency: 2 },
|
||||
);
|
||||
}
|
||||
|
||||
// Step 2: Update the general envelope data and meta.
|
||||
await updateEnvelope({
|
||||
userId: apiToken.userId,
|
||||
teamId: apiToken.teamId,
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelope.id,
|
||||
},
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId,
|
||||
visibility: data.visibility,
|
||||
globalAccessAuth: data.globalAccessAuth,
|
||||
globalActionAuth: data.globalActionAuth,
|
||||
folderId: data.folderId,
|
||||
},
|
||||
meta,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
|
||||
// Step 3: Update the recipients
|
||||
const recipientsWithClientId = data.recipients.map((recipient) => ({
|
||||
...recipient,
|
||||
clientId: nanoid(),
|
||||
}));
|
||||
|
||||
const { recipients: updatedRecipients } = await match(envelope.type)
|
||||
.with(EnvelopeType.DOCUMENT, async () =>
|
||||
setDocumentRecipients({
|
||||
userId: apiToken.userId,
|
||||
teamId: apiToken.teamId,
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelope.id,
|
||||
},
|
||||
recipients: recipientsWithClientId.map((recipient) => ({
|
||||
id: recipient.id,
|
||||
clientId: recipient.clientId,
|
||||
email: recipient.email,
|
||||
name: recipient.name ?? '',
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
actionAuth: recipient.actionAuth,
|
||||
})),
|
||||
requestMetadata: ctx.metadata,
|
||||
}),
|
||||
)
|
||||
.with(EnvelopeType.TEMPLATE, async () =>
|
||||
setTemplateRecipients({
|
||||
userId: apiToken.userId,
|
||||
teamId: apiToken.teamId,
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelope.id,
|
||||
},
|
||||
recipients: recipientsWithClientId.map((recipient) => ({
|
||||
id: recipient.id,
|
||||
clientId: recipient.clientId,
|
||||
email: recipient.email,
|
||||
name: recipient.name ?? '',
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
actionAuth: recipient.actionAuth,
|
||||
})),
|
||||
}),
|
||||
)
|
||||
.exhaustive();
|
||||
|
||||
// Step 4: Update the fields.
|
||||
const fields = recipientsWithClientId.flatMap((recipient) => {
|
||||
const recipientId = updatedRecipients.find((r) => r.clientId === recipient.clientId)?.id;
|
||||
|
||||
if (!recipientId) {
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Recipient not found',
|
||||
});
|
||||
}
|
||||
|
||||
return (recipient.fields ?? []).map((field) => {
|
||||
let envelopeItemId = field.envelopeItemId;
|
||||
|
||||
if (envelopeItemId.startsWith(PRESIGNED_ENVELOPE_ITEM_ID_PREFIX)) {
|
||||
envelopeItemId = embeddedEnvelopeItemIdMapping[envelopeItemId];
|
||||
}
|
||||
|
||||
if (!envelopeItemId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope item not found',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
recipientId,
|
||||
envelopeItemId,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
await match(envelope.type)
|
||||
.with(EnvelopeType.DOCUMENT, async () =>
|
||||
setFieldsForDocument({
|
||||
userId: apiToken.userId,
|
||||
teamId: apiToken.teamId,
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelopeId,
|
||||
},
|
||||
fields: fields.map((field) => ({
|
||||
...field,
|
||||
pageNumber: field.page,
|
||||
pageX: field.positionX,
|
||||
pageY: field.positionY,
|
||||
pageWidth: field.width,
|
||||
pageHeight: field.height,
|
||||
})),
|
||||
requestMetadata: ctx.metadata,
|
||||
}),
|
||||
)
|
||||
.with(EnvelopeType.TEMPLATE, async () =>
|
||||
setFieldsForTemplate({
|
||||
userId: apiToken.userId,
|
||||
teamId: apiToken.teamId,
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelopeId,
|
||||
},
|
||||
fields: fields.map((field) => ({
|
||||
...field,
|
||||
pageNumber: field.page,
|
||||
pageX: field.positionX,
|
||||
pageY: field.positionY,
|
||||
pageWidth: field.width,
|
||||
pageHeight: field.height,
|
||||
})),
|
||||
}),
|
||||
)
|
||||
.exhaustive();
|
||||
|
||||
// Step 5: Handle attachments (set semantics: delete all existing, create new).
|
||||
let hasEnvelopeAttachmentsChanged =
|
||||
envelope.envelopeAttachments.length !== data.attachments.length;
|
||||
|
||||
data.attachments.forEach((attachment) => {
|
||||
const foundAttachment = envelope.envelopeAttachments.find((a) => a.id === attachment.id);
|
||||
|
||||
if (!foundAttachment) {
|
||||
hasEnvelopeAttachmentsChanged = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const hasAttachmentChanged =
|
||||
foundAttachment.label !== attachment.label ||
|
||||
foundAttachment.data !== attachment.data ||
|
||||
foundAttachment.type !== attachment.type;
|
||||
|
||||
if (hasAttachmentChanged) {
|
||||
hasEnvelopeAttachmentsChanged = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasEnvelopeAttachmentsChanged) {
|
||||
await prisma.envelopeAttachment.deleteMany({
|
||||
where: {
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (data.attachments.length > 0) {
|
||||
await prisma.envelopeAttachment.createMany({
|
||||
data: data.attachments.map((attachment) => ({
|
||||
envelopeId: envelope.id,
|
||||
label: attachment.label,
|
||||
data: attachment.data,
|
||||
type: attachment.type,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
type EnvelopeItemUpdateOptions = {
|
||||
envelopeItemId: string;
|
||||
title?: string;
|
||||
order?: number;
|
||||
};
|
||||
|
||||
type EnvelopeItemCreateOptions = {
|
||||
embeddedEnvelopeItemId: string;
|
||||
title: string;
|
||||
order: number;
|
||||
file: File;
|
||||
};
|
||||
|
||||
type EnvelopeItemReplaceOptions = {
|
||||
envelopeItemId: string;
|
||||
oldDocumentDataId: string;
|
||||
title: string;
|
||||
order: number;
|
||||
file: File;
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
import { z } from 'zod';
|
||||
import { zfd } from 'zod-form-data';
|
||||
|
||||
import {
|
||||
ZDocumentAccessAuthTypesSchema,
|
||||
ZDocumentActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { ZDocumentMetaUpdateSchema } from '@documenso/lib/types/document-meta';
|
||||
import {
|
||||
ZClampedFieldHeightSchema,
|
||||
ZClampedFieldPositionXSchema,
|
||||
ZClampedFieldPositionYSchema,
|
||||
ZClampedFieldWidthSchema,
|
||||
ZFieldPageNumberSchema,
|
||||
} from '@documenso/lib/types/field';
|
||||
import { ZEnvelopeFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { EnvelopeAttachmentSchema } from '@documenso/prisma/generated/zod/modelSchema/EnvelopeAttachmentSchema';
|
||||
import { ZSetEnvelopeRecipientSchema } from '@documenso/trpc/server/envelope-router/set-envelope-recipients.types';
|
||||
|
||||
import { zfdFile, zodFormData } from '../../utils/zod-form-data';
|
||||
import {
|
||||
ZDocumentExternalIdSchema,
|
||||
ZDocumentTitleSchema,
|
||||
ZDocumentVisibilitySchema,
|
||||
} from '../document-router/schema';
|
||||
|
||||
export const ZUpdateEmbeddingEnvelopePayloadSchema = z.object({
|
||||
envelopeId: z.string(),
|
||||
data: z.object({
|
||||
title: ZDocumentTitleSchema.optional(),
|
||||
externalId: ZDocumentExternalIdSchema.nullish(),
|
||||
visibility: ZDocumentVisibilitySchema.optional(),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
||||
folderId: z.string().nullish(),
|
||||
|
||||
/**
|
||||
* The list of envelope items that are part of the envelope.
|
||||
*
|
||||
* Any missing IDs will be treated as deleting the envelope item.
|
||||
*/
|
||||
envelopeItems: z
|
||||
.object({
|
||||
/**
|
||||
* This is not necesssarily a real id, it can be a temporary id for the envelope item.
|
||||
*/
|
||||
id: z.string(),
|
||||
|
||||
/**
|
||||
* The title of the envelope item.
|
||||
*/
|
||||
title: z.string(),
|
||||
|
||||
/**
|
||||
* The order of the envelope item in the envelope.
|
||||
*/
|
||||
order: z.number().int().min(0),
|
||||
|
||||
/**
|
||||
* The file index for items that are not yet uploaded.
|
||||
*/
|
||||
index: z.number().int().min(0).optional(),
|
||||
|
||||
/**
|
||||
* The file index for existing items that need their PDF replaced.
|
||||
* Only applicable to items with real IDs (not PRESIGNED_ prefix).
|
||||
*/
|
||||
replaceFileIndex: z.number().int().min(0).optional(),
|
||||
})
|
||||
.refine((item) => !(item.index !== undefined && item.replaceFileIndex !== undefined), {
|
||||
message: 'Cannot provide both index and replaceFileIndex on the same envelope item',
|
||||
path: ['replaceFileIndex'],
|
||||
})
|
||||
.array(),
|
||||
|
||||
/**
|
||||
* This is a set command.
|
||||
*/
|
||||
recipients: ZSetEnvelopeRecipientSchema.extend({
|
||||
fields: ZEnvelopeFieldAndMetaSchema.and(
|
||||
z.object({
|
||||
id: z.number().optional(),
|
||||
page: ZFieldPageNumberSchema,
|
||||
positionX: ZClampedFieldPositionXSchema,
|
||||
positionY: ZClampedFieldPositionYSchema,
|
||||
width: ZClampedFieldWidthSchema,
|
||||
height: ZClampedFieldHeightSchema,
|
||||
envelopeItemId: z.string(),
|
||||
}),
|
||||
).array(),
|
||||
}).array(),
|
||||
|
||||
/**
|
||||
* The list of attachments for the envelope.
|
||||
*
|
||||
* This is a set command: when provided, all existing attachments are deleted
|
||||
* and replaced with the provided list.
|
||||
*/
|
||||
attachments: EnvelopeAttachmentSchema.pick({
|
||||
type: true,
|
||||
label: true,
|
||||
data: true,
|
||||
})
|
||||
.extend({
|
||||
id: z.string().optional(),
|
||||
})
|
||||
.array(),
|
||||
}),
|
||||
|
||||
meta: ZDocumentMetaUpdateSchema.optional(),
|
||||
});
|
||||
|
||||
export const ZUpdateEmbeddingEnvelopeRequestSchema = zodFormData({
|
||||
payload: zfd.json(ZUpdateEmbeddingEnvelopePayloadSchema),
|
||||
files: zfd.repeatableOfType(zfdFile()),
|
||||
});
|
||||
|
||||
export const ZUpdateEmbeddingEnvelopeResponseSchema = z.void();
|
||||
|
||||
export type TUpdateEmbeddingEnvelopePayload = z.infer<typeof ZUpdateEmbeddingEnvelopePayloadSchema>;
|
||||
export type TUpdateEmbeddingEnvelopeRequest = z.infer<typeof ZUpdateEmbeddingEnvelopeRequestSchema>;
|
||||
@@ -1,9 +1,11 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
|
||||
export const ZCreateOrganisationEmailRequestSchema = z.object({
|
||||
emailDomainId: z.string(),
|
||||
emailName: z.string().min(1).max(100),
|
||||
email: z.string().email().toLowerCase(),
|
||||
email: zEmail().toLowerCase(),
|
||||
|
||||
// This does not need to be validated to be part of the domain.
|
||||
// replyTo: z.string().email().optional(),
|
||||
|
||||
@@ -95,6 +95,7 @@ export const findOrganisationEmailDomains = async ({
|
||||
selector: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
lastVerifiedAt: true,
|
||||
_count: {
|
||||
select: {
|
||||
emails: true,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { linkOrganisationAccount } from '@documenso/ee/server-only/lib/link-organisation-account';
|
||||
import { assertRateLimit } from '@documenso/lib/server-only/rate-limit/rate-limit-middleware';
|
||||
import { linkOrgAccountRateLimit } from '@documenso/lib/server-only/rate-limit/rate-limits';
|
||||
|
||||
import { procedure } from '../trpc';
|
||||
import {
|
||||
@@ -15,6 +17,13 @@ export const linkOrganisationAccountRoute = procedure
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { token } = input;
|
||||
|
||||
const rateLimitResult = await linkOrgAccountRateLimit.check({
|
||||
ip: ctx.metadata.requestMetadata.ipAddress ?? 'unknown',
|
||||
identifier: token,
|
||||
});
|
||||
|
||||
assertRateLimit(rateLimitResult);
|
||||
|
||||
await linkOrganisationAccount({
|
||||
token,
|
||||
requestMeta: ctx.metadata.requestMetadata,
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import pMap from 'p-map';
|
||||
|
||||
import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
|
||||
import { getMultipleEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelopes-by-ids';
|
||||
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZBulkDeleteEnvelopesRequestSchema,
|
||||
ZBulkDeleteEnvelopesResponseSchema,
|
||||
} from './bulk-delete-envelopes.types';
|
||||
|
||||
export const bulkDeleteEnvelopesRoute = authenticatedProcedure
|
||||
// .meta(bulkDeleteEnvelopesMeta) // Keeping this as a private API for a little while until we're sure it's stable and the request/response schemas are finalized.
|
||||
.input(ZBulkDeleteEnvelopesRequestSchema)
|
||||
.output(ZBulkDeleteEnvelopesResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId, user } = ctx;
|
||||
const { envelopeIds } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
envelopeIds,
|
||||
},
|
||||
});
|
||||
|
||||
const { envelopeWhereInput } = await getMultipleEnvelopeWhereInput({
|
||||
ids: {
|
||||
type: 'envelopeId',
|
||||
ids: envelopeIds,
|
||||
},
|
||||
userId: user.id,
|
||||
teamId,
|
||||
type: null,
|
||||
});
|
||||
|
||||
const envelopes = await prisma.envelope.findMany({
|
||||
where: envelopeWhereInput,
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
},
|
||||
});
|
||||
|
||||
const results = await pMap(
|
||||
envelopes,
|
||||
async (envelope) => {
|
||||
const { id: envelopeId, type: envelopeType } = envelope;
|
||||
|
||||
try {
|
||||
if (envelopeType === EnvelopeType.DOCUMENT) {
|
||||
await deleteDocument({
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelopeId,
|
||||
},
|
||||
userId: user.id,
|
||||
teamId,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
} else if (envelopeType === EnvelopeType.TEMPLATE) {
|
||||
await deleteTemplate({
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelopeId,
|
||||
},
|
||||
userId: user.id,
|
||||
teamId,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
envelopeId,
|
||||
};
|
||||
} catch (err) {
|
||||
ctx.logger.warn(
|
||||
{
|
||||
envelopeId,
|
||||
error: err,
|
||||
},
|
||||
'Failed to delete envelope during bulk delete',
|
||||
);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
envelopeId,
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
concurrency: 10,
|
||||
stopOnError: false,
|
||||
},
|
||||
);
|
||||
|
||||
const deletedCount = results.filter((r) => r.success).length;
|
||||
const failedIds = results.filter((r) => !r.success).map((r) => r.envelopeId);
|
||||
|
||||
// Include envelope IDs that were not attempted (unauthorized/not found)
|
||||
const attemptedIds = new Set(envelopes.map((e) => e.id));
|
||||
const unattemptedIds = envelopeIds.filter((id) => !attemptedIds.has(id));
|
||||
|
||||
return {
|
||||
deletedCount,
|
||||
failedIds: [...failedIds, ...unattemptedIds],
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// READ ME: IF YOU UNCOMMENT THIS THEN UNSKIP THE TEST IN api-access-envelope-bulk.spec.ts
|
||||
// Keeping this as a private API for a little while until we're sure it's stable and the request/response schemas are finalized.
|
||||
// export const bulkDeleteEnvelopesMeta: TrpcRouteMeta = {
|
||||
// openapi: {
|
||||
// method: 'POST',
|
||||
// path: '/envelope/bulk/delete',
|
||||
// summary: 'Bulk delete envelopes',
|
||||
// description: 'Delete multiple envelopes.',
|
||||
// tags: ['Envelopes'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZBulkDeleteEnvelopesRequestSchema = z.object({
|
||||
envelopeIds: z
|
||||
.array(z.string())
|
||||
.min(1)
|
||||
.max(100)
|
||||
.describe(
|
||||
'The IDs of the envelopes to delete. The maximum number of envelopes you can delete at once is 100.',
|
||||
),
|
||||
});
|
||||
|
||||
export const ZBulkDeleteEnvelopesResponseSchema = z.object({
|
||||
deletedCount: z.number(),
|
||||
failedIds: z.array(z.string()),
|
||||
});
|
||||
|
||||
export type TBulkDeleteEnvelopesRequest = z.infer<typeof ZBulkDeleteEnvelopesRequestSchema>;
|
||||
export type TBulkDeleteEnvelopesResponse = z.infer<typeof ZBulkDeleteEnvelopesResponseSchema>;
|
||||
@@ -0,0 +1,73 @@
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '@documenso/lib/constants/teams';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getMultipleEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelopes-by-ids';
|
||||
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZBulkMoveEnvelopesRequestSchema,
|
||||
ZBulkMoveEnvelopesResponseSchema,
|
||||
} from './bulk-move-envelopes.types';
|
||||
|
||||
export const bulkMoveEnvelopesRoute = authenticatedProcedure
|
||||
// .meta(bulkMoveEnvelopesMeta)
|
||||
.input(ZBulkMoveEnvelopesRequestSchema)
|
||||
.output(ZBulkMoveEnvelopesResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId, user } = ctx;
|
||||
const { envelopeIds, envelopeType, folderId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
envelopeIds,
|
||||
envelopeType,
|
||||
folderId,
|
||||
},
|
||||
});
|
||||
|
||||
// Build the where input for the update query.
|
||||
const { envelopeWhereInput, team } = await getMultipleEnvelopeWhereInput({
|
||||
ids: {
|
||||
type: 'envelopeId',
|
||||
ids: envelopeIds,
|
||||
},
|
||||
userId: user.id,
|
||||
teamId,
|
||||
type: envelopeType,
|
||||
});
|
||||
|
||||
// Validate folder access if moving to a folder (not root).
|
||||
if (folderId) {
|
||||
const folder = await prisma.folder.findFirst({
|
||||
where: {
|
||||
id: folderId,
|
||||
team: buildTeamWhereQuery({
|
||||
teamId,
|
||||
userId: user.id,
|
||||
}),
|
||||
type: envelopeType,
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!folder) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Folder not found or access denied',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result = await prisma.envelope.updateMany({
|
||||
where: envelopeWhereInput,
|
||||
data: {
|
||||
folderId: folderId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
movedCount: result.count,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
// READ ME: IF YOU UNCOMMENT THIS THEN UNSKIP THE TEST IN api-access-envelope-bulk.spec.ts
|
||||
// Keeping this as a private API for a little while until we're sure it's stable and the request/response schemas are finalized.
|
||||
// export const bulkMoveEnvelopesMeta: TrpcRouteMeta = {
|
||||
// openapi: {
|
||||
// method: 'POST',
|
||||
// path: '/envelope/bulk/move',
|
||||
// summary: 'Bulk move envelopes to folder',
|
||||
// description: 'Move multiple envelopes to a specified folder.',
|
||||
// tags: ['Envelopes'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZBulkMoveEnvelopesRequestSchema = z.object({
|
||||
envelopeIds: z
|
||||
.array(z.string())
|
||||
.min(1)
|
||||
.max(100)
|
||||
.describe(
|
||||
'The IDs of the envelopes to move. The maximum number of envelopes you can move at once is 100.',
|
||||
),
|
||||
envelopeType: z.nativeEnum(EnvelopeType).describe('The type of the envelopes being moved.'),
|
||||
folderId: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe(
|
||||
'The ID of the folder to move the envelopes to. If null envelopes will be moved to the root folder.',
|
||||
),
|
||||
});
|
||||
|
||||
export const ZBulkMoveEnvelopesResponseSchema = z.object({
|
||||
movedCount: z.number().describe('The number of envelopes that were moved.'),
|
||||
});
|
||||
|
||||
export type TBulkMoveEnvelopesRequest = z.infer<typeof ZBulkMoveEnvelopesRequestSchema>;
|
||||
export type TBulkMoveEnvelopesResponse = z.infer<typeof ZBulkMoveEnvelopesResponseSchema>;
|
||||
@@ -1,10 +1,7 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { UNSAFE_createEnvelopeItems } from '@documenso/lib/server-only/envelope-item/create-envelope-items';
|
||||
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import { prefixedId } from '@documenso/lib/universal/id';
|
||||
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
|
||||
import { getEnvelopeItemPermissions } from '@documenso/lib/utils/envelope';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
@@ -66,7 +63,9 @@ export const createEnvelopeItemsRoute = authenticatedProcedure
|
||||
});
|
||||
}
|
||||
|
||||
if (!canEnvelopeItemsBeModified(envelope, envelope.recipients)) {
|
||||
const { canFileBeChanged } = getEnvelopeItemPermissions(envelope, envelope.recipients);
|
||||
|
||||
if (!canFileBeChanged) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Envelope item is not editable',
|
||||
});
|
||||
@@ -84,54 +83,17 @@ export const createEnvelopeItemsRoute = authenticatedProcedure
|
||||
});
|
||||
}
|
||||
|
||||
// For each file, stream to s3 and create the document data.
|
||||
const envelopeItems = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const { id: documentDataId } = await putNormalizedPdfFileServerSide({ file });
|
||||
|
||||
return {
|
||||
title: file.name,
|
||||
documentDataId,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const currentHighestOrderValue =
|
||||
envelope.envelopeItems[envelope.envelopeItems.length - 1]?.order ?? 1;
|
||||
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const createdItems = await tx.envelopeItem.createManyAndReturn({
|
||||
data: envelopeItems.map((item) => ({
|
||||
id: prefixedId('envelope_item'),
|
||||
envelopeId,
|
||||
title: item.title,
|
||||
documentDataId: item.documentDataId,
|
||||
order: currentHighestOrderValue + 1,
|
||||
})),
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.createMany({
|
||||
data: createdItems.map((item) =>
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_CREATED,
|
||||
envelopeId: envelope.id,
|
||||
data: {
|
||||
envelopeItemId: item.id,
|
||||
envelopeItemTitle: item.title,
|
||||
},
|
||||
user: {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
},
|
||||
requestMetadata: metadata.requestMetadata,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
return createdItems;
|
||||
const result = await UNSAFE_createEnvelopeItems({
|
||||
files: files.map((file) => ({
|
||||
file,
|
||||
})),
|
||||
envelope,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
},
|
||||
apiRequestMetadata: metadata,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { zfd } from 'zod-form-data';
|
||||
|
||||
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
|
||||
|
||||
import { zodFormData } from '../../utils/zod-form-data';
|
||||
import { zfdFile, zodFormData } from '../../utils/zod-form-data';
|
||||
import type { TrpcRouteMeta } from '../trpc';
|
||||
|
||||
export const createEnvelopeItemsMeta: TrpcRouteMeta = {
|
||||
@@ -24,7 +24,7 @@ export const ZCreateEnvelopeItemsPayloadSchema = z.object({
|
||||
|
||||
export const ZCreateEnvelopeItemsRequestSchema = zodFormData({
|
||||
payload: zfd.json(ZCreateEnvelopeItemsPayloadSchema),
|
||||
files: zfd.repeatableOfType(zfd.file()),
|
||||
files: zfd.repeatableOfType(zfdFile()),
|
||||
});
|
||||
|
||||
export const ZCreateEnvelopeItemsResponseSchema = z.object({
|
||||
@@ -33,6 +33,7 @@ export const ZCreateEnvelopeItemsResponseSchema = z.object({
|
||||
title: true,
|
||||
envelopeId: true,
|
||||
order: true,
|
||||
documentDataId: true,
|
||||
}).array(),
|
||||
});
|
||||
|
||||
|
||||
@@ -4,13 +4,17 @@ import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
|
||||
import { convertToPdfIfNeeded } from '@documenso/lib/server-only/file-conversion/convert-to-pdf';
|
||||
import { extractPdfPlaceholders } from '@documenso/lib/server-only/pdf/auto-place-fields';
|
||||
import { normalizePdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import {
|
||||
putFileServerSide,
|
||||
putNormalizedPdfFileServerSide,
|
||||
putPdfFileServerSide,
|
||||
} from '@documenso/lib/universal/upload/put-file.server';
|
||||
|
||||
import { insertFormValuesInPdf } from '../../../lib/server-only/pdf/insert-form-values-in-pdf';
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import type { TCreateEnvelopeRequest } from './create-envelope.types';
|
||||
import {
|
||||
ZCreateEnvelopeRequestSchema,
|
||||
ZCreateEnvelopeResponseSchema,
|
||||
@@ -22,152 +26,194 @@ export const createEnvelopeRoute = authenticatedProcedure
|
||||
.input(ZCreateEnvelopeRequestSchema)
|
||||
.output(ZCreateEnvelopeResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { user, teamId } = ctx;
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
folderId: input.payload.folderId,
|
||||
},
|
||||
});
|
||||
|
||||
const { payload, files } = input;
|
||||
return await createEnvelopeRouteCaller({
|
||||
userId: ctx.user.id,
|
||||
teamId: ctx.teamId,
|
||||
input,
|
||||
apiRequestMetadata: ctx.metadata,
|
||||
});
|
||||
});
|
||||
|
||||
const {
|
||||
title,
|
||||
type CreateEnvelopeRouteOptions = {
|
||||
/**
|
||||
* Verified user ID.
|
||||
*/
|
||||
userId: number;
|
||||
|
||||
/**
|
||||
* Unverified team ID.
|
||||
*/
|
||||
teamId: number;
|
||||
input: TCreateEnvelopeRequest;
|
||||
apiRequestMetadata: ApiRequestMetadata;
|
||||
|
||||
options?: {
|
||||
bypassDefaultRecipients?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export const createEnvelopeRouteCaller = async ({
|
||||
userId,
|
||||
teamId,
|
||||
input,
|
||||
apiRequestMetadata,
|
||||
options = {},
|
||||
}: CreateEnvelopeRouteOptions) => {
|
||||
const { payload, files } = input;
|
||||
|
||||
const {
|
||||
title,
|
||||
type,
|
||||
externalId,
|
||||
visibility,
|
||||
globalAccessAuth,
|
||||
globalActionAuth,
|
||||
formValues,
|
||||
recipients,
|
||||
folderId,
|
||||
meta,
|
||||
attachments,
|
||||
delegatedDocumentOwner,
|
||||
} = payload;
|
||||
|
||||
const { remaining, maximumEnvelopeItemCount } = await getServerLimits({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (remaining.documents <= 0) {
|
||||
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
|
||||
message: 'You have reached your document limit for this month. Please upgrade your plan.',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (files.length > maximumEnvelopeItemCount) {
|
||||
throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', {
|
||||
message: `You cannot upload more than ${maximumEnvelopeItemCount} envelope items per envelope`,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
// For each file: convert to PDF if needed, normalize, extract & clean placeholders, then upload.
|
||||
const envelopeItems = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const { pdfBuffer, originalBuffer, originalMimeType } = await convertToPdfIfNeeded(file);
|
||||
|
||||
let pdf = pdfBuffer;
|
||||
|
||||
if (formValues) {
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
pdf = await insertFormValuesInPdf({
|
||||
pdf,
|
||||
formValues,
|
||||
});
|
||||
}
|
||||
|
||||
const normalized = await normalizePdf(pdf, {
|
||||
flattenForm: type !== EnvelopeType.TEMPLATE,
|
||||
});
|
||||
|
||||
// Todo: Embeds - Might need to add this for client-side embeds in the future.
|
||||
const { cleanedPdf, placeholders } = await extractPdfPlaceholders(normalized);
|
||||
|
||||
let originalData: string | undefined;
|
||||
if (originalBuffer) {
|
||||
const stored = await putFileServerSide({
|
||||
name: `original-${file.name}`,
|
||||
type: originalMimeType,
|
||||
arrayBuffer: async () => Promise.resolve(originalBuffer),
|
||||
});
|
||||
originalData = stored.data;
|
||||
}
|
||||
|
||||
const { documentData } = await putPdfFileServerSide(
|
||||
{
|
||||
name: file.name,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(cleanedPdf),
|
||||
},
|
||||
{
|
||||
originalData,
|
||||
originalMimeType: originalBuffer ? originalMimeType : undefined,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
title: file.name,
|
||||
documentDataId: documentData.id,
|
||||
placeholders,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const recipientsToCreate = recipients?.map((recipient) => ({
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
accessAuth: recipient.accessAuth,
|
||||
actionAuth: recipient.actionAuth,
|
||||
fields: recipient.fields?.map((field) => {
|
||||
let documentDataId: string | undefined = undefined;
|
||||
|
||||
if (typeof field.identifier === 'string') {
|
||||
documentDataId = envelopeItems.find(
|
||||
(item) => item.title === field.identifier,
|
||||
)?.documentDataId;
|
||||
}
|
||||
|
||||
if (typeof field.identifier === 'number') {
|
||||
documentDataId = envelopeItems.at(field.identifier)?.documentDataId;
|
||||
}
|
||||
|
||||
if (field.identifier === undefined) {
|
||||
documentDataId = envelopeItems.at(0)?.documentDataId;
|
||||
}
|
||||
|
||||
if (!documentDataId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document data not found',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
documentDataId,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
const envelope = await createEnvelope({
|
||||
userId,
|
||||
teamId,
|
||||
internalVersion: 2,
|
||||
data: {
|
||||
type,
|
||||
title,
|
||||
externalId,
|
||||
formValues,
|
||||
visibility,
|
||||
globalAccessAuth,
|
||||
globalActionAuth,
|
||||
formValues,
|
||||
recipients,
|
||||
recipients: recipientsToCreate,
|
||||
folderId,
|
||||
meta,
|
||||
attachments,
|
||||
envelopeItems,
|
||||
delegatedDocumentOwner,
|
||||
} = payload;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
folderId,
|
||||
},
|
||||
});
|
||||
|
||||
const { remaining, maximumEnvelopeItemCount } = await getServerLimits({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (remaining.documents <= 0) {
|
||||
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
|
||||
message: 'You have reached your document limit for this month. Please upgrade your plan.',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (files.length > maximumEnvelopeItemCount) {
|
||||
throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', {
|
||||
message: `You cannot upload more than ${maximumEnvelopeItemCount} envelope items per envelope`,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const envelopeItems = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const { pdfBuffer, originalBuffer, originalMimeType } = await convertToPdfIfNeeded(file);
|
||||
|
||||
let pdf = pdfBuffer;
|
||||
|
||||
if (formValues) {
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
pdf = await insertFormValuesInPdf({
|
||||
pdf,
|
||||
formValues,
|
||||
});
|
||||
}
|
||||
|
||||
let originalData: string | undefined;
|
||||
if (originalBuffer) {
|
||||
const stored = await putFileServerSide({
|
||||
name: `original-${file.name}`,
|
||||
type: originalMimeType,
|
||||
arrayBuffer: async () => Promise.resolve(originalBuffer),
|
||||
});
|
||||
originalData = stored.data;
|
||||
}
|
||||
|
||||
const { id: documentDataId } = await putNormalizedPdfFileServerSide({
|
||||
file: {
|
||||
name: file.name,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(pdf),
|
||||
},
|
||||
originalData,
|
||||
originalMimeType,
|
||||
flattenForm: type !== EnvelopeType.TEMPLATE,
|
||||
});
|
||||
|
||||
return {
|
||||
title: file.name,
|
||||
documentDataId,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const recipientsToCreate = recipients?.map((recipient) => ({
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
accessAuth: recipient.accessAuth,
|
||||
actionAuth: recipient.actionAuth,
|
||||
fields: recipient.fields?.map((field) => {
|
||||
let documentDataId: string | undefined = undefined;
|
||||
|
||||
if (typeof field.identifier === 'string') {
|
||||
documentDataId = envelopeItems.find(
|
||||
(item) => item.title === field.identifier,
|
||||
)?.documentDataId;
|
||||
}
|
||||
|
||||
if (typeof field.identifier === 'number') {
|
||||
documentDataId = envelopeItems.at(field.identifier)?.documentDataId;
|
||||
}
|
||||
|
||||
if (field.identifier === undefined) {
|
||||
documentDataId = envelopeItems.at(0)?.documentDataId;
|
||||
}
|
||||
|
||||
if (!documentDataId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document data not found',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
documentDataId,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
const envelope = await createEnvelope({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
internalVersion: 2,
|
||||
data: {
|
||||
type,
|
||||
title,
|
||||
externalId,
|
||||
formValues,
|
||||
visibility,
|
||||
globalAccessAuth,
|
||||
globalActionAuth,
|
||||
recipients: recipientsToCreate,
|
||||
folderId,
|
||||
envelopeItems,
|
||||
delegatedDocumentOwner,
|
||||
},
|
||||
attachments,
|
||||
meta,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
|
||||
return {
|
||||
id: envelope.id,
|
||||
};
|
||||
},
|
||||
attachments,
|
||||
meta,
|
||||
requestMetadata: apiRequestMetadata,
|
||||
bypassDefaultRecipients: options.bypassDefaultRecipients,
|
||||
});
|
||||
|
||||
return {
|
||||
id: envelope.id,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -17,8 +17,9 @@ import {
|
||||
ZFieldPageNumberSchema,
|
||||
} from '@documenso/lib/types/field';
|
||||
import { ZEnvelopeFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
|
||||
import { zodFormData } from '../../utils/zod-form-data';
|
||||
import { zfdFile, zodFormData } from '../../utils/zod-form-data';
|
||||
import {
|
||||
ZDocumentExternalIdSchema,
|
||||
ZDocumentTitleSchema,
|
||||
@@ -41,9 +42,7 @@ export const createEnvelopeMeta: TrpcRouteMeta = {
|
||||
export const ZCreateEnvelopePayloadSchema = z.object({
|
||||
title: ZDocumentTitleSchema,
|
||||
type: z.nativeEnum(EnvelopeType),
|
||||
delegatedDocumentOwner: z
|
||||
.string()
|
||||
.email()
|
||||
delegatedDocumentOwner: zEmail()
|
||||
.describe('The email of the user who will own the document.')
|
||||
.optional(),
|
||||
externalId: ZDocumentExternalIdSchema.optional(),
|
||||
@@ -94,7 +93,7 @@ export const ZCreateEnvelopePayloadSchema = z.object({
|
||||
|
||||
export const ZCreateEnvelopeRequestSchema = zodFormData({
|
||||
payload: zfd.json(ZCreateEnvelopePayloadSchema),
|
||||
files: zfd.repeatableOfType(zfd.file()),
|
||||
files: zfd.repeatableOfType(zfdFile()),
|
||||
});
|
||||
|
||||
export const ZCreateEnvelopeResponseSchema = z.object({
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { UNSAFE_deleteEnvelopeItem } from '@documenso/lib/server-only/envelope-item/delete-envelope-item';
|
||||
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
|
||||
import { getEnvelopeItemPermissions } from '@documenso/lib/utils/envelope';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { ZGenericSuccessResponse } from '../schema';
|
||||
@@ -51,55 +50,19 @@ export const deleteEnvelopeItemRoute = authenticatedProcedure
|
||||
});
|
||||
}
|
||||
|
||||
if (!canEnvelopeItemsBeModified(envelope, envelope.recipients)) {
|
||||
const { canFileBeChanged } = getEnvelopeItemPermissions(envelope, envelope.recipients);
|
||||
|
||||
if (!canFileBeChanged) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Envelope item is not editable',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const deletedEnvelopeItem = await tx.envelopeItem.delete({
|
||||
where: {
|
||||
id: envelopeItemId,
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
documentData: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_DELETED,
|
||||
envelopeId: envelope.id,
|
||||
data: {
|
||||
envelopeItemId: deletedEnvelopeItem.id,
|
||||
envelopeItemTitle: deletedEnvelopeItem.title,
|
||||
},
|
||||
user: {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
},
|
||||
requestMetadata: metadata.requestMetadata,
|
||||
}),
|
||||
});
|
||||
|
||||
return deletedEnvelopeItem;
|
||||
});
|
||||
|
||||
await prisma.documentData.delete({
|
||||
where: {
|
||||
id: result.documentData.id,
|
||||
envelopeItem: {
|
||||
is: null,
|
||||
},
|
||||
},
|
||||
await UNSAFE_deleteEnvelopeItem({
|
||||
envelopeId,
|
||||
envelopeItemId,
|
||||
user,
|
||||
apiRequestMetadata: metadata,
|
||||
});
|
||||
|
||||
return ZGenericSuccessResponse;
|
||||
|
||||
+44
-6
@@ -22,7 +22,7 @@ export const createEnvelopeFieldsMeta: TrpcRouteMeta = {
|
||||
},
|
||||
};
|
||||
|
||||
const ZCreateFieldSchema = ZEnvelopeFieldAndMetaSchema.and(
|
||||
const ZCreateFieldBaseSchema = ZEnvelopeFieldAndMetaSchema.and(
|
||||
z.object({
|
||||
recipientId: z.number().describe('The ID of the recipient to create the field for'),
|
||||
envelopeItemId: z
|
||||
@@ -31,14 +31,52 @@ const ZCreateFieldSchema = ZEnvelopeFieldAndMetaSchema.and(
|
||||
.describe(
|
||||
'The ID of the envelope item to put the field on. If not provided, field will be placed on the first item.',
|
||||
),
|
||||
page: ZFieldPageNumberSchema,
|
||||
positionX: ZClampedFieldPositionXSchema,
|
||||
positionY: ZClampedFieldPositionYSchema,
|
||||
width: ZClampedFieldWidthSchema,
|
||||
height: ZClampedFieldHeightSchema,
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Position a field using explicit percentage-based coordinates.
|
||||
*/
|
||||
export const ZCoordinatePositionSchema = z.object({
|
||||
page: ZFieldPageNumberSchema,
|
||||
positionX: ZClampedFieldPositionXSchema,
|
||||
positionY: ZClampedFieldPositionYSchema,
|
||||
width: ZClampedFieldWidthSchema,
|
||||
height: ZClampedFieldHeightSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* Position a field using a PDF text placeholder (e.g. "{{name}}").
|
||||
*
|
||||
* The placeholder text is matched in the envelope item's PDF and the field is
|
||||
* placed at the bounding box of that match. Width and height can optionally be
|
||||
* overridden; when omitted the dimensions of the placeholder text are used.
|
||||
*/
|
||||
export const ZPlaceholderPositionSchema = z.object({
|
||||
placeholder: z
|
||||
.string()
|
||||
.describe(
|
||||
'Text to search for in the PDF (e.g. "{{name}}"). The field will be placed at the location of this text.',
|
||||
),
|
||||
width: ZClampedFieldWidthSchema.optional().describe(
|
||||
'Override the width of the field. When omitted, the width of the placeholder text is used.',
|
||||
),
|
||||
height: ZClampedFieldHeightSchema.optional().describe(
|
||||
'Override the height of the field. When omitted, the height of the placeholder text is used.',
|
||||
),
|
||||
matchAll: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
'When true, creates a field at every occurrence of the placeholder in the PDF. When false or omitted, only the first occurrence is used.',
|
||||
),
|
||||
});
|
||||
|
||||
const ZCreateFieldSchema = z.union([
|
||||
ZCreateFieldBaseSchema.and(ZCoordinatePositionSchema),
|
||||
ZCreateFieldBaseSchema.and(ZPlaceholderPositionSchema),
|
||||
]);
|
||||
|
||||
export const ZCreateEnvelopeFieldsRequestSchema = z.object({
|
||||
envelopeId: z.string(),
|
||||
data: ZCreateFieldSchema.array(),
|
||||
|
||||
+12
-14
@@ -1,16 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
ZClampedFieldHeightSchema,
|
||||
ZClampedFieldPositionXSchema,
|
||||
ZClampedFieldPositionYSchema,
|
||||
ZClampedFieldWidthSchema,
|
||||
ZFieldPageNumberSchema,
|
||||
ZFieldSchema,
|
||||
} from '@documenso/lib/types/field';
|
||||
import { ZFieldSchema } from '@documenso/lib/types/field';
|
||||
import { ZEnvelopeFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
|
||||
import type { TrpcRouteMeta } from '../../trpc';
|
||||
import { ZCoordinatePositionSchema } from './create-envelope-fields.types';
|
||||
|
||||
export const updateEnvelopeFieldsMeta: TrpcRouteMeta = {
|
||||
openapi: {
|
||||
@@ -22,7 +16,7 @@ export const updateEnvelopeFieldsMeta: TrpcRouteMeta = {
|
||||
},
|
||||
};
|
||||
|
||||
const ZUpdateFieldSchema = ZEnvelopeFieldAndMetaSchema.and(
|
||||
const ZUpdateFieldBaseSchema = ZEnvelopeFieldAndMetaSchema.and(
|
||||
z.object({
|
||||
id: z.number().describe('The ID of the field to update.'),
|
||||
envelopeItemId: z
|
||||
@@ -31,14 +25,18 @@ const ZUpdateFieldSchema = ZEnvelopeFieldAndMetaSchema.and(
|
||||
.describe(
|
||||
'The ID of the envelope item to put the field on. If not provided, field will be placed on the first item.',
|
||||
),
|
||||
page: ZFieldPageNumberSchema.optional(),
|
||||
positionX: ZClampedFieldPositionXSchema.optional(),
|
||||
positionY: ZClampedFieldPositionYSchema.optional(),
|
||||
width: ZClampedFieldWidthSchema.optional(),
|
||||
height: ZClampedFieldHeightSchema.optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
// !: Later on we should support placeholders for updates, but since we didn't do the
|
||||
// !: neccesary plumbing for updates in the initial release, we can hold off for now.
|
||||
// const ZUpdateFieldSchema = z.union([
|
||||
// ZUpdateFieldBaseSchema.and(ZCoordinatePositionSchema),
|
||||
// ZUpdateFieldBaseSchema.and(ZPlaceholderPositionSchema),
|
||||
// ]);
|
||||
|
||||
const ZUpdateFieldSchema = ZUpdateFieldBaseSchema.and(ZCoordinatePositionSchema.partial());
|
||||
|
||||
export const ZUpdateEnvelopeFieldsRequestSchema = z.object({
|
||||
envelopeId: z.string(),
|
||||
data: ZUpdateFieldSchema.array(),
|
||||
|
||||
@@ -52,5 +52,6 @@ export const findEnvelopesRoute = authenticatedProcedure
|
||||
perPage,
|
||||
folderId,
|
||||
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
|
||||
useWindowedCount: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { getEditorEnvelopeById } from '@documenso/lib/server-only/envelope/get-editor-envelope-by-id';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetEditorEnvelopeRequestSchema,
|
||||
ZGetEditorEnvelopeResponseSchema,
|
||||
} from './get-editor-envelope.types';
|
||||
|
||||
export const getEditorEnvelopeRoute = authenticatedProcedure
|
||||
.input(ZGetEditorEnvelopeRequestSchema)
|
||||
.output(ZGetEditorEnvelopeResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { teamId, user } = ctx;
|
||||
const { envelopeId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
envelopeId,
|
||||
},
|
||||
});
|
||||
|
||||
return await getEditorEnvelopeById({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelopeId,
|
||||
},
|
||||
type: null,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZEditorEnvelopeSchema } from '@documenso/lib/types/envelope-editor';
|
||||
|
||||
export const ZGetEditorEnvelopeRequestSchema = z.object({
|
||||
envelopeId: z.string(),
|
||||
});
|
||||
|
||||
export const ZGetEditorEnvelopeResponseSchema = ZEditorEnvelopeSchema;
|
||||
|
||||
export type TGetEditorEnvelopeRequest = z.infer<typeof ZGetEditorEnvelopeRequestSchema>;
|
||||
export type TGetEditorEnvelopeResponse = z.infer<typeof ZGetEditorEnvelopeResponseSchema>;
|
||||
@@ -0,0 +1,105 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { UNSAFE_replaceEnvelopeItemPdf } from '@documenso/lib/server-only/envelope-item/replace-envelope-item-pdf';
|
||||
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
|
||||
import { getEnvelopeItemPermissions } from '@documenso/lib/utils/envelope';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZReplaceEnvelopeItemPdfRequestSchema,
|
||||
ZReplaceEnvelopeItemPdfResponseSchema,
|
||||
} from './replace-envelope-item-pdf.types';
|
||||
|
||||
/**
|
||||
* Keep this internal for the envelope editor.
|
||||
*
|
||||
* If we want to make this public then create a separate one that only allows
|
||||
* the PDF to be replaced & doesn't return deleted fields, etc.
|
||||
*/
|
||||
export const replaceEnvelopeItemPdfRoute = authenticatedProcedure
|
||||
.input(ZReplaceEnvelopeItemPdfRequestSchema)
|
||||
.output(ZReplaceEnvelopeItemPdfResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { user, teamId, metadata } = ctx;
|
||||
const { payload, file } = input;
|
||||
const { envelopeId, envelopeItemId, title } = payload;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
envelopeId,
|
||||
envelopeItemId,
|
||||
},
|
||||
});
|
||||
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelopeId,
|
||||
},
|
||||
type: null,
|
||||
userId: user.id,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
envelopeItems: {
|
||||
orderBy: {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (envelope.internalVersion !== 2) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'PDF replacement is only supported for version 2 envelopes',
|
||||
});
|
||||
}
|
||||
|
||||
const { canFileBeChanged } = getEnvelopeItemPermissions(envelope, envelope.recipients);
|
||||
|
||||
if (!canFileBeChanged) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Envelope item is not editable',
|
||||
});
|
||||
}
|
||||
|
||||
const envelopeItem = envelope.envelopeItems.find((item) => item.id === envelopeItemId);
|
||||
|
||||
if (!envelopeItem) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope item not found',
|
||||
});
|
||||
}
|
||||
|
||||
const { updatedItem, fields } = await UNSAFE_replaceEnvelopeItemPdf({
|
||||
envelope,
|
||||
recipients: envelope.recipients,
|
||||
envelopeItemId,
|
||||
oldDocumentDataId: envelopeItem.documentDataId,
|
||||
data: {
|
||||
file,
|
||||
title,
|
||||
},
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
},
|
||||
apiRequestMetadata: metadata,
|
||||
});
|
||||
|
||||
return {
|
||||
data: updatedItem,
|
||||
fields,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { z } from 'zod';
|
||||
import { zfd } from 'zod-form-data';
|
||||
|
||||
import { ZEnvelopeFieldSchema } from '@documenso/lib/types/field';
|
||||
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
|
||||
|
||||
import { zfdFile, zodFormData } from '../../utils/zod-form-data';
|
||||
import { ZDocumentTitleSchema } from '../document-router/schema';
|
||||
|
||||
export const ZReplaceEnvelopeItemPdfPayloadSchema = z.object({
|
||||
envelopeId: z.string(),
|
||||
envelopeItemId: z.string(),
|
||||
title: ZDocumentTitleSchema.optional(),
|
||||
});
|
||||
|
||||
export const ZReplaceEnvelopeItemPdfRequestSchema = zodFormData({
|
||||
payload: zfd.json(ZReplaceEnvelopeItemPdfPayloadSchema),
|
||||
file: zfdFile(),
|
||||
});
|
||||
|
||||
export const ZReplaceEnvelopeItemPdfResponseSchema = z.object({
|
||||
data: EnvelopeItemSchema.pick({
|
||||
id: true,
|
||||
title: true,
|
||||
envelopeId: true,
|
||||
order: true,
|
||||
documentDataId: true,
|
||||
}),
|
||||
/**
|
||||
* The full list of fields for the envelope after the replacement.
|
||||
*
|
||||
* This is only populated if fields have been changed or deleted. It will
|
||||
* return undefined otherwise.
|
||||
*
|
||||
* Done this way to reduce number of queries.
|
||||
*/
|
||||
fields: ZEnvelopeFieldSchema.array().optional(),
|
||||
});
|
||||
|
||||
export type TReplaceEnvelopeItemPdfPayload = z.infer<typeof ZReplaceEnvelopeItemPdfPayloadSchema>;
|
||||
export type TReplaceEnvelopeItemPdfRequest = z.infer<typeof ZReplaceEnvelopeItemPdfRequestSchema>;
|
||||
export type TReplaceEnvelopeItemPdfResponse = z.infer<typeof ZReplaceEnvelopeItemPdfResponseSchema>;
|
||||
@@ -3,6 +3,8 @@ import { createAttachmentRoute } from './attachment/create-attachment';
|
||||
import { deleteAttachmentRoute } from './attachment/delete-attachment';
|
||||
import { findAttachmentsRoute } from './attachment/find-attachments';
|
||||
import { updateAttachmentRoute } from './attachment/update-attachment';
|
||||
import { bulkDeleteEnvelopesRoute } from './bulk-delete-envelopes';
|
||||
import { bulkMoveEnvelopesRoute } from './bulk-move-envelopes';
|
||||
import { createEnvelopeRoute } from './create-envelope';
|
||||
import { createEnvelopeItemsRoute } from './create-envelope-items';
|
||||
import { deleteEnvelopeRoute } from './delete-envelope';
|
||||
@@ -20,11 +22,14 @@ import { getEnvelopeRecipientRoute } from './envelope-recipients/get-envelope-re
|
||||
import { updateEnvelopeRecipientsRoute } from './envelope-recipients/update-envelope-recipients';
|
||||
import { findEnvelopeAuditLogsRoute } from './find-envelope-audit-logs';
|
||||
import { findEnvelopesRoute } from './find-envelopes';
|
||||
import { getEditorEnvelopeRoute } from './get-editor-envelope';
|
||||
import { getEnvelopeRoute } from './get-envelope';
|
||||
import { getEnvelopeItemsRoute } from './get-envelope-items';
|
||||
import { getEnvelopeItemsByTokenRoute } from './get-envelope-items-by-token';
|
||||
import { getEnvelopesByIdsRoute } from './get-envelopes-by-ids';
|
||||
import { redistributeEnvelopeRoute } from './redistribute-envelope';
|
||||
import { replaceEnvelopeItemPdfRoute } from './replace-envelope-item-pdf';
|
||||
import { saveAsTemplateRoute } from './save-as-template';
|
||||
import { setEnvelopeFieldsRoute } from './set-envelope-fields';
|
||||
import { setEnvelopeRecipientsRoute } from './set-envelope-recipients';
|
||||
import { signEnvelopeFieldRoute } from './sign-envelope-field';
|
||||
@@ -52,6 +57,7 @@ export const envelopeRouter = router({
|
||||
updateMany: updateEnvelopeItemsRoute,
|
||||
delete: deleteEnvelopeItemRoute,
|
||||
download: downloadEnvelopeItemRoute,
|
||||
replacePdf: replaceEnvelopeItemPdfRoute,
|
||||
},
|
||||
recipient: {
|
||||
get: getEnvelopeRecipientRoute,
|
||||
@@ -72,6 +78,13 @@ export const envelopeRouter = router({
|
||||
auditLog: {
|
||||
find: findEnvelopeAuditLogsRoute,
|
||||
},
|
||||
bulk: {
|
||||
move: bulkMoveEnvelopesRoute,
|
||||
delete: bulkDeleteEnvelopesRoute,
|
||||
},
|
||||
editor: {
|
||||
get: getEditorEnvelopeRoute,
|
||||
},
|
||||
get: getEnvelopeRoute,
|
||||
getMany: getEnvelopesByIdsRoute,
|
||||
create: createEnvelopeRoute,
|
||||
@@ -79,6 +92,7 @@ export const envelopeRouter = router({
|
||||
update: updateEnvelopeRoute,
|
||||
delete: deleteEnvelopeRoute,
|
||||
duplicate: duplicateEnvelopeRoute,
|
||||
saveAsTemplate: saveAsTemplateRoute,
|
||||
distribute: distributeEnvelopeRoute,
|
||||
redistribute: redistributeEnvelopeRoute,
|
||||
signingStatus: signingStatusEnvelopeRoute,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { duplicateEnvelope } from '@documenso/lib/server-only/envelope/duplicate-envelope';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZSaveAsTemplateRequestSchema,
|
||||
ZSaveAsTemplateResponseSchema,
|
||||
} from './save-as-template.types';
|
||||
|
||||
export const saveAsTemplateRoute = authenticatedProcedure
|
||||
.input(ZSaveAsTemplateRequestSchema)
|
||||
.output(ZSaveAsTemplateResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { envelopeId, includeRecipients, includeFields } = input;
|
||||
const { teamId } = ctx;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
envelopeId,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await duplicateEnvelope({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelopeId,
|
||||
},
|
||||
overrides: {
|
||||
duplicateAsTemplate: true,
|
||||
includeRecipients,
|
||||
includeFields,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: result.id,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZSaveAsTemplateRequestSchema = z.object({
|
||||
envelopeId: z.string().min(1).describe('The ID of the envelope to save as a template.'),
|
||||
includeRecipients: z.boolean(),
|
||||
includeFields: z.boolean(),
|
||||
});
|
||||
|
||||
export const ZSaveAsTemplateResponseSchema = z.object({
|
||||
id: z.string().describe('The ID of the newly created template envelope.'),
|
||||
});
|
||||
|
||||
export type TSaveAsTemplateRequest = z.infer<typeof ZSaveAsTemplateRequestSchema>;
|
||||
export type TSaveAsTemplateResponse = z.infer<typeof ZSaveAsTemplateResponseSchema>;
|
||||
@@ -4,19 +4,19 @@ import { z } from 'zod';
|
||||
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
||||
import { ZRecipientEmailSchema, ZRecipientLiteSchema } from '@documenso/lib/types/recipient';
|
||||
|
||||
export const ZSetEnvelopeRecipientSchema = z.object({
|
||||
id: z.number().optional(),
|
||||
email: ZRecipientEmailSchema,
|
||||
name: z.string().max(255),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||
});
|
||||
|
||||
export const ZSetEnvelopeRecipientsRequestSchema = z.object({
|
||||
envelopeId: z.string(),
|
||||
envelopeType: z.nativeEnum(EnvelopeType),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
id: z.number().optional(),
|
||||
email: ZRecipientEmailSchema,
|
||||
name: z.string().max(255),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||
}),
|
||||
),
|
||||
recipients: ZSetEnvelopeRecipientSchema.array(),
|
||||
});
|
||||
|
||||
export const ZSetEnvelopeRecipientsResponseSchema = z.object({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { UNSAFE_updateEnvelopeItems } from '@documenso/lib/server-only/envelope-item/update-envelope-items';
|
||||
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
|
||||
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
|
||||
import { getEnvelopeItemPermissions } from '@documenso/lib/utils/envelope';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
@@ -54,9 +55,40 @@ export const updateEnvelopeItemsRoute = authenticatedProcedure
|
||||
});
|
||||
}
|
||||
|
||||
if (!canEnvelopeItemsBeModified(envelope, envelope.recipients)) {
|
||||
const permissions = getEnvelopeItemPermissions(envelope, envelope.recipients);
|
||||
|
||||
const hasOrderChange = data.some((item) => {
|
||||
if (item.order === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const existingItem = envelope.envelopeItems.find((e) => e.id === item.envelopeItemId);
|
||||
|
||||
return !existingItem || existingItem.order !== item.order;
|
||||
});
|
||||
|
||||
const hasTitleChange = data.some((item) => item.title !== undefined);
|
||||
|
||||
if (!hasTitleChange && !hasOrderChange) {
|
||||
return {
|
||||
data: envelope.envelopeItems.map((item) => ({
|
||||
id: item.id,
|
||||
order: item.order,
|
||||
title: item.title,
|
||||
envelopeId: item.envelopeId,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
if (hasTitleChange && !permissions.canTitleBeChanged) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Envelope item is not editable',
|
||||
message: 'Envelope item title is not editable',
|
||||
});
|
||||
}
|
||||
|
||||
if (hasOrderChange && !permissions.canOrderBeChanged) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Envelope item order is not editable',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -71,28 +103,17 @@ export const updateEnvelopeItemsRoute = authenticatedProcedure
|
||||
});
|
||||
}
|
||||
|
||||
const updatedEnvelopeItems = await Promise.all(
|
||||
data.map(async ({ envelopeItemId, order, title }) =>
|
||||
prisma.envelopeItem.update({
|
||||
where: {
|
||||
envelopeId: envelope.id,
|
||||
id: envelopeItemId,
|
||||
},
|
||||
data: {
|
||||
order,
|
||||
title,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
order: true,
|
||||
title: true,
|
||||
envelopeId: true,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// Todo: Envelope [AUDIT_LOGS]
|
||||
const updatedEnvelopeItems = await UNSAFE_updateEnvelopeItems({
|
||||
envelopeId,
|
||||
envelopeType: envelope.type,
|
||||
existingEnvelopeItems: envelope.envelopeItems,
|
||||
data,
|
||||
user: {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
},
|
||||
apiRequestMetadata: ctx.metadata,
|
||||
});
|
||||
|
||||
return {
|
||||
data: updatedEnvelopeItems,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { TemplateType } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
@@ -33,6 +34,7 @@ export const ZUpdateEnvelopeRequestSchema = z.object({
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
||||
folderId: z.string().nullish(),
|
||||
templateType: z.nativeEnum(TemplateType).optional(),
|
||||
})
|
||||
.optional(),
|
||||
meta: ZDocumentMetaUpdateSchema.optional(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
import { zfd } from 'zod-form-data';
|
||||
|
||||
import { ZEnvelopeExpirationPeriod } from '@documenso/lib/constants/envelope-expiration';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
|
||||
import {
|
||||
@@ -19,7 +20,7 @@ import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-att
|
||||
import { ZFieldMetaPrefillFieldsSchema } from '@documenso/lib/types/field-meta';
|
||||
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||
|
||||
import { zodFormData } from '../../utils/zod-form-data';
|
||||
import { zfdFile, zodFormData } from '../../utils/zod-form-data';
|
||||
import type { TrpcRouteMeta } from '../trpc';
|
||||
import { ZRecipientWithSigningUrlSchema } from './schema';
|
||||
|
||||
@@ -97,6 +98,7 @@ export const ZUseEnvelopePayloadSchema = z.object({
|
||||
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
|
||||
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
|
||||
allowDictateNextSigner: z.boolean().optional(),
|
||||
envelopeExpirationPeriod: ZEnvelopeExpirationPeriod.nullish(),
|
||||
})
|
||||
.describe('Override values from the template for the created document.')
|
||||
.optional(),
|
||||
@@ -115,7 +117,7 @@ export const ZUseEnvelopePayloadSchema = z.object({
|
||||
|
||||
export const ZUseEnvelopeRequestSchema = zodFormData({
|
||||
payload: zfd.json(ZUseEnvelopePayloadSchema),
|
||||
files: zfd.repeatableOfType(zfd.file()).optional(),
|
||||
files: zfd.repeatableOfType(zfdFile()).optional(),
|
||||
});
|
||||
|
||||
export const ZUseEnvelopeResponseSchema = z.object({
|
||||
|
||||
+3
-1
@@ -1,6 +1,8 @@
|
||||
import { OrganisationMemberRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
|
||||
// export const createOrganisationMemberInvitesMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'POST',
|
||||
@@ -16,7 +18,7 @@ export const ZCreateOrganisationMemberInvitesRequestSchema = z.object({
|
||||
invitations: z
|
||||
.array(
|
||||
z.object({
|
||||
email: z.string().trim().email().toLowerCase(),
|
||||
email: zEmail().trim().toLowerCase(),
|
||||
organisationRole: z.nativeEnum(OrganisationMemberRole),
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { syncMemberCountWithStripeSeatPlan } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getMemberOrganisationRole } from '@documenso/lib/server-only/team/get-member-roles';
|
||||
import { validateIfSubscriptionIsRequired } from '@documenso/lib/utils/billing';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import {
|
||||
buildOrganisationWhereQuery,
|
||||
isOrganisationRoleWithinUserHierarchy,
|
||||
} from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
@@ -52,6 +56,41 @@ export const deleteOrganisationMemberInvitesRoute = authenticatedProcedure
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const currentOrganisationMemberRole = await getMemberOrganisationRole({
|
||||
organisationId: organisation.id,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
const invitesToDelete = await prisma.organisationMemberInvite.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: invitationIds,
|
||||
},
|
||||
organisationId: organisation.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
organisationRole: true,
|
||||
},
|
||||
});
|
||||
|
||||
const hasUnauthorizedRoleAccess = invitesToDelete.some(
|
||||
(invite) =>
|
||||
!isOrganisationRoleWithinUserHierarchy(
|
||||
currentOrganisationMemberRole,
|
||||
invite.organisationRole,
|
||||
),
|
||||
);
|
||||
|
||||
if (hasUnauthorizedRoleAccess) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'User does not have permission to delete invitations for higher roles',
|
||||
});
|
||||
}
|
||||
|
||||
const { organisationClaim } = organisation;
|
||||
|
||||
const subscription = validateIfSubscriptionIsRequired(organisation.subscription);
|
||||
|
||||
@@ -100,13 +100,15 @@ export const deleteOrganisationMembers = async ({
|
||||
organisationId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
for (const member of membersToDelete) {
|
||||
await jobs.triggerJob({
|
||||
name: 'send.organisation-member-left.email',
|
||||
payload: {
|
||||
organisationId,
|
||||
memberUserId: userId,
|
||||
memberUserId: member.userId,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -51,6 +51,7 @@ export const getOrganisationSession = async ({
|
||||
where: buildTeamWhereQuery({ teamId: undefined, userId }),
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
teamEmail: { select: { email: true } },
|
||||
teamGroups: {
|
||||
where: {
|
||||
organisationGroup: {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
||||
import { ZOrganisationSchema } from '@documenso/lib/types/organisation';
|
||||
import { OrganisationMemberRole, TeamMemberRole } from '@documenso/prisma/generated/types';
|
||||
import SubscriptionSchema from '@documenso/prisma/generated/zod/modelSchema/SubscriptionSchema';
|
||||
import { TeamEmailSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamEmailSchema';
|
||||
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
|
||||
export const ZGetOrganisationSessionResponseSchema = ZOrganisationSchema.extend({
|
||||
@@ -16,6 +17,7 @@ export const ZGetOrganisationSessionResponseSchema = ZOrganisationSchema.extend(
|
||||
organisationId: true,
|
||||
}).extend({
|
||||
currentTeamRole: z.nativeEnum(TeamMemberRole),
|
||||
teamEmail: TeamEmailSchema.pick({ email: true }).nullable(),
|
||||
preferences: z.object({
|
||||
aiFeaturesEnabled: z.boolean(),
|
||||
}),
|
||||
|
||||
@@ -38,6 +38,8 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
|
||||
drawSignatureEnabled,
|
||||
defaultRecipients,
|
||||
delegateDocumentOwnership,
|
||||
envelopeExpirationPeriod,
|
||||
reminderSettings,
|
||||
|
||||
// Branding related settings.
|
||||
brandingEnabled,
|
||||
@@ -148,6 +150,9 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
|
||||
drawSignatureEnabled,
|
||||
defaultRecipients: defaultRecipients === null ? Prisma.DbNull : defaultRecipients,
|
||||
delegateDocumentOwnership: derivedDelegateDocumentOwnership,
|
||||
envelopeExpirationPeriod:
|
||||
envelopeExpirationPeriod === null ? Prisma.DbNull : envelopeExpirationPeriod,
|
||||
reminderSettings: reminderSettings === null ? Prisma.DbNull : reminderSettings,
|
||||
|
||||
// Branding related settings.
|
||||
brandingEnabled,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZEnvelopeExpirationPeriod } from '@documenso/lib/constants/envelope-expiration';
|
||||
import { ZEnvelopeReminderSettings } from '@documenso/lib/constants/envelope-reminder';
|
||||
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
|
||||
import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
@@ -8,6 +10,7 @@ import {
|
||||
ZDocumentMetaTimezoneSchema,
|
||||
} from '@documenso/lib/types/document-meta';
|
||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
|
||||
export const ZUpdateOrganisationSettingsRequestSchema = z.object({
|
||||
organisationId: z.string(),
|
||||
@@ -25,6 +28,8 @@ export const ZUpdateOrganisationSettingsRequestSchema = z.object({
|
||||
drawSignatureEnabled: z.boolean().optional(),
|
||||
defaultRecipients: ZDefaultRecipientsSchema.nullish(),
|
||||
delegateDocumentOwnership: z.boolean().nullish(),
|
||||
envelopeExpirationPeriod: ZEnvelopeExpirationPeriod.optional(),
|
||||
reminderSettings: ZEnvelopeReminderSettings.optional(),
|
||||
|
||||
// Branding related settings.
|
||||
brandingEnabled: z.boolean().optional(),
|
||||
@@ -34,7 +39,7 @@ export const ZUpdateOrganisationSettingsRequestSchema = z.object({
|
||||
|
||||
// Email related settings.
|
||||
emailId: z.string().nullish(),
|
||||
emailReplyTo: z.string().email().nullish(),
|
||||
emailReplyTo: zEmail().nullish(),
|
||||
// emailReplyToName: z.string().optional(),
|
||||
emailDocumentSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
|
||||
|
||||
@@ -90,10 +90,12 @@ export const profileRouter = router({
|
||||
|
||||
const parsedTeamId = teamId ? Number(teamId) : null;
|
||||
|
||||
if (Number.isNaN(parsedTeamId)) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid team ID provided',
|
||||
});
|
||||
if (typeof parsedTeamId === 'number') {
|
||||
if (Number.isNaN(parsedTeamId) || parsedTeamId <= 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid team ID provided',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await submitSupportTicket({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
|
||||
export const ZGetRecipientSuggestionsRequestSchema = z.object({
|
||||
query: z.string().default(''),
|
||||
});
|
||||
@@ -8,7 +10,7 @@ export const ZGetRecipientSuggestionsResponseSchema = z.object({
|
||||
results: z.array(
|
||||
z.object({
|
||||
name: z.string().nullable(),
|
||||
email: z.string().email(),
|
||||
email: z.union([zEmail(), z.literal('')]),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ZRecipientActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { ZRecipientLiteSchema, ZRecipientSchema } from '@documenso/lib/types/recipient';
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
|
||||
export const ZGetRecipientRequestSchema = z.object({
|
||||
recipientId: z.number(),
|
||||
@@ -24,7 +25,7 @@ export const ZGetRecipientResponseSchema = ZRecipientSchema;
|
||||
* pass along required details.
|
||||
*/
|
||||
export const ZCreateRecipientSchema = z.object({
|
||||
email: z.string().toLowerCase().email().min(1).max(254),
|
||||
email: zEmail().toLowerCase().min(1).max(254),
|
||||
name: z.string().max(255),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
@@ -34,7 +35,7 @@ export const ZCreateRecipientSchema = z.object({
|
||||
|
||||
export const ZUpdateRecipientSchema = z.object({
|
||||
id: z.number().describe('The ID of the recipient to update.'),
|
||||
email: z.string().toLowerCase().email().min(1).max(254).optional(),
|
||||
email: zEmail().toLowerCase().min(1).max(254).optional(),
|
||||
name: z.string().max(255).optional(),
|
||||
role: z.nativeEnum(RecipientRole).optional(),
|
||||
signingOrder: z.number().optional(),
|
||||
@@ -83,7 +84,7 @@ export const ZSetDocumentRecipientsRequestSchema = z.object({
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
id: z.number().optional(),
|
||||
email: z.string().toLowerCase().email().min(1).max(254),
|
||||
email: zEmail().toLowerCase().min(1).max(254),
|
||||
name: z.string().max(255),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
@@ -142,10 +143,7 @@ export const ZSetTemplateRecipientsRequestSchema = z.object({
|
||||
.toLowerCase()
|
||||
.refine(
|
||||
(email) => {
|
||||
return (
|
||||
isTemplateRecipientEmailPlaceholder(email) ||
|
||||
z.string().email().safeParse(email).success
|
||||
);
|
||||
return isTemplateRecipientEmailPlaceholder(email) || zEmail().safeParse(email).success;
|
||||
},
|
||||
{ message: 'Please enter a valid email address' },
|
||||
),
|
||||
@@ -167,13 +165,13 @@ export const ZCompleteDocumentWithTokenMutationSchema = z.object({
|
||||
accessAuthOptions: ZRecipientAccessAuthSchema.optional(),
|
||||
nextSigner: z
|
||||
.object({
|
||||
email: z.string().email().max(254),
|
||||
email: zEmail().max(254),
|
||||
name: z.string().min(1).max(255),
|
||||
})
|
||||
.optional(),
|
||||
recipientOverride: z
|
||||
.object({
|
||||
email: z.string().trim().toLowerCase().email().max(254).optional(),
|
||||
email: zEmail().trim().toLowerCase().max(254).optional(),
|
||||
name: z.string().max(255).optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TeamMemberRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PROTECTED_TEAM_URLS } from '@documenso/lib/constants/teams';
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
|
||||
/**
|
||||
* Restrict team URLs schema.
|
||||
@@ -43,7 +44,7 @@ export const ZTeamNameSchema = z
|
||||
export const ZCreateTeamEmailVerificationMutationSchema = z.object({
|
||||
teamId: z.number(),
|
||||
name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
|
||||
email: z.string().trim().email().toLowerCase().min(1, 'Please enter a valid email.'),
|
||||
email: zEmail().trim().toLowerCase().min(1, 'Please enter a valid email.'),
|
||||
});
|
||||
|
||||
export const ZDeleteTeamEmailMutationSchema = z.object({
|
||||
|
||||
@@ -40,6 +40,8 @@ export const updateTeamSettingsRoute = authenticatedProcedure
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
delegateDocumentOwnership,
|
||||
envelopeExpirationPeriod,
|
||||
reminderSettings,
|
||||
|
||||
// Branding related settings.
|
||||
brandingEnabled,
|
||||
@@ -154,6 +156,9 @@ export const updateTeamSettingsRoute = authenticatedProcedure
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
delegateDocumentOwnership,
|
||||
envelopeExpirationPeriod:
|
||||
envelopeExpirationPeriod === null ? Prisma.DbNull : envelopeExpirationPeriod,
|
||||
reminderSettings: reminderSettings === null ? Prisma.DbNull : reminderSettings,
|
||||
|
||||
// Branding related settings.
|
||||
brandingEnabled,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZEnvelopeExpirationPeriod } from '@documenso/lib/constants/envelope-expiration';
|
||||
import { ZEnvelopeReminderSettings } from '@documenso/lib/constants/envelope-reminder';
|
||||
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
|
||||
import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
@@ -8,6 +10,7 @@ import {
|
||||
ZDocumentMetaTimezoneSchema,
|
||||
} from '@documenso/lib/types/document-meta';
|
||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
|
||||
/**
|
||||
* Null = Inherit from organisation.
|
||||
@@ -28,6 +31,8 @@ export const ZUpdateTeamSettingsRequestSchema = z.object({
|
||||
uploadSignatureEnabled: z.boolean().nullish(),
|
||||
drawSignatureEnabled: z.boolean().nullish(),
|
||||
delegateDocumentOwnership: z.boolean().nullish(),
|
||||
envelopeExpirationPeriod: ZEnvelopeExpirationPeriod.nullish(),
|
||||
reminderSettings: ZEnvelopeReminderSettings.nullish(),
|
||||
|
||||
// Branding related settings.
|
||||
brandingEnabled: z.boolean().nullish(),
|
||||
@@ -37,7 +42,7 @@ export const ZUpdateTeamSettingsRequestSchema = z.object({
|
||||
|
||||
// Email related settings.
|
||||
emailId: z.string().nullish(),
|
||||
emailReplyTo: z.string().email().nullish(),
|
||||
emailReplyTo: zEmail().nullish(),
|
||||
// emailReplyToName: z.string().nullish(),
|
||||
emailDocumentSettings: ZDocumentEmailSettingsSchema.nullish(),
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ export const getTemplatesByIdsRoute = authenticatedProcedure
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
documentMeta: {
|
||||
@@ -97,6 +98,7 @@ export const getTemplatesByIdsRoute = authenticatedProcedure
|
||||
? {
|
||||
id: envelope.team.id,
|
||||
url: envelope.team.url,
|
||||
name: envelope.team.name,
|
||||
}
|
||||
: null,
|
||||
fields: envelope.fields.map((field) => mapFieldToLegacyField(field, envelope)),
|
||||
|
||||
@@ -18,7 +18,9 @@ import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/
|
||||
import { createTemplateDirectLink } from '@documenso/lib/server-only/template/create-template-direct-link';
|
||||
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
|
||||
import { deleteTemplateDirectLink } from '@documenso/lib/server-only/template/delete-template-direct-link';
|
||||
import { findOrganisationTemplates } from '@documenso/lib/server-only/template/find-organisation-templates';
|
||||
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
|
||||
import { getOrganisationTemplateById } from '@documenso/lib/server-only/template/get-organisation-template-by-id';
|
||||
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||
import { toggleTemplateDirectLink } from '@documenso/lib/server-only/template/toggle-template-direct-link';
|
||||
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||
@@ -46,8 +48,11 @@ import {
|
||||
ZDeleteTemplateMutationSchema,
|
||||
ZDuplicateTemplateMutationSchema,
|
||||
ZDuplicateTemplateResponseSchema,
|
||||
ZFindOrganisationTemplatesRequestSchema,
|
||||
ZFindTemplatesRequestSchema,
|
||||
ZFindTemplatesResponseSchema,
|
||||
ZGetOrganisationTemplateByIdRequestSchema,
|
||||
ZGetOrganisationTemplateByIdResponseSchema,
|
||||
ZGetTemplateByIdRequestSchema,
|
||||
ZGetTemplateByIdResponseSchema,
|
||||
ZToggleTemplateDirectLinkRequestSchema,
|
||||
@@ -55,6 +60,7 @@ import {
|
||||
ZUpdateTemplateRequestSchema,
|
||||
ZUpdateTemplateResponseSchema,
|
||||
} from './schema';
|
||||
import { searchTemplateRoute } from './search-template';
|
||||
|
||||
export const templateRouter = router({
|
||||
/**
|
||||
@@ -121,6 +127,75 @@ export const templateRouter = router({
|
||||
};
|
||||
}),
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
findOrganisationTemplates: authenticatedProcedure
|
||||
.input(ZFindOrganisationTemplatesRequestSchema)
|
||||
.output(ZFindTemplatesResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { teamId } = ctx;
|
||||
|
||||
const result = await findOrganisationTemplates({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
...input,
|
||||
});
|
||||
|
||||
// Remapping for backwards compatibility.
|
||||
return {
|
||||
...result,
|
||||
data: result.data.map((envelope) => {
|
||||
const legacyTemplateId = mapSecondaryIdToTemplateId(envelope.secondaryId);
|
||||
|
||||
return {
|
||||
id: legacyTemplateId,
|
||||
envelopeId: envelope.id,
|
||||
type: envelope.templateType,
|
||||
visibility: envelope.visibility,
|
||||
externalId: envelope.externalId,
|
||||
title: envelope.title,
|
||||
userId: envelope.userId,
|
||||
teamId: envelope.teamId,
|
||||
authOptions: envelope.authOptions,
|
||||
createdAt: envelope.createdAt,
|
||||
updatedAt: envelope.updatedAt,
|
||||
publicTitle: envelope.publicTitle,
|
||||
publicDescription: envelope.publicDescription,
|
||||
folderId: envelope.folderId,
|
||||
useLegacyFieldInsertion: envelope.useLegacyFieldInsertion,
|
||||
team: envelope.team,
|
||||
fields: envelope.fields.map((field) => mapFieldToLegacyField(field, envelope)),
|
||||
recipients: envelope.recipients.map((recipient) =>
|
||||
mapRecipientToLegacyRecipient(recipient, envelope),
|
||||
),
|
||||
templateMeta: envelope.documentMeta,
|
||||
directLink: envelope.directLink,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
getOrganisationTemplateById: authenticatedProcedure
|
||||
.input(ZGetOrganisationTemplateByIdRequestSchema)
|
||||
.output(ZGetOrganisationTemplateByIdResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { teamId } = ctx;
|
||||
const { envelopeId } = input;
|
||||
|
||||
return await getOrganisationTemplateById({
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelopeId,
|
||||
},
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
});
|
||||
}),
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -160,6 +235,8 @@ export const templateRouter = router({
|
||||
*/
|
||||
getMany: getTemplatesByIdsRoute,
|
||||
|
||||
search: searchTemplateRoute,
|
||||
|
||||
/**
|
||||
* Wait until RR7 so we can passthrough documents.
|
||||
*
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DocumentSigningOrder, DocumentVisibility, TemplateType } from '@prisma/
|
||||
import { z } from 'zod';
|
||||
import { zfd } from 'zod-form-data';
|
||||
|
||||
import { ZEnvelopeExpirationPeriod } from '@documenso/lib/constants/envelope-expiration';
|
||||
import { ZDocumentSchema } from '@documenso/lib/types/document';
|
||||
import {
|
||||
ZDocumentAccessAuthTypesSchema,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
ZDocumentMetaTypedSignatureEnabledSchema,
|
||||
ZDocumentMetaUploadSignatureEnabledSchema,
|
||||
} from '@documenso/lib/types/document-meta';
|
||||
import { ZEnvelopeSchema } from '@documenso/lib/types/envelope';
|
||||
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
|
||||
import { ZFieldMetaPrefillFieldsSchema } from '@documenso/lib/types/field-meta';
|
||||
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||
@@ -30,10 +32,11 @@ import {
|
||||
ZTemplateManySchema,
|
||||
ZTemplateSchema,
|
||||
} from '@documenso/lib/types/template';
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
import { LegacyTemplateDirectLinkSchema } from '@documenso/prisma/types/template-legacy-schema';
|
||||
import { ZDocumentExternalIdSchema } from '@documenso/trpc/server/document-router/schema';
|
||||
|
||||
import { zodFormData } from '../../utils/zod-form-data';
|
||||
import { zfdFile, zodFormData } from '../../utils/zod-form-data';
|
||||
import { ZSignFieldWithTokenMutationSchema } from '../field-router/schema';
|
||||
|
||||
export const MAX_TEMPLATE_PUBLIC_TITLE_LENGTH = 50;
|
||||
@@ -71,7 +74,7 @@ export const ZTemplateMetaUpsertSchema = z.object({
|
||||
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
|
||||
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
|
||||
emailId: z.string().nullish(),
|
||||
emailReplyTo: z.string().email().nullish(),
|
||||
emailReplyTo: zEmail().nullish(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
|
||||
language: ZDocumentMetaLanguageSchema.optional(),
|
||||
@@ -84,14 +87,14 @@ export const ZTemplateMetaUpsertSchema = z.object({
|
||||
|
||||
export const ZCreateDocumentFromDirectTemplateRequestSchema = z.object({
|
||||
directRecipientName: z.string().max(255).optional(),
|
||||
directRecipientEmail: z.string().email().max(254),
|
||||
directRecipientEmail: zEmail().max(254),
|
||||
directTemplateToken: z.string().min(1),
|
||||
directTemplateExternalId: z.string().optional(),
|
||||
signedFieldValues: z.array(ZSignFieldWithTokenMutationSchema),
|
||||
templateUpdatedAt: z.date(),
|
||||
nextSigner: z
|
||||
.object({
|
||||
email: z.string().email().max(254),
|
||||
email: zEmail().max(254),
|
||||
name: z.string().min(1).max(255),
|
||||
})
|
||||
.optional(),
|
||||
@@ -160,6 +163,7 @@ export const ZCreateDocumentFromTemplateRequestSchema = z.object({
|
||||
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
|
||||
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
|
||||
allowDictateNextSigner: z.boolean().optional(),
|
||||
envelopeExpirationPeriod: ZEnvelopeExpirationPeriod.nullish(),
|
||||
})
|
||||
.describe('Override values from the template for the created document.')
|
||||
.optional(),
|
||||
@@ -264,7 +268,7 @@ export const ZCreateTemplatePayloadSchema = ZCreateTemplateV2RequestSchema;
|
||||
|
||||
export const ZCreateTemplateMutationSchema = zodFormData({
|
||||
payload: zfd.json(ZCreateTemplatePayloadSchema),
|
||||
file: zfd.file(),
|
||||
file: zfdFile(),
|
||||
});
|
||||
|
||||
export const ZUpdateTemplateRequestSchema = z.object({
|
||||
@@ -293,6 +297,8 @@ export const ZFindTemplatesRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
folderId: z.string().describe('The ID of the folder to filter templates by.').optional(),
|
||||
});
|
||||
|
||||
export const ZFindOrganisationTemplatesRequestSchema = ZFindSearchParamsSchema;
|
||||
|
||||
export const ZFindTemplatesResponseSchema = ZFindResultResponse.extend({
|
||||
data: ZTemplateManySchema.array(),
|
||||
});
|
||||
@@ -306,6 +312,12 @@ export const ZGetTemplateByIdRequestSchema = z.object({
|
||||
|
||||
export const ZGetTemplateByIdResponseSchema = ZTemplateSchema;
|
||||
|
||||
export const ZGetOrganisationTemplateByIdRequestSchema = z.object({
|
||||
envelopeId: z.string(),
|
||||
});
|
||||
|
||||
export const ZGetOrganisationTemplateByIdResponseSchema = ZEnvelopeSchema;
|
||||
|
||||
export const ZBulkSendTemplateMutationSchema = z.object({
|
||||
templateId: z.number(),
|
||||
teamId: z.number(),
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { searchTemplatesWithKeyword } from '@documenso/lib/server-only/template/search-templates-with-keyword';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZSearchTemplateRequestSchema,
|
||||
ZSearchTemplateResponseSchema,
|
||||
} from './search-template.types';
|
||||
|
||||
export const searchTemplateRoute = authenticatedProcedure
|
||||
.input(ZSearchTemplateRequestSchema)
|
||||
.output(ZSearchTemplateResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { query } = input;
|
||||
|
||||
const templates = await searchTemplatesWithKeyword({
|
||||
query,
|
||||
userId: ctx.user.id,
|
||||
});
|
||||
|
||||
return templates;
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZSearchTemplateRequestSchema = z.object({
|
||||
query: z.string().trim().min(1).max(1024),
|
||||
});
|
||||
|
||||
export const ZSearchTemplateResponseSchema = z
|
||||
.object({
|
||||
title: z.string(),
|
||||
path: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
.array();
|
||||
|
||||
export type TSearchTemplateRequest = z.infer<typeof ZSearchTemplateRequestSchema>;
|
||||
export type TSearchTemplateResponse = z.infer<typeof ZSearchTemplateResponseSchema>;
|
||||
@@ -37,7 +37,7 @@ const t = initTRPC
|
||||
.create({
|
||||
transformer: dataTransformer,
|
||||
errorFormatter(opts) {
|
||||
const { shape, error } = opts;
|
||||
const { shape, error, ctx } = opts;
|
||||
|
||||
const originalError = error.cause;
|
||||
|
||||
@@ -46,6 +46,12 @@ const t = initTRPC
|
||||
// Default unknown errors to 400, since if you're throwing an AppError it is expected
|
||||
// that you already know what you're doing.
|
||||
if (originalError instanceof AppError) {
|
||||
if (originalError.headers && ctx) {
|
||||
for (const [headerKey, headerValue] of Object.entries(originalError.headers)) {
|
||||
ctx.res.headers.append(headerKey, headerValue);
|
||||
}
|
||||
}
|
||||
|
||||
data = {
|
||||
...data,
|
||||
appError: AppError.toJSON(originalError),
|
||||
@@ -67,7 +73,7 @@ const t = initTRPC
|
||||
/**
|
||||
* Middlewares
|
||||
*/
|
||||
export const authenticatedMiddleware = t.middleware(async ({ ctx, next, path }) => {
|
||||
export const authenticatedMiddleware = t.middleware(async ({ ctx, next, path, meta }) => {
|
||||
const infoToLog: TrpcApiLog = {
|
||||
path,
|
||||
auth: ctx.metadata.auth,
|
||||
@@ -78,8 +84,10 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next, path })
|
||||
|
||||
const authorizationHeader = ctx.req.headers.get('authorization');
|
||||
|
||||
const isApiV2 = Boolean(meta?.openapi?.path);
|
||||
|
||||
// Taken from `authenticatedMiddleware` in `@documenso/api/v1/middleware/authenticated.ts`.
|
||||
if (authorizationHeader) {
|
||||
if (authorizationHeader && isApiV2) {
|
||||
// Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx"
|
||||
const [token] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0);
|
||||
|
||||
@@ -158,7 +166,7 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next, path })
|
||||
});
|
||||
});
|
||||
|
||||
export const maybeAuthenticatedMiddleware = t.middleware(async ({ ctx, next, path }) => {
|
||||
export const maybeAuthenticatedMiddleware = t.middleware(async ({ ctx, next, path, meta }) => {
|
||||
// Recreate the logger with a sub request ID to differentiate between batched requests.
|
||||
const trpcSessionLogger = ctx.logger.child({
|
||||
nonBatchedRequestId: alphaid(),
|
||||
@@ -174,8 +182,10 @@ export const maybeAuthenticatedMiddleware = t.middleware(async ({ ctx, next, pat
|
||||
|
||||
const authorizationHeader = ctx.req.headers.get('authorization');
|
||||
|
||||
const isApiV2 = Boolean(meta?.openapi?.path);
|
||||
|
||||
// Taken from `authenticatedMiddleware` in `@documenso/api/v1/middleware/authenticated.ts`.
|
||||
if (authorizationHeader) {
|
||||
if (authorizationHeader && isApiV2) {
|
||||
// Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx"
|
||||
const [token] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0);
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Prisma, WebhookCallStatus, WebhookTriggerEvents } from '@prisma/client';
|
||||
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { FindResultResponse } from '@documenso/lib/types/search-params';
|
||||
import { jobs } from '@documenso/lib/jobs/client';
|
||||
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@@ -35,46 +33,18 @@ export const resendWebhookCallRoute = authenticatedProcedure
|
||||
}),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
webhook: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!webhookCall) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const { webhook } = webhookCall;
|
||||
|
||||
// Note: This is duplicated in `execute-webhook.handler.ts`.
|
||||
const response = await fetch(webhookCall.url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(webhookCall.requestBody),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Documenso-Secret': webhook.secret ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
const body = await response.text();
|
||||
|
||||
let responseBody: Prisma.InputJsonValue | Prisma.JsonNullValueInput = Prisma.JsonNull;
|
||||
|
||||
try {
|
||||
responseBody = JSON.parse(body);
|
||||
} catch (err) {
|
||||
responseBody = body;
|
||||
}
|
||||
|
||||
return await prisma.webhookCall.update({
|
||||
where: {
|
||||
id: webhookCall.id,
|
||||
},
|
||||
data: {
|
||||
status: response.ok ? WebhookCallStatus.SUCCESS : WebhookCallStatus.FAILED,
|
||||
responseCode: response.status,
|
||||
responseBody,
|
||||
responseHeaders: Object.fromEntries(response.headers.entries()),
|
||||
await jobs.triggerJob({
|
||||
name: 'internal.execute-webhook',
|
||||
payload: {
|
||||
event: webhookCall.event,
|
||||
webhookId,
|
||||
data: webhookCall.requestBody,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,26 +1,11 @@
|
||||
import { WebhookCallStatus, WebhookTriggerEvents } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import WebhookCallSchema from '@documenso/prisma/generated/zod/modelSchema/WebhookCallSchema';
|
||||
|
||||
export const ZResendWebhookCallRequestSchema = z.object({
|
||||
webhookId: z.string(),
|
||||
webhookCallId: z.string(),
|
||||
});
|
||||
|
||||
export const ZResendWebhookCallResponseSchema = WebhookCallSchema.pick({
|
||||
webhookId: true,
|
||||
status: true,
|
||||
event: true,
|
||||
id: true,
|
||||
url: true,
|
||||
responseCode: true,
|
||||
createdAt: true,
|
||||
}).extend({
|
||||
requestBody: z.unknown(),
|
||||
responseHeaders: z.unknown().nullable(),
|
||||
responseBody: z.unknown().nullable(),
|
||||
});
|
||||
export const ZResendWebhookCallResponseSchema = z.void();
|
||||
|
||||
export type TResendWebhookRequest = z.infer<typeof ZResendWebhookCallRequestSchema>;
|
||||
export type TResendWebhookResponse = z.infer<typeof ZResendWebhookCallResponseSchema>;
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { WebhookTriggerEvents } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { isPrivateUrl } from '@documenso/lib/server-only/webhooks/is-private-url';
|
||||
|
||||
export const ZWebhookUrlSchema = z
|
||||
.string()
|
||||
.url()
|
||||
.refine((url) => !isPrivateUrl(url), {
|
||||
message: 'Webhook URL cannot point to a private or loopback address',
|
||||
});
|
||||
|
||||
export const ZCreateWebhookRequestSchema = z.object({
|
||||
webhookUrl: z.string().url(),
|
||||
webhookUrl: ZWebhookUrlSchema,
|
||||
eventTriggers: z
|
||||
.array(z.nativeEnum(WebhookTriggerEvents))
|
||||
.min(1, { message: 'At least one event trigger is required' }),
|
||||
|
||||
Reference in New Issue
Block a user