Merge branch 'main' into feat/document-2fa-redo

This commit is contained in:
Ephraim Atta-Duncan
2025-08-01 09:00:30 +00:00
167 changed files with 6918 additions and 1245 deletions

View File

@ -1,20 +0,0 @@
import { router } from '../trpc';
import { createSubscriptionRoute } from './create-subscription';
import { getInvoicesRoute } from './get-invoices';
import { getPlansRoute } from './get-plans';
import { getSubscriptionRoute } from './get-subscription';
import { manageSubscriptionRoute } from './manage-subscription';
export const billingRouter = router({
plans: {
get: getPlansRoute,
},
subscription: {
get: getSubscriptionRoute,
create: createSubscriptionRoute,
manage: manageSubscriptionRoute,
},
invoices: {
get: getInvoicesRoute,
},
});

View File

@ -322,7 +322,7 @@ export const documentRouter = router({
return {
document: createdDocument,
folder: createdDocument.folder,
folder: createdDocument.folder, // Todo: Remove this prior to api-v2 release.
uploadUrl: url,
};
}),
@ -367,7 +367,7 @@ export const documentRouter = router({
title,
documentDataId,
normalizePdf: true,
timezone,
userTimezone: timezone,
requestMetadata: ctx.metadata,
folderId,
});
@ -477,6 +477,8 @@ export const documentRouter = router({
distributionMethod: meta.distributionMethod,
emailSettings: meta.emailSettings,
language: meta.language,
emailId: meta.emailId,
emailReplyTo: meta.emailReplyTo,
requestMetadata: ctx.metadata,
});
}

View File

@ -294,6 +294,8 @@ export const ZDistributeDocumentRequestSchema = z.object({
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
emailId: z.string().nullish(),
emailReplyTo: z.string().nullish(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),

View File

@ -44,6 +44,8 @@ export const updateDocumentRoute = authenticatedProcedure
distributionMethod: meta.distributionMethod,
signingOrder: meta.signingOrder,
allowDictateNextSigner: meta.allowDictateNextSigner,
emailId: meta.emailId,
emailReplyTo: meta.emailReplyTo,
emailSettings: meta.emailSettings,
requestMetadata: ctx.metadata,
});

View File

@ -61,6 +61,8 @@ export const ZUpdateDocumentRequestSchema = z.object({
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
emailId: z.string().nullish(),
emailReplyTo: z.string().nullish(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),

View File

@ -33,7 +33,9 @@ export const createEmbeddingTemplateRoute = procedure
// First create the template
const template = await createTemplate({
userId: apiToken.userId,
title,
data: {
title,
},
templateDocumentDataId: documentDataId,
teamId: apiToken.teamId ?? undefined,
});
@ -77,16 +79,31 @@ export const createEmbeddingTemplateRoute = procedure
// Update the template meta if needed
if (meta) {
const upsertMetaData = {
subject: meta.subject,
message: meta.message,
timezone: meta.timezone,
dateFormat: meta.dateFormat,
distributionMethod: meta.distributionMethod,
signingOrder: meta.signingOrder,
redirectUrl: meta.redirectUrl,
language: meta.language,
typedSignatureEnabled: meta.typedSignatureEnabled,
drawSignatureEnabled: meta.drawSignatureEnabled,
uploadSignatureEnabled: meta.uploadSignatureEnabled,
emailSettings: meta.emailSettings,
};
await prisma.templateMeta.upsert({
where: {
templateId: template.id,
},
create: {
templateId: template.id,
...meta,
...upsertMetaData,
},
update: {
...meta,
...upsertMetaData,
},
});
}

View File

@ -0,0 +1,66 @@
import { createEmailDomain } from '@documenso/ee/server-only/lib/create-email-domain';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZCreateOrganisationEmailDomainRequestSchema,
ZCreateOrganisationEmailDomainResponseSchema,
} from './create-organisation-email-domain.types';
export const createOrganisationEmailDomainRoute = authenticatedProcedure
.input(ZCreateOrganisationEmailDomainRequestSchema)
.output(ZCreateOrganisationEmailDomainResponseSchema)
.mutation(async ({ input, ctx }) => {
const { organisationId, domain } = input;
const { user } = ctx;
ctx.logger.info({
input: {
organisationId,
domain,
},
});
if (!IS_BILLING_ENABLED()) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Billing is not enabled',
});
}
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery({
organisationId,
userId: user.id,
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
}),
include: {
emailDomains: true,
organisationClaim: true,
},
});
if (!organisation) {
throw new AppError(AppErrorCode.UNAUTHORIZED);
}
if (!organisation.organisationClaim.flags.emailDomains) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Email domains are not enabled for this organisation',
});
}
if (organisation.emailDomains.length >= 100) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'You have reached the maximum number of email domains',
});
}
return await createEmailDomain({
domain,
organisationId,
});
});

