Compare commits

..

4 Commits

Author SHA1 Message Date
Mythie
a3ee732a9b v1.6.0-rc.2 2024-07-15 10:50:49 +10:00
Lucas Smith
c3035dbd15 feat: add external id to documents and templates (#1227)
## Description

Adds the external ID column to documents and templates with an option to
configure it in the API or UI.

External ID's can be used to link a document or template to an external
system and identify them via webhooks, etc.
2024-07-13 16:45:09 +10:00
Catalin Pit
7f5b27372f feat: resend document via API (#1226)
Allow users to re-send documents via the API.
2024-07-12 21:03:52 +10:00
Rene Steen
b0c081683f feat: allow anonymous smtp authentication (#1204)
Introduces the ability to use anonymous SMTP authentication where no username or password is provided.

Also introduces a new flag to disable TLS avoiding cases also where STARTTLS is used despite `secure` being
set to `false`
2024-07-09 10:39:59 +10:00
30 changed files with 229 additions and 16 deletions

View File

@@ -78,6 +78,8 @@ NEXT_PRIVATE_SMTP_APIKEY_USER=
NEXT_PRIVATE_SMTP_APIKEY= NEXT_PRIVATE_SMTP_APIKEY=
# OPTIONAL: Defines whether to force the use of TLS. # OPTIONAL: Defines whether to force the use of TLS.
NEXT_PRIVATE_SMTP_SECURE= NEXT_PRIVATE_SMTP_SECURE=
# OPTIONAL: if this is true and NEXT_PRIVATE_SMTP_SECURE is false then TLS is not used even if the server supports STARTTLS extension
NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS=
# REQUIRED: Defines the sender name to use for the from address. # REQUIRED: Defines the sender name to use for the from address.
NEXT_PRIVATE_SMTP_FROM_NAME="Documenso" NEXT_PRIVATE_SMTP_FROM_NAME="Documenso"
# REQUIRED: Defines the email address to use as the from address. # REQUIRED: Defines the email address to use as the from address.

View File

@@ -1,6 +1,6 @@
{ {
"name": "@documenso/marketing", "name": "@documenso/marketing",
"version": "1.6.0-rc.1", "version": "1.6.0-rc.2",
"private": true, "private": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@documenso/web", "name": "@documenso/web",
"version": "1.6.0-rc.1", "version": "1.6.0-rc.2",
"private": true, "private": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {

View File

@@ -172,6 +172,7 @@ export const EditDocumentForm = ({
teamId: team?.id, teamId: team?.id,
data: { data: {
title: data.title, title: data.title,
externalId: data.externalId || null,
globalAccessAuth: data.globalAccessAuth ?? null, globalAccessAuth: data.globalAccessAuth ?? null,
globalActionAuth: data.globalActionAuth ?? null, globalActionAuth: data.globalActionAuth ?? null,
}, },

View File

@@ -132,6 +132,7 @@ export const EditTemplateForm = ({
teamId: team?.id, teamId: team?.id,
data: { data: {
title: data.title, title: data.title,
externalId: data.externalId || null,
globalAccessAuth: data.globalAccessAuth ?? null, globalAccessAuth: data.globalAccessAuth ?? null,
globalActionAuth: data.globalActionAuth ?? null, globalActionAuth: data.globalActionAuth ?? null,
}, },

View File

@@ -271,6 +271,23 @@ export const DocumentHistorySheet = ({
]} ]}
/> />
)) ))
.with(
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED },
({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Old',
value: data.from,
},
{
key: 'New',
value: data.to,
},
]}
/>
),
)
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED }, ({ data }) => ( .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED }, ({ data }) => (
<DocumentHistorySheetChanges <DocumentHistorySheetChanges
values={[ values={[

View File

@@ -128,6 +128,7 @@ Here's a markdown table documenting all the provided environment variables:
| `NEXT_PRIVATE_SMTP_APIKEY_USER` | The API key user for the SMTP server for the `smtp-api` transport. | | `NEXT_PRIVATE_SMTP_APIKEY_USER` | The API key user for the SMTP server for the `smtp-api` transport. |
| `NEXT_PRIVATE_SMTP_APIKEY` | The API key for the SMTP server for the `smtp-api` transport. | | `NEXT_PRIVATE_SMTP_APIKEY` | The API key for the SMTP server for the `smtp-api` transport. |
| `NEXT_PRIVATE_SMTP_SECURE` | Whether to force the use of TLS for the SMTP server for SMTP transports. | | `NEXT_PRIVATE_SMTP_SECURE` | Whether to force the use of TLS for the SMTP server for SMTP transports. |
| `NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS` | If true, then no TLS will be used (even if STARTTLS is supported) |
| `NEXT_PRIVATE_SMTP_FROM_ADDRESS` | The email address for the "from" address. | | `NEXT_PRIVATE_SMTP_FROM_ADDRESS` | The email address for the "from" address. |
| `NEXT_PRIVATE_SMTP_FROM_NAME` | The sender name for the "from" address. | | `NEXT_PRIVATE_SMTP_FROM_NAME` | The sender name for the "from" address. |
| `NEXT_PRIVATE_RESEND_API_KEY` | The API key for Resend.com for the `resend` transport. | | `NEXT_PRIVATE_RESEND_API_KEY` | The API key for Resend.com for the `resend` transport. |

8
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@documenso/root", "name": "@documenso/root",
"version": "1.6.0-rc.1", "version": "1.6.0-rc.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@documenso/root", "name": "@documenso/root",
"version": "1.6.0-rc.1", "version": "1.6.0-rc.2",
"workspaces": [ "workspaces": [
"apps/*", "apps/*",
"packages/*" "packages/*"
@@ -39,7 +39,7 @@
}, },
"apps/marketing": { "apps/marketing": {
"name": "@documenso/marketing", "name": "@documenso/marketing",
"version": "1.6.0-rc.1", "version": "1.6.0-rc.2",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@documenso/assets": "*", "@documenso/assets": "*",
@@ -100,7 +100,7 @@
}, },
"apps/web": { "apps/web": {
"name": "@documenso/web", "name": "@documenso/web",
"version": "1.6.0-rc.1", "version": "1.6.0-rc.2",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@documenso/api": "*", "@documenso/api": "*",

View File

@@ -1,6 +1,6 @@
{ {
"private": true, "private": true,
"version": "1.6.0-rc.1", "version": "1.6.0-rc.2",
"scripts": { "scripts": {
"build": "turbo run build", "build": "turbo run build",
"build:web": "turbo run build --filter=@documenso/web", "build:web": "turbo run build --filter=@documenso/web",

View File

@@ -17,6 +17,7 @@ import {
ZGetDocumentsQuerySchema, ZGetDocumentsQuerySchema,
ZGetTemplatesQuerySchema, ZGetTemplatesQuerySchema,
ZNoBodyMutationSchema, ZNoBodyMutationSchema,
ZResendDocumentForSigningMutationSchema,
ZSendDocumentForSigningMutationSchema, ZSendDocumentForSigningMutationSchema,
ZSuccessfulDeleteTemplateResponseSchema, ZSuccessfulDeleteTemplateResponseSchema,
ZSuccessfulDocumentResponseSchema, ZSuccessfulDocumentResponseSchema,
@@ -25,6 +26,7 @@ import {
ZSuccessfulGetTemplateResponseSchema, ZSuccessfulGetTemplateResponseSchema,
ZSuccessfulGetTemplatesResponseSchema, ZSuccessfulGetTemplatesResponseSchema,
ZSuccessfulRecipientResponseSchema, ZSuccessfulRecipientResponseSchema,
ZSuccessfulResendDocumentResponseSchema,
ZSuccessfulResponseSchema, ZSuccessfulResponseSchema,
ZSuccessfulSigningResponseSchema, ZSuccessfulSigningResponseSchema,
ZUnsuccessfulResponseSchema, ZUnsuccessfulResponseSchema,
@@ -161,6 +163,20 @@ export const ApiContractV1 = c.router(
summary: 'Send a document for signing', summary: 'Send a document for signing',
}, },
resendDocument: {
method: 'POST',
path: '/api/v1/documents/:id/resend',
body: ZResendDocumentForSigningMutationSchema,
responses: {
200: ZSuccessfulResendDocumentResponseSchema,
400: ZUnsuccessfulResponseSchema,
401: ZUnsuccessfulResponseSchema,
404: ZUnsuccessfulResponseSchema,
500: ZUnsuccessfulResponseSchema,
},
summary: 'Re-send a document for signing',
},
deleteDocument: { deleteDocument: {
method: 'DELETE', method: 'DELETE',
path: '/api/v1/documents/:id', path: '/api/v1/documents/:id',

View File

@@ -9,6 +9,7 @@ import { createDocument } from '@documenso/lib/server-only/document/create-docum
import { deleteDocument } from '@documenso/lib/server-only/document/delete-document'; import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
import { findDocuments } from '@documenso/lib/server-only/document/find-documents'; import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
import { sendDocument } from '@documenso/lib/server-only/document/send-document'; import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { updateDocument } from '@documenso/lib/server-only/document/update-document'; import { updateDocument } from '@documenso/lib/server-only/document/update-document';
import { createField } from '@documenso/lib/server-only/field/create-field'; import { createField } from '@documenso/lib/server-only/field/create-field';
@@ -232,6 +233,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
const document = await createDocument({ const document = await createDocument({
title: body.title, title: body.title,
externalId: body.externalId || null,
userId: user.id, userId: user.id,
teamId: team?.id, teamId: team?.id,
formValues: body.formValues, formValues: body.formValues,
@@ -397,6 +399,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
teamId: team?.id, teamId: team?.id,
data: { data: {
title: fileName, title: fileName,
externalId: body.externalId || null,
formValues: body.formValues, formValues: body.formValues,
documentData: { documentData: {
connect: { connect: {
@@ -453,6 +456,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
try { try {
document = await createDocumentFromTemplate({ document = await createDocumentFromTemplate({
templateId, templateId,
externalId: body.externalId || null,
userId: user.id, userId: user.id,
teamId: team?.id, teamId: team?.id,
recipients: body.recipients, recipients: body.recipients,
@@ -600,6 +604,35 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
} }
}), }),
resendDocument: authenticatedMiddleware(async (args, user, team) => {
const { id: documentId } = args.params;
const { recipients } = args.body;
try {
await resendDocument({
userId: user.id,
documentId: Number(documentId),
recipients,
teamId: team?.id,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
return {
status: 200,
body: {
message: 'Document resend successfully initiated',
},
};
} catch (err) {
return {
status: 500,
body: {
message: 'An error has occured while resending the document',
},
};
}
}),
createRecipient: authenticatedMiddleware(async (args, user, team) => { createRecipient: authenticatedMiddleware(async (args, user, team) => {
const { id: documentId } = args.params; const { id: documentId } = args.params;
const { name, email, role } = args.body; const { name, email, role } = args.body;

View File

@@ -29,6 +29,7 @@ export type TDeleteDocumentMutationSchema = typeof ZDeleteDocumentMutationSchema
export const ZSuccessfulDocumentResponseSchema = z.object({ export const ZSuccessfulDocumentResponseSchema = z.object({
id: z.number(), id: z.number(),
externalId: z.string().nullish(),
userId: z.number(), userId: z.number(),
teamId: z.number().nullish(), teamId: z.number().nullish(),
title: z.string(), title: z.string(),
@@ -57,6 +58,20 @@ export const ZSendDocumentForSigningMutationSchema = z
export type TSendDocumentForSigningMutationSchema = typeof ZSendDocumentForSigningMutationSchema; export type TSendDocumentForSigningMutationSchema = typeof ZSendDocumentForSigningMutationSchema;
export const ZResendDocumentForSigningMutationSchema = z.object({
recipients: z.array(z.number()),
});
export type TResendDocumentForSigningMutationSchema = z.infer<
typeof ZResendDocumentForSigningMutationSchema
>;
export const ZSuccessfulResendDocumentResponseSchema = z.object({
message: z.string(),
});
export type TResendDocumentResponseSchema = z.infer<typeof ZSuccessfulResendDocumentResponseSchema>;
export const ZUploadDocumentSuccessfulSchema = z.object({ export const ZUploadDocumentSuccessfulSchema = z.object({
url: z.string(), url: z.string(),
key: z.string(), key: z.string(),
@@ -70,6 +85,7 @@ export type TUploadDocumentSuccessfulSchema = z.infer<typeof ZUploadDocumentSucc
export const ZCreateDocumentMutationSchema = z.object({ export const ZCreateDocumentMutationSchema = z.object({
title: z.string().min(1), title: z.string().min(1),
externalId: z.string().nullish(),
recipients: z.array( recipients: z.array(
z.object({ z.object({
name: z.string().min(1), name: z.string().min(1),
@@ -94,6 +110,7 @@ export type TCreateDocumentMutationSchema = z.infer<typeof ZCreateDocumentMutati
export const ZCreateDocumentMutationResponseSchema = z.object({ export const ZCreateDocumentMutationResponseSchema = z.object({
uploadUrl: z.string().min(1), uploadUrl: z.string().min(1),
documentId: z.number(), documentId: z.number(),
externalId: z.string().nullish(),
recipients: z.array( recipients: z.array(
z.object({ z.object({
recipientId: z.number(), recipientId: z.number(),
@@ -113,6 +130,7 @@ export type TCreateDocumentMutationResponseSchema = z.infer<
export const ZCreateDocumentFromTemplateMutationSchema = z.object({ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
title: z.string().min(1), title: z.string().min(1),
externalId: z.string().nullish(),
recipients: z.array( recipients: z.array(
z.object({ z.object({
name: z.string().min(1), name: z.string().min(1),
@@ -139,6 +157,7 @@ export type TCreateDocumentFromTemplateMutationSchema = z.infer<
export const ZCreateDocumentFromTemplateMutationResponseSchema = z.object({ export const ZCreateDocumentFromTemplateMutationResponseSchema = z.object({
documentId: z.number(), documentId: z.number(),
externalId: z.string().nullish(),
recipients: z.array( recipients: z.array(
z.object({ z.object({
recipientId: z.number(), recipientId: z.number(),
@@ -158,6 +177,7 @@ export type TCreateDocumentFromTemplateMutationResponseSchema = z.infer<
export const ZGenerateDocumentFromTemplateMutationSchema = z.object({ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
title: z.string().optional(), title: z.string().optional(),
externalId: z.string().nullish(),
recipients: z recipients: z
.array( .array(
z.object({ z.object({
@@ -194,6 +214,7 @@ export type TGenerateDocumentFromTemplateMutationSchema = z.infer<
export const ZGenerateDocumentFromTemplateMutationResponseSchema = z.object({ export const ZGenerateDocumentFromTemplateMutationResponseSchema = z.object({
documentId: z.number(), documentId: z.number(),
externalId: z.string().nullish(),
recipients: z.array( recipients: z.array(
z.object({ z.object({
recipientId: z.number(), recipientId: z.number(),
@@ -332,6 +353,7 @@ export const ZTemplateMetaSchema = z.object({
export const ZTemplateSchema = z.object({ export const ZTemplateSchema = z.object({
id: z.number(), id: z.number(),
externalId: z.string().nullish(),
type: z.nativeEnum(TemplateType), type: z.nativeEnum(TemplateType),
title: z.string(), title: z.string(),
userId: z.number(), userId: z.number(),

View File

@@ -46,10 +46,13 @@ const getTransport = () => {
host: process.env.NEXT_PRIVATE_SMTP_HOST ?? 'localhost:2500', host: process.env.NEXT_PRIVATE_SMTP_HOST ?? 'localhost:2500',
port: Number(process.env.NEXT_PRIVATE_SMTP_PORT) || 587, port: Number(process.env.NEXT_PRIVATE_SMTP_PORT) || 587,
secure: process.env.NEXT_PRIVATE_SMTP_SECURE === 'true', secure: process.env.NEXT_PRIVATE_SMTP_SECURE === 'true',
auth: { ignoreTLS: process.env.NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS === 'true',
user: process.env.NEXT_PRIVATE_SMTP_USERNAME ?? '', auth: process.env.NEXT_PRIVATE_SMTP_USERNAME
? {
user: process.env.NEXT_PRIVATE_SMTP_USERNAME,
pass: process.env.NEXT_PRIVATE_SMTP_PASSWORD ?? '', pass: process.env.NEXT_PRIVATE_SMTP_PASSWORD ?? '',
}, }
: undefined,
}); });
}; };

View File

@@ -38,7 +38,7 @@ export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailPro
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const confirmationLink = `${assetBaseUrl}/verify-email/${verificationToken.token}`; const confirmationLink = `${assetBaseUrl}/verify-email/${verificationToken.token}`;
const senderName = NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso'; const senderName = NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso';
const senderAdress = NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com'; const senderAddress = NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com';
const confirmationTemplate = createElement(ConfirmEmailTemplate, { const confirmationTemplate = createElement(ConfirmEmailTemplate, {
assetBaseUrl, assetBaseUrl,
@@ -52,7 +52,7 @@ export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailPro
}, },
from: { from: {
name: senderName, name: senderName,
address: senderAdress, address: senderAddress,
}, },
subject: 'Please confirm your email', subject: 'Please confirm your email',
html: render(confirmationTemplate), html: render(confirmationTemplate),

View File

@@ -11,6 +11,7 @@ import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
export type CreateDocumentOptions = { export type CreateDocumentOptions = {
title: string; title: string;
externalId?: string | null;
userId: number; userId: number;
teamId?: number; teamId?: number;
documentDataId: string; documentDataId: string;
@@ -21,6 +22,7 @@ export type CreateDocumentOptions = {
export const createDocument = async ({ export const createDocument = async ({
userId, userId,
title, title,
externalId,
documentDataId, documentDataId,
teamId, teamId,
formValues, formValues,
@@ -50,6 +52,7 @@ export const createDocument = async ({
const document = await tx.document.create({ const document = await tx.document.create({
data: { data: {
title, title,
externalId,
documentDataId, documentDataId,
userId, userId,
teamId, teamId,

View File

@@ -18,6 +18,7 @@ export type UpdateDocumentSettingsOptions = {
documentId: number; documentId: number;
data: { data: {
title?: string; title?: string;
externalId?: string | null;
globalAccessAuth?: TDocumentAccessAuthTypes | null; globalAccessAuth?: TDocumentAccessAuthTypes | null;
globalActionAuth?: TDocumentActionAuthTypes | null; globalActionAuth?: TDocumentActionAuthTypes | null;
}; };
@@ -91,6 +92,7 @@ export const updateDocumentSettings = async ({
} }
const isTitleSame = data.title === document.title; const isTitleSame = data.title === document.title;
const isExternalIdSame = data.externalId === document.externalId;
const isGlobalAccessSame = documentGlobalAccessAuth === newGlobalAccessAuth; const isGlobalAccessSame = documentGlobalAccessAuth === newGlobalAccessAuth;
const isGlobalActionSame = documentGlobalActionAuth === newGlobalActionAuth; const isGlobalActionSame = documentGlobalActionAuth === newGlobalActionAuth;
@@ -118,6 +120,21 @@ export const updateDocumentSettings = async ({
); );
} }
if (!isExternalIdSame) {
auditLogs.push(
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED,
documentId,
user,
requestMetadata,
data: {
from: document.externalId,
to: data.externalId || '',
},
}),
);
}
if (!isGlobalAccessSame) { if (!isGlobalAccessSame) {
auditLogs.push( auditLogs.push(
createDocumentAuditLogData({ createDocumentAuditLogData({
@@ -165,6 +182,7 @@ export const updateDocumentSettings = async ({
}, },
data: { data: {
title: data.title, title: data.title,
externalId: data.externalId || null,
authOptions, authOptions,
}, },
}); });

View File

@@ -33,6 +33,7 @@ export type CreateDocumentFromTemplateResponse = Awaited<
export type CreateDocumentFromTemplateOptions = { export type CreateDocumentFromTemplateOptions = {
templateId: number; templateId: number;
externalId?: string | null;
userId: number; userId: number;
teamId?: number; teamId?: number;
recipients: { recipients: {
@@ -58,6 +59,7 @@ export type CreateDocumentFromTemplateOptions = {
export const createDocumentFromTemplate = async ({ export const createDocumentFromTemplate = async ({
templateId, templateId,
externalId,
userId, userId,
teamId, teamId,
recipients, recipients,
@@ -147,6 +149,7 @@ export const createDocumentFromTemplate = async ({
const document = await tx.document.create({ const document = await tx.document.create({
data: { data: {
source: DocumentSource.TEMPLATE, source: DocumentSource.TEMPLATE,
externalId,
templateId: template.id, templateId: template.id,
userId, userId,
teamId: template.teamId, teamId: template.teamId,

View File

@@ -15,6 +15,7 @@ export type UpdateTemplateSettingsOptions = {
templateId: number; templateId: number;
data: { data: {
title?: string; title?: string;
externalId?: string | null;
globalAccessAuth?: TDocumentAccessAuthTypes | null; globalAccessAuth?: TDocumentAccessAuthTypes | null;
globalActionAuth?: TDocumentActionAuthTypes | null; globalActionAuth?: TDocumentActionAuthTypes | null;
publicTitle?: string; publicTitle?: string;
@@ -99,6 +100,7 @@ export const updateTemplateSettings = async ({
}, },
data: { data: {
title: data.title, title: data.title,
externalId: data.externalId || null,
type: data.type, type: data.type,
publicDescription: data.publicDescription, publicDescription: data.publicDescription,
publicTitle: data.publicTitle, publicTitle: data.publicTitle,

View File

@@ -35,6 +35,7 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
'DOCUMENT_RECIPIENT_COMPLETED', // When a recipient completes all their required tasks for the document. 'DOCUMENT_RECIPIENT_COMPLETED', // When a recipient completes all their required tasks for the document.
'DOCUMENT_SENT', // When the document transitions from DRAFT to PENDING. 'DOCUMENT_SENT', // When the document transitions from DRAFT to PENDING.
'DOCUMENT_TITLE_UPDATED', // When the document title is updated. 'DOCUMENT_TITLE_UPDATED', // When the document title is updated.
'DOCUMENT_EXTERNAL_ID_UPDATED', // When the document external ID is updated.
'DOCUMENT_MOVED_TO_TEAM', // When the document is moved to a team. 'DOCUMENT_MOVED_TO_TEAM', // When the document is moved to a team.
]); ]);
@@ -355,6 +356,17 @@ export const ZDocumentAuditLogEventDocumentTitleUpdatedSchema = z.object({
}), }),
}); });
/**
* Event: Document external ID updated.
*/
export const ZDocumentAuditLogEventDocumentExternalIdUpdatedSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED),
data: z.object({
from: z.string().nullish(),
to: z.string().nullish(),
}),
});
/** /**
* Event: Field created. * Event: Field created.
*/ */
@@ -450,6 +462,7 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
ZDocumentAuditLogEventDocumentRecipientCompleteSchema, ZDocumentAuditLogEventDocumentRecipientCompleteSchema,
ZDocumentAuditLogEventDocumentSentSchema, ZDocumentAuditLogEventDocumentSentSchema,
ZDocumentAuditLogEventDocumentTitleUpdatedSchema, ZDocumentAuditLogEventDocumentTitleUpdatedSchema,
ZDocumentAuditLogEventDocumentExternalIdUpdatedSchema,
ZDocumentAuditLogEventFieldCreatedSchema, ZDocumentAuditLogEventFieldCreatedSchema,
ZDocumentAuditLogEventFieldRemovedSchema, ZDocumentAuditLogEventFieldRemovedSchema,
ZDocumentAuditLogEventFieldUpdatedSchema, ZDocumentAuditLogEventFieldUpdatedSchema,

View File

@@ -332,6 +332,10 @@ export const formatDocumentAuditLogAction = (auditLog: TDocumentAuditLog, userId
anonymous: 'Document title updated', anonymous: 'Document title updated',
identified: 'updated the document title', identified: 'updated the document title',
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED }, () => ({
anonymous: 'Document external ID updated',
identified: 'updated the document external ID',
}))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT }, () => ({
anonymous: 'Document sent', anonymous: 'Document sent',
identified: 'sent the document', identified: 'sent the document',

View File

@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Document" ADD COLUMN "externalId" TEXT;
-- AlterTable
ALTER TABLE "Template" ADD COLUMN "externalId" TEXT;

View File

@@ -283,6 +283,7 @@ enum DocumentSource {
model Document { model Document {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
externalId String?
userId Int userId Int
User User @relation(fields: [userId], references: [id], onDelete: Cascade) User User @relation(fields: [userId], references: [id], onDelete: Cascade)
authOptions Json? authOptions Json?
@@ -589,6 +590,7 @@ model TemplateMeta {
model Template { model Template {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
externalId String?
type TemplateType @default(PRIVATE) type TemplateType @default(PRIVATE)
title String title String
userId Int userId Int

View File

@@ -55,6 +55,7 @@ export const ZSetSettingsForDocumentMutationSchema = z.object({
teamId: z.number().min(1).optional(), teamId: z.number().min(1).optional(),
data: z.object({ data: z.object({
title: z.string().min(1).optional(), title: z.string().min(1).optional(),
externalId: z.string().nullish(),
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(), globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(), globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(),
}), }),

View File

@@ -74,6 +74,7 @@ export const ZUpdateTemplateSettingsMutationSchema = z.object({
teamId: z.number().min(1).optional(), teamId: z.number().min(1).optional(),
data: z.object({ data: z.object({
title: z.string().min(1).optional(), title: z.string().min(1).optional(),
externalId: z.string().nullish(),
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(), globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(), globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(),
publicTitle: z.string().trim().min(1).max(MAX_TEMPLATE_PUBLIC_TITLE_LENGTH).optional(), publicTitle: z.string().trim().min(1).max(MAX_TEMPLATE_PUBLIC_TITLE_LENGTH).optional(),

View File

@@ -59,6 +59,7 @@ declare namespace NodeJS {
NEXT_PRIVATE_SMTP_APIKEY?: string; NEXT_PRIVATE_SMTP_APIKEY?: string;
NEXT_PRIVATE_SMTP_SECURE?: string; NEXT_PRIVATE_SMTP_SECURE?: string;
NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS?: string;
NEXT_PRIVATE_SMTP_FROM_NAME?: string; NEXT_PRIVATE_SMTP_FROM_NAME?: string;
NEXT_PRIVATE_SMTP_FROM_ADDRESS?: string; NEXT_PRIVATE_SMTP_FROM_ADDRESS?: string;

View File

@@ -78,6 +78,7 @@ export const AddSettingsFormPartial = ({
resolver: zodResolver(ZAddSettingsFormSchema), resolver: zodResolver(ZAddSettingsFormSchema),
defaultValues: { defaultValues: {
title: document.title, title: document.title,
externalId: document.externalId || '',
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined, globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
globalActionAuth: documentAuthOption?.globalActionAuth || undefined, globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
meta: { meta: {
@@ -183,6 +184,34 @@ export const AddSettingsFormPartial = ({
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-2 text-sm leading-relaxed"> <AccordionContent className="text-muted-foreground -mx-1 px-1 pt-2 text-sm leading-relaxed">
<div className="flex flex-col space-y-6 "> <div className="flex flex-col space-y-6 ">
<FormField
control={form.control}
name="externalId"
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
External ID{' '}
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
Add an external ID to the document. This can be used to identify the
document in external systems.
</TooltipContent>
</Tooltip>
</FormLabel>
<FormControl>
<Input className="bg-background" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="meta.dateFormat" name="meta.dateFormat"

View File

@@ -21,6 +21,7 @@ export const ZMapNegativeOneToUndefinedSchema = z
export const ZAddSettingsFormSchema = z.object({ export const ZAddSettingsFormSchema = z.object({
title: z.string().trim().min(1, { message: "Title can't be empty" }), title: z.string().trim().min(1, { message: "Title can't be empty" }),
externalId: z.string().optional(),
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe( globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
ZDocumentAccessAuthTypesSchema.optional(), ZDocumentAccessAuthTypesSchema.optional(),
), ),

View File

@@ -79,6 +79,7 @@ export const AddTemplateSettingsFormPartial = ({
resolver: zodResolver(ZAddTemplateSettingsFormSchema), resolver: zodResolver(ZAddTemplateSettingsFormSchema),
defaultValues: { defaultValues: {
title: template.title, title: template.title,
externalId: template.externalId || undefined,
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined, globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
globalActionAuth: documentAuthOption?.globalActionAuth || undefined, globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
meta: { meta: {
@@ -223,6 +224,34 @@ export const AddTemplateSettingsFormPartial = ({
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-4 text-sm leading-relaxed"> <AccordionContent className="text-muted-foreground -mx-1 px-1 pt-4 text-sm leading-relaxed">
<div className="flex flex-col space-y-6"> <div className="flex flex-col space-y-6">
<FormField
control={form.control}
name="externalId"
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
External ID{' '}
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
Add an external ID to the template. This can be used to identify in
external systems.
</TooltipContent>
</Tooltip>
</FormLabel>
<FormControl>
<Input className="bg-background" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="meta.dateFormat" name="meta.dateFormat"

View File

@@ -12,6 +12,7 @@ import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.
export const ZAddTemplateSettingsFormSchema = z.object({ export const ZAddTemplateSettingsFormSchema = z.object({
title: z.string().trim().min(1, { message: "Title can't be empty" }), title: z.string().trim().min(1, { message: "Title can't be empty" }),
externalId: z.string().optional(),
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe( globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
ZDocumentAccessAuthTypesSchema.optional(), ZDocumentAccessAuthTypesSchema.optional(),
), ),

View File

@@ -12,9 +12,12 @@
] ]
}, },
"prebuild": { "prebuild": {
"cache": false,
"dependsOn": [ "dependsOn": [
"^prebuild" "^prebuild"
],
"outputs": [
".next/**",
"!.next/cache/**"
] ]
}, },
"lint": { "lint": {
@@ -109,6 +112,7 @@
"NEXT_PRIVATE_SMTP_APIKEY_USER", "NEXT_PRIVATE_SMTP_APIKEY_USER",
"NEXT_PRIVATE_SMTP_APIKEY", "NEXT_PRIVATE_SMTP_APIKEY",
"NEXT_PRIVATE_SMTP_SECURE", "NEXT_PRIVATE_SMTP_SECURE",
"NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS",
"NEXT_PRIVATE_SMTP_FROM_NAME", "NEXT_PRIVATE_SMTP_FROM_NAME",
"NEXT_PRIVATE_SMTP_FROM_ADDRESS", "NEXT_PRIVATE_SMTP_FROM_ADDRESS",
"NEXT_PRIVATE_STRIPE_API_KEY", "NEXT_PRIVATE_STRIPE_API_KEY",