mirror of
https://github.com/documenso/documenso.git
synced 2025-11-17 02:01:33 +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**  ## Changes Made - 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 ## Testing Performed 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,35 +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(),
|
||||
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,67 @@
|
||||
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(),
|
||||
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(),
|
||||
})
|
||||
.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