View File

@ -0,0 +1,27 @@
import { z } from 'zod';
import { ZEmailDomainSchema } from '@documenso/lib/types/email-domain';
const domainRegex =
/^(?!https?:\/\/)(?!www\.)([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
export const ZDomainSchema = z
.string()
.regex(domainRegex, { message: 'Invalid domain name' })
.toLowerCase();
export const ZCreateOrganisationEmailDomainRequestSchema = z.object({
organisationId: z.string(),
domain: ZDomainSchema,
});
export const ZCreateOrganisationEmailDomainResponseSchema = z.object({
emailDomain: ZEmailDomainSchema,
records: z.array(
z.object({
name: z.string(),
value: z.string(),
type: z.string(),
}),
),
});

View File

@ -0,0 +1,61 @@
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { generateDatabaseId } from '@documenso/lib/universal/id';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZCreateOrganisationEmailRequestSchema,
ZCreateOrganisationEmailResponseSchema,
} from './create-organisation-email.types';
export const createOrganisationEmailRoute = authenticatedProcedure
.input(ZCreateOrganisationEmailRequestSchema)
.output(ZCreateOrganisationEmailResponseSchema)
.mutation(async ({ input, ctx }) => {
const { email, emailName, emailDomainId } = input;
const { user } = ctx;
ctx.logger.info({
input: {
emailDomainId,
},
});
const emailDomain = await prisma.emailDomain.findFirst({
where: {
id: emailDomainId,
organisation: buildOrganisationWhereQuery({
organisationId: undefined,
userId: user.id,
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
}),
},
});
if (!emailDomain) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Email domain not found',
});
}
const allowedEmailSuffix = '@' + emailDomain.domain;
if (!email.endsWith(allowedEmailSuffix)) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Cannot create an email with a different domain',
});
}
await prisma.organisationEmail.create({
data: {
id: generateDatabaseId('org_email'),
organisationId: emailDomain.organisationId,
emailName,
// replyTo,
email,
emailDomainId,
},
});
});

View File

@ -0,0 +1,12 @@
import { z } from 'zod';
export const ZCreateOrganisationEmailRequestSchema = z.object({
emailDomainId: z.string(),
emailName: z.string().min(1).max(100),
email: z.string().email().toLowerCase(),
// This does not need to be validated to be part of the domain.
// replyTo: z.string().email().optional(),
});
export const ZCreateOrganisationEmailResponseSchema = z.void();

View File

@ -0,0 +1,53 @@
import { deleteEmailDomain } from '@documenso/ee/server-only/lib/delete-email-domain';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZDeleteOrganisationEmailDomainRequestSchema,
ZDeleteOrganisationEmailDomainResponseSchema,
} from './delete-organisation-email-domain.types';
export const deleteOrganisationEmailDomainRoute = authenticatedProcedure
.input(ZDeleteOrganisationEmailDomainRequestSchema)
.output(ZDeleteOrganisationEmailDomainResponseSchema)
.mutation(async ({ input, ctx }) => {
const { emailDomainId } = input;
const { user } = ctx;
ctx.logger.info({
input: {
emailDomainId,
},
});
if (!IS_BILLING_ENABLED()) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Billing is not enabled',
});
}
const emailDomain = await prisma.emailDomain.findFirst({
where: {
id: emailDomainId,
organisation: buildOrganisationWhereQuery({
organisationId: undefined,
userId: user.id,
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
}),
},
});
if (!emailDomain) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Email domain not found',
});
}
await deleteEmailDomain({
emailDomainId: emailDomain.id,
});
});

View File

@ -0,0 +1,7 @@
import { z } from 'zod';
export const ZDeleteOrganisationEmailDomainRequestSchema = z.object({
emailDomainId: z.string(),
});
export const ZDeleteOrganisationEmailDomainResponseSchema = z.void();

