mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 15:53:02 +10:00
feat: add signature configurations (#1710)
Add ability to enable or disable allowed signature types: - Drawn - Typed - Uploaded **Tabbed style signature dialog**  **Document settings**  **Team preferences**  - Add multiselect to select allowed signatures in document and templates settings tab - Add multiselect to select allowed signatures in teams preferences - Removed "Enable typed signatures" from document/template edit page - Refactored signature pad to use tabs instead of an all in one signature pad Added E2E tests to check settings are applied correctly for documents and templates
This commit is contained in:
@ -56,9 +56,12 @@ import {
|
||||
ZSearchDocumentsMutationSchema,
|
||||
ZSetSigningOrderForDocumentMutationSchema,
|
||||
ZSuccessResponseSchema,
|
||||
} from './schema';
|
||||
import { updateDocumentRoute } from './update-document';
|
||||
import {
|
||||
ZUpdateDocumentRequestSchema,
|
||||
ZUpdateDocumentResponseSchema,
|
||||
} from './schema';
|
||||
} from './update-document.types';
|
||||
|
||||
export const documentRouter = router({
|
||||
/**
|
||||
@ -335,20 +338,12 @@ export const documentRouter = router({
|
||||
});
|
||||
}),
|
||||
|
||||
updateDocument: updateDocumentRoute,
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* Todo: Refactor to updateDocument.
|
||||
* @deprecated Delete this after updateDocument endpoint is deployed
|
||||
*/
|
||||
setSettingsForDocument: authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/document/update',
|
||||
summary: 'Update document',
|
||||
tags: ['Document'],
|
||||
},
|
||||
})
|
||||
.input(ZUpdateDocumentRequestSchema)
|
||||
.output(ZUpdateDocumentResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@ -368,6 +363,8 @@ export const documentRouter = router({
|
||||
dateFormat: meta.dateFormat,
|
||||
language: meta.language,
|
||||
typedSignatureEnabled: meta.typedSignatureEnabled,
|
||||
uploadSignatureEnabled: meta.uploadSignatureEnabled,
|
||||
drawSignatureEnabled: meta.drawSignatureEnabled,
|
||||
redirectUrl: meta.redirectUrl,
|
||||
distributionMethod: meta.distributionMethod,
|
||||
signingOrder: meta.signingOrder,
|
||||
|
||||
@ -109,6 +109,14 @@ export const ZDocumentMetaTypedSignatureEnabledSchema = z
|
||||
.boolean()
|
||||
.describe('Whether to allow recipients to sign using a typed signature.');
|
||||
|
||||
export const ZDocumentMetaDrawSignatureEnabledSchema = z
|
||||
.boolean()
|
||||
.describe('Whether to allow recipients to sign using a draw signature.');
|
||||
|
||||
export const ZDocumentMetaUploadSignatureEnabledSchema = z
|
||||
.boolean()
|
||||
.describe('Whether to allow recipients to sign using an uploaded signature.');
|
||||
|
||||
export const ZFindDocumentsRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
templateId: z
|
||||
.number()
|
||||
@ -233,6 +241,8 @@ export const ZCreateDocumentV2RequestSchema = z.object({
|
||||
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
|
||||
language: ZDocumentMetaLanguageSchema.optional(),
|
||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
|
||||
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
@ -249,36 +259,6 @@ export const ZCreateDocumentV2ResponseSchema = z.object({
|
||||
),
|
||||
});
|
||||
|
||||
export const ZUpdateDocumentRequestSchema = z.object({
|
||||
documentId: z.number(),
|
||||
data: z
|
||||
.object({
|
||||
title: ZDocumentTitleSchema.optional(),
|
||||
externalId: ZDocumentExternalIdSchema.nullish(),
|
||||
visibility: ZDocumentVisibilitySchema.optional(),
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullish(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.nullish(),
|
||||
})
|
||||
.optional(),
|
||||
meta: z
|
||||
.object({
|
||||
subject: ZDocumentMetaSubjectSchema.optional(),
|
||||
message: ZDocumentMetaMessageSchema.optional(),
|
||||
timezone: ZDocumentMetaTimezoneSchema.optional(),
|
||||
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
|
||||
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||
allowDictateNextSigner: z.boolean().optional(),
|
||||
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
|
||||
language: ZDocumentMetaLanguageSchema.optional(),
|
||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const ZUpdateDocumentResponseSchema = ZDocumentLiteSchema;
|
||||
|
||||
export const ZSetFieldsForDocumentMutationSchema = z.object({
|
||||
documentId: z.number(),
|
||||
fields: z.array(
|
||||
|
||||
52
packages/trpc/server/document-router/update-document.ts
Normal file
52
packages/trpc/server/document-router/update-document.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
|
||||
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZUpdateDocumentRequestSchema,
|
||||
ZUpdateDocumentResponseSchema,
|
||||
} from './update-document.types';
|
||||
import { updateDocumentMeta } from './update-document.types';
|
||||
|
||||
/**
|
||||
* Public route.
|
||||
*/
|
||||
export const updateDocumentRoute = authenticatedProcedure
|
||||
.meta(updateDocumentMeta)
|
||||
.input(ZUpdateDocumentRequestSchema)
|
||||
.output(ZUpdateDocumentResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId } = ctx;
|
||||
const { documentId, data, meta = {} } = input;
|
||||
|
||||
const userId = ctx.user.id;
|
||||
|
||||
if (Object.values(meta).length > 0) {
|
||||
await upsertDocumentMeta({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
documentId,
|
||||
subject: meta.subject,
|
||||
message: meta.message,
|
||||
timezone: meta.timezone,
|
||||
dateFormat: meta.dateFormat,
|
||||
language: meta.language,
|
||||
typedSignatureEnabled: meta.typedSignatureEnabled,
|
||||
uploadSignatureEnabled: meta.uploadSignatureEnabled,
|
||||
drawSignatureEnabled: meta.drawSignatureEnabled,
|
||||
redirectUrl: meta.redirectUrl,
|
||||
distributionMethod: meta.distributionMethod,
|
||||
signingOrder: meta.signingOrder,
|
||||
emailSettings: meta.emailSettings,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
return await updateDocument({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
data,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,68 @@
|
||||
import { DocumentSigningOrder } from '@prisma/client';
|
||||
// import type { OpenApiMeta } from 'trpc-to-openapi';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZDocumentLiteSchema } from '@documenso/lib/types/document';
|
||||
import {
|
||||
ZDocumentAccessAuthTypesSchema,
|
||||
ZDocumentActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
|
||||
import type { TrpcRouteMeta } from '../trpc';
|
||||
import {
|
||||
ZDocumentExternalIdSchema,
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
ZDocumentMetaDistributionMethodSchema,
|
||||
ZDocumentMetaDrawSignatureEnabledSchema,
|
||||
ZDocumentMetaLanguageSchema,
|
||||
ZDocumentMetaMessageSchema,
|
||||
ZDocumentMetaRedirectUrlSchema,
|
||||
ZDocumentMetaSubjectSchema,
|
||||
ZDocumentMetaTimezoneSchema,
|
||||
ZDocumentMetaTypedSignatureEnabledSchema,
|
||||
ZDocumentMetaUploadSignatureEnabledSchema,
|
||||
ZDocumentTitleSchema,
|
||||
ZDocumentVisibilitySchema,
|
||||
} from './schema';
|
||||
|
||||
export const updateDocumentMeta: TrpcRouteMeta = {
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/document/update',
|
||||
summary: 'Update document',
|
||||
tags: ['Document'],
|
||||
},
|
||||
};
|
||||
|
||||
export const ZUpdateDocumentRequestSchema = z.object({
|
||||
documentId: z.number(),
|
||||
data: z
|
||||
.object({
|
||||
title: ZDocumentTitleSchema.optional(),
|
||||
externalId: ZDocumentExternalIdSchema.nullish(),
|
||||
visibility: ZDocumentVisibilitySchema.optional(),
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullish(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.nullish(),
|
||||
})
|
||||
.optional(),
|
||||
meta: z
|
||||
.object({
|
||||
subject: ZDocumentMetaSubjectSchema.optional(),
|
||||
message: ZDocumentMetaMessageSchema.optional(),
|
||||
timezone: ZDocumentMetaTimezoneSchema.optional(),
|
||||
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
|
||||
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||
allowDictateNextSigner: z.boolean().optional(),
|
||||
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
|
||||
language: ZDocumentMetaLanguageSchema.optional(),
|
||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
|
||||
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const ZUpdateDocumentResponseSchema = ZDocumentLiteSchema;
|
||||
@ -33,7 +33,6 @@ import { resendTeamEmailVerification } from '@documenso/lib/server-only/team/res
|
||||
import { resendTeamMemberInvitation } from '@documenso/lib/server-only/team/resend-team-member-invitation';
|
||||
import { updateTeam } from '@documenso/lib/server-only/team/update-team';
|
||||
import { updateTeamBrandingSettings } from '@documenso/lib/server-only/team/update-team-branding-settings';
|
||||
import { updateTeamDocumentSettings } from '@documenso/lib/server-only/team/update-team-document-settings';
|
||||
import { updateTeamEmail } from '@documenso/lib/server-only/team/update-team-email';
|
||||
import { updateTeamMember } from '@documenso/lib/server-only/team/update-team-member';
|
||||
import { updateTeamPublicProfile } from '@documenso/lib/server-only/team/update-team-public-profile';
|
||||
@ -66,12 +65,12 @@ import {
|
||||
ZResendTeamEmailVerificationMutationSchema,
|
||||
ZResendTeamMemberInvitationMutationSchema,
|
||||
ZUpdateTeamBrandingSettingsMutationSchema,
|
||||
ZUpdateTeamDocumentSettingsMutationSchema,
|
||||
ZUpdateTeamEmailMutationSchema,
|
||||
ZUpdateTeamMemberMutationSchema,
|
||||
ZUpdateTeamMutationSchema,
|
||||
ZUpdateTeamPublicProfileMutationSchema,
|
||||
} from './schema';
|
||||
import { updateTeamDocumentSettingsRoute } from './update-team-document-settings';
|
||||
|
||||
export const teamRouter = router({
|
||||
// Internal endpoint for now.
|
||||
@ -571,18 +570,7 @@ export const teamRouter = router({
|
||||
return await getTeamPrices();
|
||||
}),
|
||||
|
||||
// Internal endpoint. Use updateTeam instead.
|
||||
updateTeamDocumentSettings: authenticatedProcedure
|
||||
.input(ZUpdateTeamDocumentSettingsMutationSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { teamId, settings } = input;
|
||||
|
||||
return await updateTeamDocumentSettings({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
settings,
|
||||
});
|
||||
}),
|
||||
updateTeamDocumentSettings: updateTeamDocumentSettingsRoute,
|
||||
|
||||
// Internal endpoint for now.
|
||||
acceptTeamInvitation: authenticatedProcedure
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
|
||||
import { PROTECTED_TEAM_URLS } from '@documenso/lib/constants/teams';
|
||||
import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
|
||||
@ -195,20 +194,6 @@ export const ZUpdateTeamBrandingSettingsMutationSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZUpdateTeamDocumentSettingsMutationSchema = z.object({
|
||||
teamId: z.number(),
|
||||
settings: z.object({
|
||||
documentVisibility: z
|
||||
.nativeEnum(DocumentVisibility)
|
||||
.optional()
|
||||
.default(DocumentVisibility.EVERYONE),
|
||||
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).optional().default('en'),
|
||||
includeSenderDetails: z.boolean().optional().default(false),
|
||||
typedSignatureEnabled: z.boolean().optional().default(true),
|
||||
includeSigningCertificate: z.boolean().optional().default(true),
|
||||
}),
|
||||
});
|
||||
|
||||
export type TCreateTeamMutationSchema = z.infer<typeof ZCreateTeamMutationSchema>;
|
||||
export type TCreateTeamEmailVerificationMutationSchema = z.infer<
|
||||
typeof ZCreateTeamEmailVerificationMutationSchema
|
||||
@ -247,6 +232,3 @@ export type TResendTeamMemberInvitationMutationSchema = z.infer<
|
||||
export type TUpdateTeamBrandingSettingsMutationSchema = z.infer<
|
||||
typeof ZUpdateTeamBrandingSettingsMutationSchema
|
||||
>;
|
||||
export type TUpdateTeamDocumentSettingsMutationSchema = z.infer<
|
||||
typeof ZUpdateTeamDocumentSettingsMutationSchema
|
||||
>;
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZUpdateTeamDocumentSettingsRequestSchema,
|
||||
ZUpdateTeamDocumentSettingsResponseSchema,
|
||||
} from './update-team-document-settings.types';
|
||||
|
||||
/**
|
||||
* Private route.
|
||||
*/
|
||||
export const updateTeamDocumentSettingsRoute = authenticatedProcedure
|
||||
.input(ZUpdateTeamDocumentSettingsRequestSchema)
|
||||
.output(ZUpdateTeamDocumentSettingsResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { user } = ctx;
|
||||
const { teamId, settings } = input;
|
||||
|
||||
const {
|
||||
documentVisibility,
|
||||
documentLanguage,
|
||||
includeSenderDetails,
|
||||
includeSigningCertificate,
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
} = settings;
|
||||
|
||||
const member = await prisma.teamMember.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
teamId,
|
||||
role: {
|
||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP.MANAGE_TEAM,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!member) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update this team.',
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.teamGlobalSettings.upsert({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
create: {
|
||||
teamId,
|
||||
documentVisibility,
|
||||
documentLanguage,
|
||||
includeSenderDetails,
|
||||
includeSigningCertificate,
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
},
|
||||
update: {
|
||||
documentVisibility,
|
||||
documentLanguage,
|
||||
includeSenderDetails,
|
||||
includeSigningCertificate,
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,23 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
|
||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
import TeamGlobalSettingsSchema from '@documenso/prisma/generated/zod/modelSchema/TeamGlobalSettingsSchema';
|
||||
|
||||
export const ZUpdateTeamDocumentSettingsRequestSchema = z.object({
|
||||
teamId: z.number(),
|
||||
settings: z.object({
|
||||
documentVisibility: z
|
||||
.nativeEnum(DocumentVisibility)
|
||||
.optional()
|
||||
.default(DocumentVisibility.EVERYONE),
|
||||
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).optional().default('en'),
|
||||
includeSenderDetails: z.boolean().optional().default(false),
|
||||
includeSigningCertificate: z.boolean().optional().default(true),
|
||||
typedSignatureEnabled: z.boolean().optional().default(true),
|
||||
uploadSignatureEnabled: z.boolean().optional().default(true),
|
||||
drawSignatureEnabled: z.boolean().optional().default(true),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZUpdateTeamDocumentSettingsResponseSchema = TeamGlobalSettingsSchema;
|
||||
@ -19,12 +19,14 @@ import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod/modelS
|
||||
import {
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
ZDocumentMetaDistributionMethodSchema,
|
||||
ZDocumentMetaDrawSignatureEnabledSchema,
|
||||
ZDocumentMetaLanguageSchema,
|
||||
ZDocumentMetaMessageSchema,
|
||||
ZDocumentMetaRedirectUrlSchema,
|
||||
ZDocumentMetaSubjectSchema,
|
||||
ZDocumentMetaTimezoneSchema,
|
||||
ZDocumentMetaTypedSignatureEnabledSchema,
|
||||
ZDocumentMetaUploadSignatureEnabledSchema,
|
||||
} from '../document-router/schema';
|
||||
import { ZSignFieldWithTokenMutationSchema } from '../field-router/schema';
|
||||
|
||||
@ -164,6 +166,8 @@ export const ZUpdateTemplateRequestSchema = z.object({
|
||||
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
|
||||
language: ZDocumentMetaLanguageSchema.optional(),
|
||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
|
||||
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||
allowDictateNextSigner: z.boolean().optional(),
|
||||
})
|
||||
|
||||
@ -9,8 +9,8 @@ import { isAdmin } from '@documenso/lib/utils/is-admin';
|
||||
|
||||
import type { TrpcContext } from './context';
|
||||
|
||||
// Can't import type from trpc-to-openapi because it breaks nextjs build, not sure why.
|
||||
type OpenApiMeta = {
|
||||
// Can't import type from trpc-to-openapi because it breaks build, not sure why.
|
||||
export type TrpcRouteMeta = {
|
||||
openapi?: {
|
||||
enabled?: boolean;
|
||||
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
||||
@ -30,7 +30,7 @@ type OpenApiMeta = {
|
||||
} & Record<string, unknown>;
|
||||
|
||||
const t = initTRPC
|
||||
.meta<OpenApiMeta>()
|
||||
.meta<TrpcRouteMeta>()
|
||||
.context<TrpcContext>()
|
||||
.create({
|
||||
transformer: SuperJSON,
|
||||
|
||||
Reference in New Issue
Block a user