View File

@ -0,0 +1,45 @@
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZDeleteOrganisationEmailRequestSchema,
ZDeleteOrganisationEmailResponseSchema,
} from './delete-organisation-email.types';
export const deleteOrganisationEmailRoute = authenticatedProcedure
.input(ZDeleteOrganisationEmailRequestSchema)
.output(ZDeleteOrganisationEmailResponseSchema)
.mutation(async ({ input, ctx }) => {
const { emailId } = input;
const { user } = ctx;
ctx.logger.info({
input: {
emailId,
},
});
const email = await prisma.organisationEmail.findFirst({
where: {
id: emailId,
organisation: buildOrganisationWhereQuery({
organisationId: undefined,
userId: user.id,
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
}),
},
});
if (!email) {
throw new AppError(AppErrorCode.UNAUTHORIZED);
}
await prisma.organisationEmail.delete({
where: {
id: email.id,
},
});
});

View File

@ -0,0 +1,7 @@
import { z } from 'zod';
export const ZDeleteOrganisationEmailRequestSchema = z.object({
emailId: z.string(),
});
export const ZDeleteOrganisationEmailResponseSchema = z.void();

View File

@ -0,0 +1,122 @@
import type { EmailDomainStatus } from '@prisma/client';
import { Prisma } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { FindResultResponse } from '@documenso/lib/types/search-params';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZFindOrganisationEmailDomainsRequestSchema,
ZFindOrganisationEmailDomainsResponseSchema,
} from './find-organisation-email-domain.types';
export const findOrganisationEmailDomainsRoute = authenticatedProcedure
.input(ZFindOrganisationEmailDomainsRequestSchema)
.output(ZFindOrganisationEmailDomainsResponseSchema)
.query(async ({ input, ctx }) => {
const { organisationId, emailDomainId, statuses, query, page, perPage } = input;
const { user } = ctx;
ctx.logger.info({
input: {
organisationId,
},
});
return await findOrganisationEmailDomains({
userId: user.id,
organisationId,
emailDomainId,
statuses,
query,
page,
perPage,
});
});
type FindOrganisationEmailDomainsOptions = {
userId: number;
organisationId: string;
emailDomainId?: string;
statuses?: EmailDomainStatus[];
query?: string;
page?: number;
perPage?: number;
};
export const findOrganisationEmailDomains = async ({
userId,
organisationId,
emailDomainId,
statuses = [],
query,
page = 1,
perPage = 100,
}: FindOrganisationEmailDomainsOptions) => {
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery({ organisationId, userId }),
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
const whereClause: Prisma.EmailDomainWhereInput = {
organisationId: organisation.id,
status: statuses.length > 0 ? { in: statuses } : undefined,
};
if (emailDomainId) {
whereClause.id = emailDomainId;
}
if (query) {
whereClause.domain = {
contains: query,
mode: Prisma.QueryMode.insensitive,
};
}
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,
status: true,
organisationId: true,
domain: true,
selector: true,
createdAt: true,
updatedAt: true,
_count: {
select: {
emails: true,
},
},
},
}),
prisma.emailDomain.count({
where: whereClause,
}),
]);
const mappedData = data.map((item) => ({
...item,
emailCount: item._count.emails,
}));
return {
data: mappedData,
count,
currentPage: page,
perPage,
totalPages: Math.ceil(count / perPage),
} satisfies FindResultResponse<typeof mappedData>;
};

View File

@ -0,0 +1,23 @@
import { EmailDomainStatus } from '@prisma/client';
import { z } from 'zod';
import { ZEmailDomainManySchema } from '@documenso/lib/types/email-domain';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
export const ZFindOrganisationEmailDomainsRequestSchema = ZFindSearchParamsSchema.extend({
organisationId: z.string(),
emailDomainId: z.string().optional(),
statuses: z.nativeEnum(EmailDomainStatus).array().optional(),
});
export const ZFindOrganisationEmailDomainsResponseSchema = ZFindResultResponse.extend({
data: z.array(
ZEmailDomainManySchema.extend({
emailCount: z.number(),
}),
),
});
export type TFindOrganisationEmailDomainsResponse = z.infer<
typeof ZFindOrganisationEmailDomainsResponseSchema
>;

View File

@ -0,0 +1,105 @@
import { Prisma } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { FindResultResponse } from '@documenso/lib/types/search-params';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZFindOrganisationEmailsRequestSchema,
ZFindOrganisationEmailsResponseSchema,
} from './find-organisation-emails.types';
export const findOrganisationEmailsRoute = authenticatedProcedure
.input(ZFindOrganisationEmailsRequestSchema)
.output(ZFindOrganisationEmailsResponseSchema)
.query(async ({ input, ctx }) => {
const { organisationId, emailDomainId, query, page, perPage } = input;
const { user } = ctx;
ctx.logger.info({
input: {
organisationId,
},
});
return await findOrganisationEmails({
userId: user.id,
organisationId,
emailDomainId,
query,
page,
perPage,
});
});
type FindOrganisationEmailsOptions = {
userId: number;
organisationId: string;
emailDomainId?: string;
query?: string;
page?: number;
perPage?: number;
};
export const findOrganisationEmails = async ({
userId,
organisationId,
emailDomainId,
query,
page = 1,
perPage = 100,
}: FindOrganisationEmailsOptions) => {
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery({ organisationId, userId }),
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
const whereClause: Prisma.OrganisationEmailWhereInput = {
organisationId: organisation.id,
emailDomainId,
};
if (query) {
whereClause.email = {
contains: query,
mode: Prisma.QueryMode.insensitive,
};
}
const [data, count] = await Promise.all([
prisma.organisationEmail.findMany({
where: whereClause,
skip: Math.max(page - 1, 0) * perPage,
take: perPage,
orderBy: {
createdAt: 'desc',
},
select: {
id: true,
createdAt: true,
updatedAt: true,
email: true,
emailName: true,
// replyTo: true,
emailDomainId: true,
organisationId: true,
},
}),
prisma.organisationEmail.count({
where: whereClause,
}),
]);
return {
data,
count,
currentPage: page,
perPage,
totalPages: Math.ceil(count / perPage),
} satisfies FindResultResponse<typeof data>;
};

View File

@ -0,0 +1,15 @@
import { z } from 'zod';
import { ZOrganisationEmailManySchema } from '@documenso/lib/types/organisation-email';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
export const ZFindOrganisationEmailsRequestSchema = ZFindSearchParamsSchema.extend({
organisationId: z.string(),
emailDomainId: z.string().optional(),
});
export const ZFindOrganisationEmailsResponseSchema = ZFindResultResponse.extend({
data: ZOrganisationEmailManySchema.array(),
});
export type TFindOrganisationEmailsResponse = z.infer<typeof ZFindOrganisationEmailsResponseSchema>;

View File

@ -0,0 +1,63 @@
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZGetOrganisationEmailDomainRequestSchema,
ZGetOrganisationEmailDomainResponseSchema,
} from './get-organisation-email-domain.types';
export const getOrganisationEmailDomainRoute = authenticatedProcedure
.input(ZGetOrganisationEmailDomainRequestSchema)
.output(ZGetOrganisationEmailDomainResponseSchema)
.query(async ({ input, ctx }) => {
const { emailDomainId } = input;
ctx.logger.info({
input: {
emailDomainId,
},
});
return await getOrganisationEmailDomain({
userId: ctx.user.id,
emailDomainId,
});
});
type GetOrganisationEmailDomainOptions = {
userId: number;
emailDomainId: string;
};
export const getOrganisationEmailDomain = async ({
userId,
emailDomainId,
}: GetOrganisationEmailDomainOptions) => {
const emailDomain = await prisma.emailDomain.findFirst({
where: {
id: emailDomainId,
organisation: buildOrganisationWhereQuery({
organisationId: undefined,
userId,
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
}),
},
omit: {
privateKey: true,
},
include: {
emails: true,
},
});
if (!emailDomain) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Email domain not found',
});
}
return emailDomain;
};

View File

@ -0,0 +1,13 @@
import { z } from 'zod';
import { ZEmailDomainSchema } from '@documenso/lib/types/email-domain';
export const ZGetOrganisationEmailDomainRequestSchema = z.object({
emailDomainId: z.string(),
});
export const ZGetOrganisationEmailDomainResponseSchema = ZEmailDomainSchema;
export type TGetOrganisationEmailDomainResponse = z.infer<
typeof ZGetOrganisationEmailDomainResponseSchema
>;

View File

@ -0,0 +1,46 @@
import { router } from '../trpc';
import { createOrganisationEmailRoute } from './create-organisation-email';
import { createOrganisationEmailDomainRoute } from './create-organisation-email-domain';
import { createSubscriptionRoute } from './create-subscription';
import { deleteOrganisationEmailRoute } from './delete-organisation-email';
import { deleteOrganisationEmailDomainRoute } from './delete-organisation-email-domain';
import { findOrganisationEmailDomainsRoute } from './find-organisation-email-domain';
import { findOrganisationEmailsRoute } from './find-organisation-emails';
import { getInvoicesRoute } from './get-invoices';
import { getOrganisationEmailDomainRoute } from './get-organisation-email-domain';
import { getPlansRoute } from './get-plans';
import { getSubscriptionRoute } from './get-subscription';
import { manageSubscriptionRoute } from './manage-subscription';
import { updateOrganisationEmailRoute } from './update-organisation-email';
import { verifyOrganisationEmailDomainRoute } from './verify-organisation-email-domain';
export const enterpriseRouter = router({
organisation: {
email: {
find: findOrganisationEmailsRoute,
create: createOrganisationEmailRoute,
update: updateOrganisationEmailRoute,
delete: deleteOrganisationEmailRoute,
},
emailDomain: {
get: getOrganisationEmailDomainRoute,
find: findOrganisationEmailDomainsRoute,
create: createOrganisationEmailDomainRoute,
delete: deleteOrganisationEmailDomainRoute,
verify: verifyOrganisationEmailDomainRoute,
},
},
billing: {
plans: {
get: getPlansRoute,
},
subscription: {
get: getSubscriptionRoute,
create: createSubscriptionRoute,
manage: manageSubscriptionRoute,
},
invoices: {
get: getInvoicesRoute,
},
},
});

View File

@ -0,0 +1,49 @@
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZUpdateOrganisationEmailRequestSchema,
ZUpdateOrganisationEmailResponseSchema,
} from './update-organisation-email.types';
export const updateOrganisationEmailRoute = authenticatedProcedure
.input(ZUpdateOrganisationEmailRequestSchema)
.output(ZUpdateOrganisationEmailResponseSchema)
.mutation(async ({ input, ctx }) => {
const { emailId, emailName } = input;
const { user } = ctx;
ctx.logger.info({
input: {
emailId,
},
});
const organisationEmail = await prisma.organisationEmail.findFirst({
where: {
id: emailId,
organisation: buildOrganisationWhereQuery({
organisationId: undefined,
userId: user.id,
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
}),
},
});
if (!organisationEmail) {
throw new AppError(AppErrorCode.UNAUTHORIZED);
}
await prisma.organisationEmail.update({
where: {
id: emailId,
},
data: {
emailName,
// replyTo,
},
});
});

View File

@ -0,0 +1,18 @@
import { z } from 'zod';
import { ZCreateOrganisationEmailRequestSchema } from './create-organisation-email.types';
export const ZUpdateOrganisationEmailRequestSchema = z
.object({
emailId: z.string(),
})
.extend(
ZCreateOrganisationEmailRequestSchema.pick({
emailName: true,
// replyTo: true
}).shape,
);
export const ZUpdateOrganisationEmailResponseSchema = z.void();
export type TUpdateOrganisationEmailRequest = z.infer<typeof ZUpdateOrganisationEmailRequestSchema>;

View File

@ -0,0 +1,59 @@
import { verifyEmailDomain } from '@documenso/ee/server-only/lib/verify-email-domain';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZVerifyOrganisationEmailDomainRequestSchema,
ZVerifyOrganisationEmailDomainResponseSchema,
} from './verify-organisation-email-domain.types';
export const verifyOrganisationEmailDomainRoute = authenticatedProcedure
.input(ZVerifyOrganisationEmailDomainRequestSchema)
.output(ZVerifyOrganisationEmailDomainResponseSchema)
.mutation(async ({ input, ctx }) => {
const { organisationId, emailDomainId } = input;
const { user } = ctx;
ctx.logger.info({
input: {
organisationId,
emailDomainId,
},
});
if (!IS_BILLING_ENABLED()) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Billing is not enabled',
});
}
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery({
organisationId,
userId: user.id,
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
}),
include: {
emailDomains: true,
},
});
if (!organisation) {
throw new AppError(AppErrorCode.UNAUTHORIZED);
}
// Filter down emails to verify a specific email, otherwise verify all emails regardless of status.
const emailsToVerify = organisation.emailDomains.filter((email) => {
if (emailDomainId && email.id !== emailDomainId) {
return false;
}
return true;
});
await Promise.all(emailsToVerify.map(async (email) => verifyEmailDomain(email.id)));
});

View File

@ -0,0 +1,8 @@
import { z } from 'zod';
export const ZVerifyOrganisationEmailDomainRequestSchema = z.object({
organisationId: z.string(),
emailDomainId: z.string().optional().describe('Leave blank to revalidate all emails'),
});
export const ZVerifyOrganisationEmailDomainResponseSchema = z.void();

View File

@ -26,16 +26,25 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
// Document related settings.
documentVisibility,
documentLanguage,
documentTimezone,
documentDateFormat,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
// Branding related settings.
brandingEnabled,
brandingLogo,
brandingUrl,
brandingCompanyDetails,
// Email related settings.
emailId,
emailReplyTo,
// emailReplyToName,
emailDocumentSettings,
} = data;
if (Object.values(data).length === 0) {
@ -61,6 +70,22 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
});
}
// Validate that the email ID belongs to the organisation.
if (emailId) {
const email = await prisma.organisationEmail.findFirst({
where: {
id: emailId,
organisationId,
},
});
if (!email) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Email not found',
});
}
}
const derivedTypedSignatureEnabled =
typedSignatureEnabled ?? organisation.organisationGlobalSettings.typedSignatureEnabled;
const derivedUploadSignatureEnabled =
@ -88,6 +113,8 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
// Document related settings.
documentVisibility,
documentLanguage,
documentTimezone,
documentDateFormat,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
@ -99,6 +126,12 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
brandingLogo,
brandingUrl,
brandingCompanyDetails,
// Email related settings.
emailId,
emailReplyTo,
// emailReplyToName,
emailDocumentSettings,
},
},
},

View File

@ -1,14 +1,22 @@
import { z } from 'zod';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaTimezoneSchema,
} from '../document-router/schema';
export const ZUpdateOrganisationSettingsRequestSchema = z.object({
organisationId: z.string(),
data: z.object({
// Document related settings.
documentVisibility: z.nativeEnum(DocumentVisibility).optional(),
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
documentTimezone: ZDocumentMetaTimezoneSchema.nullish(), // Null means local timezone.
documentDateFormat: ZDocumentMetaDateFormatSchema.optional(),
includeSenderDetails: z.boolean().optional(),
includeSigningCertificate: z.boolean().optional(),
typedSignatureEnabled: z.boolean().optional(),
@ -20,6 +28,12 @@ export const ZUpdateOrganisationSettingsRequestSchema = z.object({
brandingLogo: z.string().optional(),
brandingUrl: z.string().optional(),
brandingCompanyDetails: z.string().optional(),
// Email related settings.
emailId: z.string().nullish(),
emailReplyTo: z.string().email().nullish(),
// emailReplyToName: z.string().optional(),
emailDocumentSettings: ZDocumentEmailSettingsSchema.optional(),
}),
});

View File

@ -1,9 +1,9 @@
import { adminRouter } from './admin-router/router';
import { apiTokenRouter } from './api-token-router/router';
import { authRouter } from './auth-router/router';
import { billingRouter } from './billing/router';
import { documentRouter } from './document-router/router';
import { embeddingPresignRouter } from './embedding-router/_router';
import { enterpriseRouter } from './enterprise-router/router';
import { fieldRouter } from './field-router/router';
import { folderRouter } from './folder-router/router';
import { organisationRouter } from './organisation-router/router';
@ -16,8 +16,8 @@ import { router } from './trpc';
import { webhookRouter } from './webhook-router/router';
export const appRouter = router({
enterprise: enterpriseRouter,
auth: authRouter,
billing: billingRouter,
profile: profileRouter,
document: documentRouter,
field: fieldRouter,

View File

@ -1,3 +1,5 @@
import { Prisma } from '@prisma/client';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
@ -26,6 +28,8 @@ export const updateTeamSettingsRoute = authenticatedProcedure
// Document related settings.
documentVisibility,
documentLanguage,
documentTimezone,
documentDateFormat,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
@ -37,6 +41,12 @@ export const updateTeamSettingsRoute = authenticatedProcedure
brandingLogo,
brandingUrl,
brandingCompanyDetails,
// Email related settings.
emailId,
emailReplyTo,
// emailReplyToName,
emailDocumentSettings,
} = data;
if (Object.values(data).length === 0) {
@ -70,6 +80,22 @@ export const updateTeamSettingsRoute = authenticatedProcedure
});
}
// Validate that the email ID belongs to the organisation.
if (emailId) {
const email = await prisma.organisationEmail.findFirst({
where: {
id: emailId,
organisationId: team.organisationId,
},
});
if (!email) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Email not found',
});
}
}
await prisma.team.update({
where: {
id: teamId,
@ -80,6 +106,8 @@ export const updateTeamSettingsRoute = authenticatedProcedure
// Document related settings.
documentVisibility,
documentLanguage,
documentTimezone,
documentDateFormat,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
@ -91,6 +119,13 @@ export const updateTeamSettingsRoute = authenticatedProcedure
brandingLogo,
brandingUrl,
brandingCompanyDetails,
// Email related settings.
emailId,
emailReplyTo,
// emailReplyToName,
emailDocumentSettings:
emailDocumentSettings === null ? Prisma.DbNull : emailDocumentSettings,
},
},
},

View File

@ -1,8 +1,14 @@
import { z } from 'zod';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaTimezoneSchema,
} from '../document-router/schema';
/**
* Null = Inherit from organisation.
* Undefined = Do nothing
@ -13,6 +19,8 @@ export const ZUpdateTeamSettingsRequestSchema = z.object({
// Document related settings.
documentVisibility: z.nativeEnum(DocumentVisibility).nullish(),
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).nullish(),
documentTimezone: ZDocumentMetaTimezoneSchema.nullish(),
documentDateFormat: ZDocumentMetaDateFormatSchema.nullish(),
includeSenderDetails: z.boolean().nullish(),
includeSigningCertificate: z.boolean().nullish(),
typedSignatureEnabled: z.boolean().nullish(),
@ -24,6 +32,12 @@ export const ZUpdateTeamSettingsRequestSchema = z.object({
brandingLogo: z.string().nullish(),
brandingUrl: z.string().nullish(),
brandingCompanyDetails: z.string().nullish(),
// Email related settings.
emailId: z.string().nullish(),
emailReplyTo: z.string().email().nullish(),
// emailReplyToName: z.string().nullish(),
emailDocumentSettings: ZDocumentEmailSettingsSchema.nullish(),
}),
});

View File

@ -1,9 +1,11 @@
import type { Document } from '@prisma/client';
import { DocumentDataType } from '@prisma/client';
import { TRPCError } from '@trpc/server';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobs } from '@documenso/lib/jobs/client';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import {
@ -23,6 +25,7 @@ import { findTemplates } from '@documenso/lib/server-only/template/find-template
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 { updateTemplate } from '@documenso/lib/server-only/template/update-template';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../document-router/schema';
import { authenticatedProcedure, maybeAuthenticatedProcedure, router } from '../trpc';
@ -34,6 +37,8 @@ import {
ZCreateTemplateDirectLinkRequestSchema,
ZCreateTemplateDirectLinkResponseSchema,
ZCreateTemplateMutationSchema,
ZCreateTemplateV2RequestSchema,
ZCreateTemplateV2ResponseSchema,
ZDeleteTemplateDirectLinkRequestSchema,
ZDeleteTemplateMutationSchema,
ZDuplicateTemplateMutationSchema,
@ -141,12 +146,88 @@ export const templateRouter = router({
return await createTemplate({
userId: ctx.user.id,
teamId,
title,
templateDocumentDataId,
folderId,
data: {
title,
folderId,
},
});
}),
/**
* Temporariy endpoint for V2 Beta until we allow passthrough documents on create.
*
* @public
* @deprecated
*/
createTemplateTemporary: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/create/beta',
summary: 'Create template',
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: ['Template'],
},
})
.input(ZCreateTemplateV2RequestSchema)
.output(ZCreateTemplateV2ResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const {
title,
folderId,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
publicTitle,
publicDescription,
type,
meta,
} = input;
const fileName = title.endsWith('.pdf') ? title : `${title}.pdf`;
const { url, key } = await getPresignPostUrl(fileName, 'application/pdf');
const templateDocumentData = await createDocumentData({
data: key,
type: DocumentDataType.S3_PATH,
});
const createdTemplate = await createTemplate({
userId: user.id,
teamId,
templateDocumentDataId: templateDocumentData.id,
data: {
title,
folderId,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
publicTitle,
publicDescription,
type,
},
meta,
});
const fullTemplate = await getTemplateById({
id: createdTemplate.id,
userId: user.id,
teamId,
});
return {
template: fullTemplate,
uploadUrl: url,
};
}),
/**
* @public
*/

View File

@ -30,6 +30,52 @@ import {
} from '../document-router/schema';
import { ZSignFieldWithTokenMutationSchema } from '../field-router/schema';
export const MAX_TEMPLATE_PUBLIC_TITLE_LENGTH = 50;
export const MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH = 256;
export const ZTemplateTitleSchema = z
.string()
.trim()
.min(1)
.max(255)
.describe('The title of the document.');
export const ZTemplatePublicTitleSchema = z
.string()
.trim()
.min(1)
.max(MAX_TEMPLATE_PUBLIC_TITLE_LENGTH)
.describe(
'The title of the template that will be displayed to the public. Only applicable for public templates.',
);
export const ZTemplatePublicDescriptionSchema = z
.string()
.trim()
.min(1)
.max(MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH)
.describe(
'The description of the template that will be displayed to the public. Only applicable for public templates.',
);
export const ZTemplateMetaUpsertSchema = z.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
emailId: z.string().nullish(),
emailReplyTo: z.string().nullish(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
allowDictateNextSigner: z.boolean().optional(),
});
export const ZCreateTemplateMutationSchema = z.object({
title: z.string().min(1).trim(),
templateDocumentDataId: z.string().min(1),
@ -123,57 +169,46 @@ export const ZDeleteTemplateMutationSchema = z.object({
templateId: z.number(),
});
export const MAX_TEMPLATE_PUBLIC_TITLE_LENGTH = 50;
export const MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH = 256;
/**
* Note: This is the same between V1 and V2. Be careful when updating this schema and think of the consequences.
*/
export const ZCreateTemplateV2RequestSchema = z.object({
title: ZTemplateTitleSchema,
folderId: z.string().optional(),
externalId: z.string().nullish(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
publicTitle: ZTemplatePublicTitleSchema.optional(),
publicDescription: ZTemplatePublicDescriptionSchema.optional(),
type: z.nativeEnum(TemplateType).optional(),
meta: ZTemplateMetaUpsertSchema.optional(),
});
/**
* Note: This is the same between V1 and V2. Be careful when updating this schema and think of the consequences.
*/
export const ZCreateTemplateV2ResponseSchema = z.object({
template: ZTemplateSchema,
uploadUrl: z.string().min(1),
});
export const ZUpdateTemplateRequestSchema = z.object({
templateId: z.number(),
data: z
.object({
title: z.string().min(1).optional(),
title: ZTemplateTitleSchema.optional(),
externalId: z.string().nullish(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
publicTitle: z
.string()
.trim()
.min(1)
.max(MAX_TEMPLATE_PUBLIC_TITLE_LENGTH)
.describe(
'The title of the template that will be displayed to the public. Only applicable for public templates.',
)
.optional(),
publicDescription: z
.string()
.trim()
.min(1)
.max(MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH)
.describe(
'The description of the template that will be displayed to the public. Only applicable for public templates.',
)
.optional(),
publicTitle: ZTemplatePublicTitleSchema.optional(),
publicDescription: ZTemplatePublicDescriptionSchema.optional(),
type: z.nativeEnum(TemplateType).optional(),
useLegacyFieldInsertion: z.boolean().optional(),
})
.optional(),
meta: z
.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
allowDictateNextSigner: z.boolean().optional(),
})
.optional(),
meta: ZTemplateMetaUpsertSchema.optional(),
});
export const ZUpdateTemplateResponseSchema = ZTemplateLiteSchema;