mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 01:15:49 +10:00
fix: merge conflicts
This commit is contained in:
@@ -1178,13 +1178,12 @@ test.describe('Unauthorized Access - Document API V2', () => {
|
||||
const { user: firstRecipientUser } = await seedUser();
|
||||
const { user: secondRecipientUser } = await seedUser();
|
||||
|
||||
await prisma.template.update({
|
||||
const updatedTemplate = await prisma.template.update({
|
||||
where: { id: template.id },
|
||||
data: {
|
||||
recipients: {
|
||||
create: [
|
||||
{
|
||||
id: firstRecipientUser.id,
|
||||
name: firstRecipientUser.name || '',
|
||||
email: firstRecipientUser.email,
|
||||
token: nanoid(12),
|
||||
@@ -1193,7 +1192,6 @@ test.describe('Unauthorized Access - Document API V2', () => {
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
},
|
||||
{
|
||||
id: secondRecipientUser.id,
|
||||
name: secondRecipientUser.name || '',
|
||||
email: secondRecipientUser.email,
|
||||
token: nanoid(12),
|
||||
@@ -1204,21 +1202,35 @@ test.describe('Unauthorized Access - Document API V2', () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
const recipientAId = updatedTemplate.recipients.find(
|
||||
(recipient) => recipient.email === firstRecipientUser.email,
|
||||
)?.id;
|
||||
const recipientBId = updatedTemplate.recipients.find(
|
||||
(recipient) => recipient.email === secondRecipientUser.email,
|
||||
)?.id;
|
||||
|
||||
if (!recipientAId || !recipientBId) {
|
||||
throw new Error('Recipient IDs not found');
|
||||
}
|
||||
|
||||
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, {
|
||||
headers: { Authorization: `Bearer ${tokenB}` },
|
||||
data: {
|
||||
templateId: template.id,
|
||||
recipients: [
|
||||
{
|
||||
id: firstRecipientUser.id,
|
||||
id: recipientAId,
|
||||
name: firstRecipientUser.name,
|
||||
email: firstRecipientUser.email,
|
||||
role: RecipientRole.SIGNER,
|
||||
},
|
||||
{
|
||||
id: secondRecipientUser.id,
|
||||
id: recipientBId,
|
||||
name: secondRecipientUser.name,
|
||||
email: secondRecipientUser.email,
|
||||
role: RecipientRole.SIGNER,
|
||||
|
||||
@@ -379,10 +379,11 @@ test('[TEAMS]: can create a template inside a template folder', async ({ page })
|
||||
.filter({ hasText: /^Upload Template DocumentDrag & drop your PDF here\.$/ })
|
||||
.nth(2)
|
||||
.click();
|
||||
await page.locator('input[type="file"]').waitFor({ state: 'attached' });
|
||||
await page.locator('input[type="file"]').nth(0).waitFor({ state: 'attached' });
|
||||
|
||||
await page
|
||||
.locator('input[type="file"]')
|
||||
.nth(0)
|
||||
.setInputFiles(path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf'));
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
@@ -268,7 +268,7 @@ test('[TEMPLATE]: should create a document from a template with custom document'
|
||||
// Upload document.
|
||||
const [fileChooser] = await Promise.all([
|
||||
page.waitForEvent('filechooser'),
|
||||
page.locator('input[type=file]').evaluate((e) => {
|
||||
page.getByTestId('template-use-dialog-file-input').evaluate((e) => {
|
||||
if (e instanceof HTMLInputElement) {
|
||||
e.click();
|
||||
}
|
||||
@@ -361,7 +361,7 @@ test('[TEMPLATE]: should create a team document from a template with custom docu
|
||||
// Upload document.
|
||||
const [fileChooser] = await Promise.all([
|
||||
page.waitForEvent('filechooser'),
|
||||
page.locator('input[type=file]').evaluate((e) => {
|
||||
page.getByTestId('template-use-dialog-file-input').evaluate((e) => {
|
||||
if (e instanceof HTMLInputElement) {
|
||||
e.click();
|
||||
}
|
||||
|
||||
@@ -13,4 +13,4 @@ export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED
|
||||
|
||||
export const API_V2_BETA_URL = '/api/v2-beta';
|
||||
|
||||
export const SUPPORT_EMAIL = 'support@documenso.com';
|
||||
export const SUPPORT_EMAIL = env('NEXT_PUBLIC_SUPPORT_EMAIL') ?? 'support@documenso.com';
|
||||
|
||||
@@ -49,15 +49,24 @@ type DocumentSignatureTypeData = {
|
||||
|
||||
export const DOCUMENT_SIGNATURE_TYPES = {
|
||||
[DocumentSignatureType.DRAW]: {
|
||||
label: msg`Draw`,
|
||||
label: msg({
|
||||
message: `Draw`,
|
||||
context: `Draw signatute type`,
|
||||
}),
|
||||
value: DocumentSignatureType.DRAW,
|
||||
},
|
||||
[DocumentSignatureType.TYPE]: {
|
||||
label: msg`Type`,
|
||||
label: msg({
|
||||
message: `Type`,
|
||||
context: `Type signatute type`,
|
||||
}),
|
||||
value: DocumentSignatureType.TYPE,
|
||||
},
|
||||
[DocumentSignatureType.UPLOAD]: {
|
||||
label: msg`Upload`,
|
||||
label: msg({
|
||||
message: `Upload`,
|
||||
context: `Upload signatute type`,
|
||||
}),
|
||||
value: DocumentSignatureType.UPLOAD,
|
||||
},
|
||||
} satisfies Record<DocumentSignatureType, DocumentSignatureTypeData>;
|
||||
|
||||
@@ -4,39 +4,114 @@ import { RecipientRole } from '@prisma/client';
|
||||
|
||||
export const RECIPIENT_ROLES_DESCRIPTION = {
|
||||
[RecipientRole.APPROVER]: {
|
||||
actionVerb: msg`Approve`,
|
||||
actioned: msg`Approved`,
|
||||
progressiveVerb: msg`Approving`,
|
||||
roleName: msg`Approver`,
|
||||
roleNamePlural: msg`Approvers`,
|
||||
actionVerb: msg({
|
||||
message: `Approve`,
|
||||
context: `Recipient role action verb`,
|
||||
}),
|
||||
actioned: msg({
|
||||
message: `Approved`,
|
||||
context: `Recipient role actioned`,
|
||||
}),
|
||||
progressiveVerb: msg({
|
||||
message: `Approving`,
|
||||
context: `Recipient role progressive verb`,
|
||||
}),
|
||||
roleName: msg({
|
||||
message: `Approver`,
|
||||
context: `Recipient role name`,
|
||||
}),
|
||||
roleNamePlural: msg({
|
||||
message: `Approvers`,
|
||||
context: `Recipient role plural name`,
|
||||
}),
|
||||
},
|
||||
[RecipientRole.CC]: {
|
||||
actionVerb: msg`CC`,
|
||||
actioned: msg`CC'd`,
|
||||
progressiveVerb: msg`CC`,
|
||||
roleName: msg`Cc`,
|
||||
roleNamePlural: msg`Ccers`,
|
||||
actionVerb: msg({
|
||||
message: `CC`,
|
||||
context: `Recipient role action verb`,
|
||||
}),
|
||||
actioned: msg({
|
||||
message: `CC'd`,
|
||||
context: `Recipient role actioned`,
|
||||
}),
|
||||
progressiveVerb: msg({
|
||||
message: `CC`,
|
||||
context: `Recipient role progressive verb`,
|
||||
}),
|
||||
roleName: msg({
|
||||
message: `Cc`,
|
||||
context: `Recipient role name`,
|
||||
}),
|
||||
roleNamePlural: msg({
|
||||
message: `Ccers`,
|
||||
context: `Recipient role plural name`,
|
||||
}),
|
||||
},
|
||||
[RecipientRole.SIGNER]: {
|
||||
actionVerb: msg`Sign`,
|
||||
actioned: msg`Signed`,
|
||||
progressiveVerb: msg`Signing`,
|
||||
roleName: msg`Signer`,
|
||||
roleNamePlural: msg`Signers`,
|
||||
actionVerb: msg({
|
||||
message: `Sign`,
|
||||
context: `Recipient role action verb`,
|
||||
}),
|
||||
actioned: msg({
|
||||
message: `Signed`,
|
||||
context: `Recipient role actioned`,
|
||||
}),
|
||||
progressiveVerb: msg({
|
||||
message: `Signing`,
|
||||
context: `Recipient role progressive verb`,
|
||||
}),
|
||||
roleName: msg({
|
||||
message: `Signer`,
|
||||
context: `Recipient role name`,
|
||||
}),
|
||||
roleNamePlural: msg({
|
||||
message: `Signers`,
|
||||
context: `Recipient role plural name`,
|
||||
}),
|
||||
},
|
||||
[RecipientRole.VIEWER]: {
|
||||
actionVerb: msg`View`,
|
||||
actioned: msg`Viewed`,
|
||||
progressiveVerb: msg`Viewing`,
|
||||
roleName: msg`Viewer`,
|
||||
roleNamePlural: msg`Viewers`,
|
||||
actionVerb: msg({
|
||||
message: `View`,
|
||||
context: `Recipient role action verb`,
|
||||
}),
|
||||
actioned: msg({
|
||||
message: `Viewed`,
|
||||
context: `Recipient role actioned`,
|
||||
}),
|
||||
progressiveVerb: msg({
|
||||
message: `Viewing`,
|
||||
context: `Recipient role progressive verb`,
|
||||
}),
|
||||
roleName: msg({
|
||||
message: `Viewer`,
|
||||
context: `Recipient role name`,
|
||||
}),
|
||||
roleNamePlural: msg({
|
||||
message: `Viewers`,
|
||||
context: `Recipient role plural name`,
|
||||
}),
|
||||
},
|
||||
[RecipientRole.ASSISTANT]: {
|
||||
actionVerb: msg`Assist`,
|
||||
actioned: msg`Assisted`,
|
||||
progressiveVerb: msg`Assisting`,
|
||||
roleName: msg`Assistant`,
|
||||
roleNamePlural: msg`Assistants`,
|
||||
actionVerb: msg({
|
||||
message: `Assist`,
|
||||
context: `Recipient role action verb`,
|
||||
}),
|
||||
actioned: msg({
|
||||
message: `Assisted`,
|
||||
context: `Recipient role actioned`,
|
||||
}),
|
||||
progressiveVerb: msg({
|
||||
message: `Assisting`,
|
||||
context: `Recipient role progressive verb`,
|
||||
}),
|
||||
roleName: msg({
|
||||
message: `Assistant`,
|
||||
context: `Recipient role name`,
|
||||
}),
|
||||
roleNamePlural: msg({
|
||||
message: `Assistants`,
|
||||
context: `Recipient role plural name`,
|
||||
}),
|
||||
},
|
||||
} satisfies Record<keyof typeof RecipientRole, unknown>;
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@ import { msg } from '@lingui/core/macro';
|
||||
export const TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX = /recipient\.\d+@documenso\.com/i;
|
||||
export const TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX = /Recipient \d+/i;
|
||||
|
||||
export const isTemplateRecipientEmailPlaceholder = (email: string) => {
|
||||
return TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX.test(email);
|
||||
};
|
||||
|
||||
export const DIRECT_TEMPLATE_DOCUMENTATION = [
|
||||
{
|
||||
title: msg`Enable Direct Link Signing`,
|
||||
|
||||
@@ -48,7 +48,7 @@ export const run = async ({
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
meta: document.documentMeta,
|
||||
});
|
||||
|
||||
const { documentMeta, user: documentOwner } = document;
|
||||
|
||||
@@ -76,7 +76,7 @@ export const run = async ({
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
meta: document.documentMeta,
|
||||
});
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
@@ -68,7 +68,7 @@ export const run = async ({
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
meta: document.documentMeta,
|
||||
});
|
||||
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
@@ -86,7 +86,7 @@ export const run = async ({
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
meta: document.documentMeta,
|
||||
});
|
||||
|
||||
const customEmail = document?.documentMeta;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { signPdf } from '@documenso/signing';
|
||||
import { AppError, AppErrorCode } from '../../../errors/app-error';
|
||||
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
||||
import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client';
|
||||
import { getAuditLogsPdf } from '../../../server-only/htmltopdf/get-audit-logs-pdf';
|
||||
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
|
||||
import { addRejectionStampToPdf } from '../../../server-only/pdf/add-rejection-stamp-to-pdf';
|
||||
import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
|
||||
@@ -145,7 +146,24 @@ export const run = async ({
|
||||
? await getCertificatePdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch(() => null)
|
||||
}).catch((e) => {
|
||||
console.log('Failed to get certificate PDF');
|
||||
console.error(e);
|
||||
|
||||
return null;
|
||||
})
|
||||
: null;
|
||||
|
||||
const auditLogData = settings.includeAuditLog
|
||||
? await getAuditLogsPdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch((e) => {
|
||||
console.log('Failed to get audit logs PDF');
|
||||
console.error(e);
|
||||
|
||||
return null;
|
||||
})
|
||||
: null;
|
||||
|
||||
const newDataId = await io.runTask('decorate-and-sign-pdf', async () => {
|
||||
@@ -174,6 +192,16 @@ export const run = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (auditLogData) {
|
||||
const auditLogDoc = await PDFDocument.load(auditLogData);
|
||||
|
||||
const auditLogPages = await pdfDoc.copyPages(auditLogDoc, auditLogDoc.getPageIndices());
|
||||
|
||||
auditLogPages.forEach((page) => {
|
||||
pdfDoc.addPage(page);
|
||||
});
|
||||
}
|
||||
|
||||
for (const field of fields) {
|
||||
if (field.inserted) {
|
||||
document.useLegacyFieldInsertion
|
||||
|
||||
@@ -212,7 +212,7 @@ export const createDocumentV2 = async ({
|
||||
}),
|
||||
);
|
||||
|
||||
// Todo: Is it necessary to create a full audit log with all fields and recipients audit logs?
|
||||
// Todo: Is it necessary to create a full audit logs with all fields and recipients audit logs?
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
|
||||
@@ -156,7 +156,7 @@ const handleDocumentOwnerDelete = async ({
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
meta: document.documentMeta,
|
||||
});
|
||||
|
||||
// Soft delete completed documents.
|
||||
|
||||
@@ -102,7 +102,7 @@ export const resendDocument = async ({
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
meta: document.documentMeta,
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers';
|
||||
import { getAuditLogsPdf } from '../htmltopdf/get-audit-logs-pdf';
|
||||
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
|
||||
import { addRejectionStampToPdf } from '../pdf/add-rejection-stamp-to-pdf';
|
||||
import { flattenAnnotations } from '../pdf/flatten-annotations';
|
||||
@@ -125,6 +126,18 @@ export const sealDocument = async ({
|
||||
})
|
||||
: null;
|
||||
|
||||
const auditLogData = settings.includeAuditLog
|
||||
? await getAuditLogsPdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch((e) => {
|
||||
console.log('Failed to get audit logs PDF');
|
||||
console.error(e);
|
||||
|
||||
return null;
|
||||
})
|
||||
: null;
|
||||
|
||||
const doc = await PDFDocument.load(pdfData);
|
||||
|
||||
// Normalize and flatten layers that could cause issues with the signature
|
||||
@@ -147,6 +160,16 @@ export const sealDocument = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (auditLogData) {
|
||||
const auditLog = await PDFDocument.load(auditLogData);
|
||||
|
||||
const auditLogPages = await doc.copyPages(auditLog, auditLog.getPageIndices());
|
||||
|
||||
auditLogPages.forEach((page) => {
|
||||
doc.addPage(page);
|
||||
});
|
||||
}
|
||||
|
||||
for (const field of fields) {
|
||||
document.useLegacyFieldInsertion
|
||||
? await legacy_insertFieldInPDF(doc, field)
|
||||
|
||||
@@ -59,7 +59,7 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
meta: document.documentMeta,
|
||||
});
|
||||
|
||||
const { user: owner } = document;
|
||||
|
||||
@@ -49,7 +49,7 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
meta: document.documentMeta,
|
||||
});
|
||||
|
||||
const { email, name } = document.user;
|
||||
|
||||
@@ -51,7 +51,7 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
meta: document.documentMeta,
|
||||
});
|
||||
|
||||
const isDocumentPendingEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
|
||||
@@ -46,7 +46,7 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
meta: document.documentMeta,
|
||||
});
|
||||
|
||||
const { status, user } = document;
|
||||
|
||||
@@ -59,7 +59,7 @@ type RecipientGetEmailContextOptions = BaseGetEmailContextOptions & {
|
||||
* Force meta options as a typesafe way to ensure developers don't forget to
|
||||
* pass it in if it is available.
|
||||
*/
|
||||
meta: EmailMetaOption | null;
|
||||
meta: EmailMetaOption | null | undefined;
|
||||
};
|
||||
|
||||
type GetEmailContextOptions = InternalGetEmailContextOptions | RecipientGetEmailContextOptions;
|
||||
@@ -104,7 +104,7 @@ export const getEmailContext = async (
|
||||
}
|
||||
|
||||
const replyToEmail = meta?.emailReplyTo || emailContext.settings.emailReplyTo || undefined;
|
||||
const senderEmailId = meta?.emailId || emailContext.settings.emailId;
|
||||
const senderEmailId = meta?.emailId === null ? null : emailContext.settings.emailId;
|
||||
|
||||
const foundSenderEmail = emailContext.allowedEmails.find((email) => email.id === senderEmailId);
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import type { Browser } from 'playwright';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { type SupportedLanguageCodes, isValidLanguageCode } from '../../constants/i18n';
|
||||
import { env } from '../../utils/env';
|
||||
import { encryptSecondaryData } from '../crypto/encrypt';
|
||||
|
||||
export type GetAuditLogsPdfOptions = {
|
||||
documentId: number;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
language?: SupportedLanguageCodes | (string & {});
|
||||
};
|
||||
|
||||
export const getAuditLogsPdf = async ({ documentId, language }: GetAuditLogsPdfOptions) => {
|
||||
const { chromium } = await import('playwright');
|
||||
|
||||
const encryptedId = encryptSecondaryData({
|
||||
data: documentId.toString(),
|
||||
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
|
||||
});
|
||||
|
||||
let browser: Browser;
|
||||
|
||||
const browserlessUrl = env('NEXT_PRIVATE_BROWSERLESS_URL');
|
||||
|
||||
if (browserlessUrl) {
|
||||
// !: Use CDP rather than the default `connect` method to avoid coupling to the playwright version.
|
||||
// !: Previously we would have to keep the playwright version in sync with the browserless version to avoid errors.
|
||||
browser = await chromium.connectOverCDP(browserlessUrl);
|
||||
} else {
|
||||
browser = await chromium.launch();
|
||||
}
|
||||
|
||||
if (!browser) {
|
||||
throw new Error(
|
||||
'Failed to establish a browser, please ensure you have either a Browserless.io url or chromium browser installed',
|
||||
);
|
||||
}
|
||||
|
||||
const browserContext = await browser.newContext();
|
||||
|
||||
const page = await browserContext.newPage();
|
||||
|
||||
const lang = isValidLanguageCode(language) ? language : 'en';
|
||||
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: 'language',
|
||||
value: lang,
|
||||
url: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
},
|
||||
]);
|
||||
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/audit-log?d=${encryptedId}`, {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// !: This is a workaround to ensure the page is loaded correctly.
|
||||
// !: It's not clear why but suddenly browserless cdp connections would
|
||||
// !: cause the page to render blank until a reload is performed.
|
||||
await page.reload({
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
await page.waitForSelector('h1', {
|
||||
state: 'visible',
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
const result = await page.pdf({
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
});
|
||||
|
||||
await browserContext.close();
|
||||
|
||||
void browser.close();
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -46,7 +46,7 @@ export const getCertificatePdf = async ({ documentId, language }: GetCertificate
|
||||
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: 'language',
|
||||
name: 'lang',
|
||||
value: lang,
|
||||
url: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
},
|
||||
@@ -57,8 +57,22 @@ export const getCertificatePdf = async ({ documentId, language }: GetCertificate
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// !: This is a workaround to ensure the page is loaded correctly.
|
||||
// !: It's not clear why but suddenly browserless cdp connections would
|
||||
// !: cause the page to render blank until a reload is performed.
|
||||
await page.reload({
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
await page.waitForSelector('h1', {
|
||||
state: 'visible',
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
const result = await page.pdf({
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
});
|
||||
|
||||
await browserContext.close();
|
||||
|
||||
@@ -2,8 +2,10 @@ import type { Prisma } from '@prisma/client';
|
||||
import { OrganisationType } from '@prisma/client';
|
||||
import { OrganisationMemberRole } from '@prisma/client';
|
||||
|
||||
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { IS_BILLING_ENABLED } from '../../constants/app';
|
||||
import { ORGANISATION_INTERNAL_GROUPS } from '../../constants/organisations';
|
||||
import { AppErrorCode } from '../../errors/app-error';
|
||||
import { AppError } from '../../errors/app-error';
|
||||
@@ -30,6 +32,33 @@ export const createOrganisation = async ({
|
||||
customerId,
|
||||
claim,
|
||||
}: CreateOrganisationOptions) => {
|
||||
let customerIdToUse = customerId;
|
||||
|
||||
if (!customerId && IS_BILLING_ENABLED()) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
customerIdToUse = await createCustomer({
|
||||
name: user.name || user.email,
|
||||
email: user.email,
|
||||
})
|
||||
.then((customer) => customer.id)
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const organisationSetting = await tx.organisationGlobalSettings.create({
|
||||
data: {
|
||||
@@ -64,7 +93,7 @@ export const createOrganisation = async ({
|
||||
id: generateDatabaseId('org_group'),
|
||||
})),
|
||||
},
|
||||
customerId,
|
||||
customerId: customerIdToUse,
|
||||
},
|
||||
include: {
|
||||
groups: true,
|
||||
|
||||
@@ -130,7 +130,7 @@ export const deleteDocumentRecipient = async ({
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
meta: document.documentMeta,
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
|
||||
@@ -95,7 +95,7 @@ export const setDocumentRecipients = async ({
|
||||
type: 'team',
|
||||
teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
meta: document.documentMeta,
|
||||
});
|
||||
|
||||
const recipientsHaveActionAuth = recipients.some(
|
||||
|
||||
+681
-114
File diff suppressed because it is too large
Load Diff
@@ -167,6 +167,10 @@ msgstr "{0} Recipient(s)"
|
||||
msgid "{0} Teams"
|
||||
msgstr "{0} Teams"
|
||||
|
||||
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
|
||||
msgid "{browserInfo} on {os}"
|
||||
msgstr "{browserInfo} on {os}"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx
|
||||
msgid "{charactersRemaining, plural, one {1 character remaining} other {{charactersRemaining} characters remaining}}"
|
||||
msgstr "{charactersRemaining, plural, one {1 character remaining} other {{charactersRemaining} characters remaining}}"
|
||||
@@ -368,6 +372,10 @@ msgstr "{teamName} has invited you to {0}<0/>\"{documentName}\""
|
||||
msgid "{teamName} has invited you to {action} {documentName}"
|
||||
msgstr "{teamName} has invited you to {action} {documentName}"
|
||||
|
||||
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
|
||||
msgid "{userAgent}"
|
||||
msgstr "{userAgent}"
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgid "{userName} approved the document"
|
||||
msgstr "{userName} approved the document"
|
||||
@@ -745,7 +753,6 @@ msgstr "Acknowledgment"
|
||||
|
||||
#: apps/remix/app/components/tables/settings-security-activity-table.tsx
|
||||
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
|
||||
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/document-logs-table.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
@@ -1486,10 +1493,14 @@ msgstr "At least one signature type must be enabled"
|
||||
msgid "Attempts sealing the document again, useful for after a code change has occurred to resolve an erroneous document."
|
||||
msgstr "Attempts sealing the document again, useful for after a code change has occurred to resolve an erroneous document."
|
||||
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
|
||||
msgid "Audit Log"
|
||||
msgstr "Audit Log"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
msgid "Audit Logs"
|
||||
msgstr "Audit Logs"
|
||||
|
||||
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
|
||||
msgid "Authentication Level"
|
||||
msgstr "Authentication Level"
|
||||
@@ -1591,7 +1602,6 @@ msgid "Branding preferences updated"
|
||||
msgstr "Branding preferences updated"
|
||||
|
||||
#: apps/remix/app/components/tables/settings-security-activity-table.tsx
|
||||
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
|
||||
msgid "Browser"
|
||||
msgstr "Browser"
|
||||
|
||||
@@ -2109,6 +2119,10 @@ msgstr "Controls the formatting of the message that will be sent when inviting a
|
||||
msgid "Controls the language for the document, including the language to be used for email notifications, and the final certificate that is generated and attached to the document."
|
||||
msgstr "Controls the language for the document, including the language to be used for email notifications, and the final certificate that is generated and attached to the document."
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Controls whether the audit logs will be included in the document when it is downloaded. The audit logs can still be downloaded from the logs page separately."
|
||||
msgstr "Controls whether the audit logs will be included in the document when it is downloaded. The audit logs can still be downloaded from the logs page separately."
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Controls whether the signing certificate will be included in the document when it is downloaded. The signing certificate can still be downloaded from the logs page separately."
|
||||
msgstr "Controls whether the signing certificate will be included in the document when it is downloaded. The signing certificate can still be downloaded from the logs page separately."
|
||||
@@ -3140,6 +3154,7 @@ msgstr "Drag & drop your PDF here."
|
||||
msgid "Drag and drop or click to upload"
|
||||
msgstr "Drag and drop or click to upload"
|
||||
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx
|
||||
msgid "Drag and drop your PDF file here"
|
||||
msgstr "Drag and drop your PDF file here"
|
||||
@@ -3668,6 +3683,7 @@ msgstr "Fields"
|
||||
msgid "Fields updated"
|
||||
msgstr "Fields updated"
|
||||
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/document/document-upload.tsx
|
||||
#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx
|
||||
@@ -4042,6 +4058,10 @@ msgstr "Inbox"
|
||||
msgid "Inbox documents"
|
||||
msgstr "Inbox documents"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Include the Audit Logs in the Document"
|
||||
msgstr "Include the Audit Logs in the Document"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Include the Signing Certificate in the Document"
|
||||
msgstr "Include the Signing Certificate in the Document"
|
||||
@@ -4064,6 +4084,7 @@ msgstr "Inherit authentication method"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/branding-preferences-form.tsx
|
||||
msgid "Inherit from organisation"
|
||||
msgstr "Inherit from organisation"
|
||||
@@ -4599,6 +4620,10 @@ msgstr "Multiple access methods can be selected."
|
||||
msgid "My Folder"
|
||||
msgstr "My Folder"
|
||||
|
||||
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email-domains.$id.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table.tsx
|
||||
@@ -4679,6 +4704,7 @@ msgstr "Next"
|
||||
msgid "Next field"
|
||||
msgstr "Next field"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/branding-preferences-form.tsx
|
||||
@@ -5338,6 +5364,7 @@ msgstr "Please try a different domain."
|
||||
msgid "Please try again and make sure you enter the correct email address."
|
||||
msgstr "Please try again and make sure you enter the correct email address."
|
||||
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/dialogs/template-create-dialog.tsx
|
||||
msgid "Please try again later."
|
||||
msgstr "Please try again later."
|
||||
@@ -6390,6 +6417,7 @@ msgstr "Some signers have not been assigned a signature field. Please assign at
|
||||
#: apps/remix/app/components/general/share-document-download-button.tsx
|
||||
#: apps/remix/app/components/general/billing-plans.tsx
|
||||
#: apps/remix/app/components/general/billing-plans.tsx
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-usage.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-dropdown.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
|
||||
@@ -6827,6 +6855,10 @@ msgstr "Template title"
|
||||
msgid "Template updated successfully"
|
||||
msgstr "Template updated successfully"
|
||||
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
msgid "Template uploaded"
|
||||
msgstr "Template uploaded"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
|
||||
@@ -7445,7 +7477,6 @@ msgstr "This will remove all emails associated with this email domain"
|
||||
msgid "This will sign you out of all other devices. You will need to sign in again on those devices to continue using your account."
|
||||
msgstr "This will sign you out of all other devices. You will need to sign in again on those devices to continue using your account."
|
||||
|
||||
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
|
||||
#: apps/remix/app/components/tables/document-logs-table.tsx
|
||||
msgid "Time"
|
||||
msgstr "Time"
|
||||
@@ -7479,6 +7510,10 @@ msgstr "Title cannot be empty"
|
||||
msgid "To accept this invitation you must create an account."
|
||||
msgstr "To accept this invitation you must create an account."
|
||||
|
||||
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
|
||||
msgid "To be able to add members to a team, you must first add them to the organisation. For more information, please see the <0>documentation</0>."
|
||||
msgstr "To be able to add members to a team, you must first add them to the organisation. For more information, please see the <0>documentation</0>."
|
||||
|
||||
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
|
||||
msgid "To change the email you must remove and add a new email address."
|
||||
msgstr "To change the email you must remove and add a new email address."
|
||||
@@ -7928,6 +7963,10 @@ msgstr "Upload Document"
|
||||
msgid "Upload Signature"
|
||||
msgstr "Upload Signature"
|
||||
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
msgid "Upload Template"
|
||||
msgstr "Upload Template"
|
||||
|
||||
#: packages/ui/primitives/document-upload.tsx
|
||||
#: packages/ui/primitives/document-dropzone.tsx
|
||||
msgid "Upload Template Document"
|
||||
@@ -7958,6 +7997,10 @@ msgstr "Uploaded file not an allowed file type"
|
||||
msgid "Uploading document..."
|
||||
msgstr "Uploading document..."
|
||||
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
msgid "Uploading template..."
|
||||
msgstr "Uploading template..."
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
|
||||
msgid "Use"
|
||||
msgstr "Use"
|
||||
@@ -7985,6 +8028,10 @@ msgstr "Use your passkey for authentication"
|
||||
msgid "User"
|
||||
msgstr "User"
|
||||
|
||||
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
|
||||
msgid "User Agent"
|
||||
msgstr "User Agent"
|
||||
|
||||
#: apps/remix/app/components/forms/password.tsx
|
||||
msgid "User has no password."
|
||||
msgstr "User has no password."
|
||||
@@ -8062,10 +8109,6 @@ msgstr "Verify your email to upload documents."
|
||||
msgid "Verify your team email address"
|
||||
msgstr "Verify your team email address"
|
||||
|
||||
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
|
||||
msgid "Version History"
|
||||
msgstr "Version History"
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
|
||||
msgid "Vertical"
|
||||
msgstr "Vertical"
|
||||
@@ -8616,6 +8659,7 @@ msgstr "Write a description to display on your public profile"
|
||||
msgid "Yearly"
|
||||
msgstr "Yearly"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/branding-preferences-form.tsx
|
||||
@@ -9224,6 +9268,10 @@ msgstr "Your team has been successfully deleted."
|
||||
msgid "Your team has been successfully updated."
|
||||
msgstr "Your team has been successfully updated."
|
||||
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
msgid "Your template failed to upload."
|
||||
msgstr "Your template failed to upload."
|
||||
|
||||
#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx
|
||||
msgid "Your template has been created successfully"
|
||||
msgstr "Your template has been created successfully"
|
||||
@@ -9236,6 +9284,10 @@ msgstr "Your template has been duplicated successfully."
|
||||
msgid "Your template has been successfully deleted."
|
||||
msgstr "Your template has been successfully deleted."
|
||||
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
msgid "Your template has been uploaded successfully. You will be redirected to the template page."
|
||||
msgstr "Your template has been uploaded successfully. You will be redirected to the template page."
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
|
||||
msgid "Your template will be duplicated."
|
||||
msgstr "Your template will be duplicated."
|
||||
|
||||
+683
-115
File diff suppressed because it is too large
Load Diff
+681
-114
File diff suppressed because it is too large
Load Diff
+693
-120
File diff suppressed because it is too large
Load Diff
+681
-114
File diff suppressed because it is too large
Load Diff
@@ -134,7 +134,7 @@ export const internalClaims: InternalClaims = {
|
||||
unlimitedDocuments: true,
|
||||
allowCustomBranding: true,
|
||||
hidePoweredBy: true,
|
||||
emailDomains: true,
|
||||
emailDomains: false,
|
||||
embedAuthoring: false,
|
||||
embedAuthoringWhiteLabel: true,
|
||||
embedSigning: false,
|
||||
|
||||
@@ -305,87 +305,150 @@ export const formatDocumentAuditLogAction = (
|
||||
|
||||
const description = match(auditLog)
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED }, () => ({
|
||||
anonymous: msg`A field was added`,
|
||||
anonymous: msg({
|
||||
message: `A field was added`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} added a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED }, () => ({
|
||||
anonymous: msg`A field was removed`,
|
||||
anonymous: msg({
|
||||
message: `A field was removed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} removed a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED }, () => ({
|
||||
anonymous: msg`A field was updated`,
|
||||
anonymous: msg({
|
||||
message: `A field was updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED }, () => ({
|
||||
anonymous: msg`A recipient was added`,
|
||||
anonymous: msg({
|
||||
message: `A recipient was added`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} added a recipient`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED }, () => ({
|
||||
anonymous: msg`A recipient was removed`,
|
||||
anonymous: msg({
|
||||
message: `A recipient was removed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} removed a recipient`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED }, () => ({
|
||||
anonymous: msg`A recipient was updated`,
|
||||
anonymous: msg({
|
||||
message: `A recipient was updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated a recipient`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED }, () => ({
|
||||
anonymous: msg`Document created`,
|
||||
anonymous: msg({
|
||||
message: `Document created`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} created the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED }, () => ({
|
||||
anonymous: msg`Document deleted`,
|
||||
anonymous: msg({
|
||||
message: `Document deleted`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} deleted the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED }, () => ({
|
||||
anonymous: msg`Field signed`,
|
||||
anonymous: msg({
|
||||
message: `Field signed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} signed a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED }, () => ({
|
||||
anonymous: msg`Field unsigned`,
|
||||
anonymous: msg({
|
||||
message: `Field unsigned`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} unsigned a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED }, () => ({
|
||||
anonymous: msg`Field prefilled by assistant`,
|
||||
anonymous: msg({
|
||||
message: `Field prefilled by assistant`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} prefilled a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED }, () => ({
|
||||
anonymous: msg`Document visibility updated`,
|
||||
anonymous: msg({
|
||||
message: `Document visibility updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated the document visibility`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED }, () => ({
|
||||
anonymous: msg`Document access auth updated`,
|
||||
anonymous: msg({
|
||||
message: `Document access auth updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated the document access auth requirements`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED }, () => ({
|
||||
anonymous: msg`Document signing auth updated`,
|
||||
anonymous: msg({
|
||||
message: `Document signing auth updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated the document signing auth requirements`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED }, () => ({
|
||||
anonymous: msg`Document updated`,
|
||||
anonymous: msg({
|
||||
message: `Document updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED }, () => ({
|
||||
anonymous: msg`Document opened`,
|
||||
anonymous: msg({
|
||||
message: `Document opened`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} opened the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VIEWED }, () => ({
|
||||
anonymous: msg`Document viewed`,
|
||||
anonymous: msg({
|
||||
message: `Document viewed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} viewed the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED }, () => ({
|
||||
anonymous: msg`Document title updated`,
|
||||
anonymous: msg({
|
||||
message: `Document title updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated the document title`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED }, () => ({
|
||||
anonymous: msg`Document external ID updated`,
|
||||
anonymous: msg({
|
||||
message: `Document external ID updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated the document external ID`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT }, () => ({
|
||||
anonymous: msg`Document sent`,
|
||||
anonymous: msg({
|
||||
message: `Document sent`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} sent the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM }, () => ({
|
||||
anonymous: msg`Document moved to team`,
|
||||
anonymous: msg({
|
||||
message: `Document moved to team`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} moved the document to team`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED }, ({ data }) => {
|
||||
@@ -420,8 +483,14 @@ export const formatDocumentAuditLogAction = (
|
||||
: msg`${prefix} sent an email to ${data.recipientEmail}`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED }, () => ({
|
||||
anonymous: msg`Document completed`,
|
||||
identified: msg`Document completed`,
|
||||
anonymous: msg({
|
||||
message: `Document completed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg({
|
||||
message: `Document completed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
}))
|
||||
.exhaustive();
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { type TransportTargetOptions, pino } from 'pino';
|
||||
|
||||
import type { BaseApiLog } from '../types/api-logs';
|
||||
import { extractRequestMetadata } from '../universal/extract-request-metadata';
|
||||
import { env } from './env';
|
||||
|
||||
const transports: TransportTargetOptions[] = [];
|
||||
@@ -33,3 +35,31 @@ export const logger = pino({
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
export const logDocumentAccess = ({
|
||||
request,
|
||||
documentId,
|
||||
userId,
|
||||
}: {
|
||||
request: Request;
|
||||
documentId: number;
|
||||
userId: number;
|
||||
}) => {
|
||||
const metadata = extractRequestMetadata(request);
|
||||
|
||||
const data: BaseApiLog = {
|
||||
ipAddress: metadata.ipAddress,
|
||||
userAgent: metadata.userAgent,
|
||||
path: new URL(request.url).pathname,
|
||||
auth: 'session',
|
||||
source: 'app',
|
||||
userId,
|
||||
};
|
||||
|
||||
logger.info({
|
||||
...data,
|
||||
input: {
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -120,6 +120,7 @@ export const generateDefaultOrganisationSettings = (): Omit<
|
||||
|
||||
includeSenderDetails: true,
|
||||
includeSigningCertificate: true,
|
||||
includeAuditLog: false,
|
||||
|
||||
typedSignatureEnabled: true,
|
||||
uploadSignatureEnabled: true,
|
||||
|
||||
@@ -170,6 +170,7 @@ export const generateDefaultTeamSettings = (): Omit<TeamGlobalSettings, 'id' | '
|
||||
|
||||
includeSenderDetails: null,
|
||||
includeSigningCertificate: null,
|
||||
includeAuditLog: null,
|
||||
|
||||
typedSignatureEnabled: null,
|
||||
uploadSignatureEnabled: null,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "OrganisationGlobalSettings" ADD COLUMN "includeAuditLog" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "TeamGlobalSettings" ADD COLUMN "includeAuditLog" BOOLEAN;
|
||||
@@ -735,13 +735,13 @@ model OrganisationGlobalSettings {
|
||||
id String @id
|
||||
organisation Organisation?
|
||||
|
||||
documentVisibility DocumentVisibility @default(EVERYONE)
|
||||
documentLanguage String @default("en")
|
||||
documentTimezone String? // Nullable to allow using local timezones if not set.
|
||||
documentDateFormat String @default("yyyy-MM-dd hh:mm a")
|
||||
|
||||
includeSenderDetails Boolean @default(true)
|
||||
includeSigningCertificate Boolean @default(true)
|
||||
documentVisibility DocumentVisibility @default(EVERYONE)
|
||||
documentLanguage String @default("en")
|
||||
includeSenderDetails Boolean @default(true)
|
||||
includeSigningCertificate Boolean @default(true)
|
||||
includeAuditLog Boolean @default(false)
|
||||
documentTimezone String? // Nullable to allow using local timezones if not set.
|
||||
documentDateFormat String @default("yyyy-MM-dd hh:mm a")
|
||||
|
||||
typedSignatureEnabled Boolean @default(true)
|
||||
uploadSignatureEnabled Boolean @default(true)
|
||||
@@ -772,6 +772,7 @@ model TeamGlobalSettings {
|
||||
|
||||
includeSenderDetails Boolean?
|
||||
includeSigningCertificate Boolean?
|
||||
includeAuditLog Boolean?
|
||||
|
||||
typedSignatureEnabled Boolean?
|
||||
uploadSignatureEnabled Boolean?
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { DocumentDataType } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||
import { getPresignGetUrl } from '@documenso/lib/universal/upload/server-actions';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import { ZDownloadDocumentRequestSchema, ZDownloadDocumentResponseSchema } from './schema';
|
||||
|
||||
export const downloadDocumentRoute = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/document/{documentId}/download-beta',
|
||||
summary: 'Download document (beta)',
|
||||
description: 'Get a pre-signed download URL for the original or signed version of a document',
|
||||
tags: ['Document'],
|
||||
},
|
||||
})
|
||||
.input(ZDownloadDocumentRequestSchema)
|
||||
.output(ZDownloadDocumentResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { teamId, user } = ctx;
|
||||
const { documentId, version } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId,
|
||||
version,
|
||||
},
|
||||
});
|
||||
|
||||
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document downloads are only available when S3 storage is configured.',
|
||||
});
|
||||
}
|
||||
|
||||
const document = await getDocumentById({
|
||||
documentId,
|
||||
userId: user.id,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!document.documentData) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document data not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.documentData.type !== DocumentDataType.S3_PATH) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document is not stored in S3 and cannot be downloaded via URL.',
|
||||
});
|
||||
}
|
||||
|
||||
if (version === 'signed' && !isDocumentCompleted(document.status)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document is not completed yet.',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const documentData =
|
||||
version === 'original'
|
||||
? document.documentData.initialData || document.documentData.data
|
||||
: document.documentData.data;
|
||||
|
||||
const { url } = await getPresignGetUrl(documentData);
|
||||
|
||||
const baseTitle = document.title.replace(/\.pdf$/, '');
|
||||
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
|
||||
const filename = `${baseTitle}${suffix}`;
|
||||
|
||||
return {
|
||||
downloadUrl: url,
|
||||
filename,
|
||||
contentType: 'application/pdf',
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.logger.error({
|
||||
error,
|
||||
message: 'Failed to generate download URL',
|
||||
documentId,
|
||||
version,
|
||||
});
|
||||
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Failed to generate download URL',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DocumentDataType } from '@prisma/client';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||
@@ -27,6 +26,7 @@ import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-action
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
|
||||
import { authenticatedProcedure, procedure, router } from '../trpc';
|
||||
import { downloadDocumentRoute } from './download-document';
|
||||
import { findInboxRoute } from './find-inbox';
|
||||
import { getInboxCountRoute } from './get-inbox-count';
|
||||
import {
|
||||
@@ -63,6 +63,7 @@ export const documentRouter = router({
|
||||
getCount: getInboxCountRoute,
|
||||
},
|
||||
updateDocument: updateDocumentRoute,
|
||||
downloadDocument: downloadDocumentRoute,
|
||||
|
||||
/**
|
||||
* @private
|
||||
@@ -636,8 +637,7 @@ export const documentRouter = router({
|
||||
}).catch(() => null);
|
||||
|
||||
if (!document || (teamId && document.teamId !== teamId)) {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have access to this document.',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -295,7 +295,7 @@ export const ZDistributeDocumentRequestSchema = z.object({
|
||||
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
|
||||
language: ZDocumentMetaLanguageSchema.optional(),
|
||||
emailId: z.string().nullish(),
|
||||
emailReplyTo: z.string().nullish(),
|
||||
emailReplyTo: z.string().email().nullish(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
@@ -346,3 +346,22 @@ export const ZDownloadAuditLogsMutationSchema = z.object({
|
||||
export const ZDownloadCertificateMutationSchema = z.object({
|
||||
documentId: z.number(),
|
||||
});
|
||||
|
||||
export const ZDownloadDocumentRequestSchema = z.object({
|
||||
documentId: z.number().describe('The ID of the document to download.'),
|
||||
version: z
|
||||
.enum(['original', 'signed'])
|
||||
.describe(
|
||||
'The version of the document to download. "signed" returns the completed document with signatures, "original" returns the original uploaded document.',
|
||||
)
|
||||
.default('signed'),
|
||||
});
|
||||
|
||||
export const ZDownloadDocumentResponseSchema = z.object({
|
||||
downloadUrl: z.string().describe('Pre-signed URL for downloading the PDF file'),
|
||||
filename: z.string().describe('The filename of the PDF file'),
|
||||
contentType: z.string().describe('MIME type of the file'),
|
||||
});
|
||||
|
||||
export type TDownloadDocumentRequest = z.infer<typeof ZDownloadDocumentRequestSchema>;
|
||||
export type TDownloadDocumentResponse = z.infer<typeof ZDownloadDocumentResponseSchema>;
|
||||
|
||||
@@ -62,7 +62,7 @@ export const ZUpdateDocumentRequestSchema = z.object({
|
||||
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
|
||||
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
|
||||
emailId: z.string().nullish(),
|
||||
emailReplyTo: z.string().nullish(),
|
||||
emailReplyTo: z.string().email().nullish(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
@@ -54,7 +54,7 @@ export const createSubscriptionRoute = authenticatedProcedure
|
||||
|
||||
if (!customerId) {
|
||||
const customer = await createCustomer({
|
||||
name: organisation.name,
|
||||
name: organisation.owner.name || organisation.owner.email,
|
||||
email: organisation.owner.email,
|
||||
});
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export const manageSubscriptionRoute = authenticatedProcedure
|
||||
// If the customer ID is still missing create a new customer.
|
||||
if (!customerId) {
|
||||
const customer = await createCustomer({
|
||||
name: organisation.name,
|
||||
name: organisation.owner.name || organisation.owner.email,
|
||||
email: organisation.owner.email,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { createFolder } from '@documenso/lib/server-only/folder/create-folder';
|
||||
import { deleteFolder } from '@documenso/lib/server-only/folder/delete-folder';
|
||||
import { findFolders } from '@documenso/lib/server-only/folder/find-folders';
|
||||
@@ -137,8 +136,7 @@ export const folderRouter = router({
|
||||
type,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Parent folder not found',
|
||||
});
|
||||
}
|
||||
@@ -248,8 +246,7 @@ export const folderRouter = router({
|
||||
type: currentFolder.type,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Parent folder not found',
|
||||
});
|
||||
}
|
||||
@@ -294,8 +291,7 @@ export const folderRouter = router({
|
||||
type: FolderType.DOCUMENT,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Folder not found',
|
||||
});
|
||||
}
|
||||
@@ -340,8 +336,7 @@ export const folderRouter = router({
|
||||
type: FolderType.TEMPLATE,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Folder not found',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
|
||||
documentDateFormat,
|
||||
includeSenderDetails,
|
||||
includeSigningCertificate,
|
||||
includeAuditLog,
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
@@ -117,6 +118,7 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
|
||||
documentDateFormat,
|
||||
includeSenderDetails,
|
||||
includeSigningCertificate,
|
||||
includeAuditLog,
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
|
||||
@@ -19,6 +19,7 @@ export const ZUpdateOrganisationSettingsRequestSchema = z.object({
|
||||
documentDateFormat: ZDocumentMetaDateFormatSchema.optional(),
|
||||
includeSenderDetails: z.boolean().optional(),
|
||||
includeSigningCertificate: z.boolean().optional(),
|
||||
includeAuditLog: z.boolean().optional(),
|
||||
typedSignatureEnabled: z.boolean().optional(),
|
||||
uploadSignatureEnabled: z.boolean().optional(),
|
||||
drawSignatureEnabled: z.boolean().optional(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
|
||||
import {
|
||||
ZRecipientAccessAuthTypesSchema,
|
||||
ZRecipientActionAuthSchema,
|
||||
@@ -186,7 +187,18 @@ export const ZSetTemplateRecipientsRequestSchema = z
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
nativeId: z.number().optional(),
|
||||
email: z.string().toLowerCase().email().min(1),
|
||||
email: z
|
||||
.string()
|
||||
.toLowerCase()
|
||||
.refine(
|
||||
(email) => {
|
||||
return (
|
||||
isTemplateRecipientEmailPlaceholder(email) ||
|
||||
z.string().email().safeParse(email).success
|
||||
);
|
||||
},
|
||||
{ message: 'Please enter a valid email address' },
|
||||
),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
@@ -196,9 +208,12 @@ export const ZSetTemplateRecipientsRequestSchema = z
|
||||
})
|
||||
.refine(
|
||||
(schema) => {
|
||||
const emails = schema.recipients.map((recipient) => recipient.email);
|
||||
// Filter out placeholder emails and only check uniqueness for actual emails
|
||||
const nonPlaceholderEmails = schema.recipients
|
||||
.map((recipient) => recipient.email)
|
||||
.filter((email) => !isTemplateRecipientEmailPlaceholder(email));
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
return new Set(nonPlaceholderEmails).size === nonPlaceholderEmails.length;
|
||||
},
|
||||
// Dirty hack to handle errors when .root is populated for an array type
|
||||
{ message: 'Recipients must have unique emails', path: ['recipients__root'] },
|
||||
|
||||
@@ -32,6 +32,7 @@ export const updateTeamSettingsRoute = authenticatedProcedure
|
||||
documentDateFormat,
|
||||
includeSenderDetails,
|
||||
includeSigningCertificate,
|
||||
includeAuditLog,
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
@@ -110,6 +111,7 @@ export const updateTeamSettingsRoute = authenticatedProcedure
|
||||
documentDateFormat,
|
||||
includeSenderDetails,
|
||||
includeSigningCertificate,
|
||||
includeAuditLog,
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
|
||||
@@ -23,6 +23,7 @@ export const ZUpdateTeamSettingsRequestSchema = z.object({
|
||||
documentDateFormat: ZDocumentMetaDateFormatSchema.nullish(),
|
||||
includeSenderDetails: z.boolean().nullish(),
|
||||
includeSigningCertificate: z.boolean().nullish(),
|
||||
includeAuditLog: z.boolean().nullish(),
|
||||
typedSignatureEnabled: z.boolean().nullish(),
|
||||
uploadSignatureEnabled: z.boolean().nullish(),
|
||||
drawSignatureEnabled: z.boolean().nullish(),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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';
|
||||
@@ -556,9 +555,9 @@ export const templateRouter = router({
|
||||
});
|
||||
|
||||
if (csv.length > 4 * 1024 * 1024) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
|
||||
message: 'File size exceeds 4MB limit',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -569,8 +568,7 @@ export const templateRouter = router({
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template not found',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export const ZTemplateMetaUpsertSchema = z.object({
|
||||
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
|
||||
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
|
||||
emailId: z.string().nullish(),
|
||||
emailReplyTo: z.string().nullish(),
|
||||
emailReplyTo: z.string().email().nullish(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
|
||||
language: ZDocumentMetaLanguageSchema.optional(),
|
||||
|
||||
@@ -7,6 +7,7 @@ import { SigningStatus } from '@prisma/client';
|
||||
import { Clock, EyeOffIcon } from 'lucide-react';
|
||||
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
|
||||
import type { DocumentField } from '@documenso/lib/server-only/field/get-fields-for-document';
|
||||
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
@@ -21,6 +22,18 @@ import { PopoverHover } from '@documenso/ui/primitives/popover';
|
||||
import { getRecipientColorStyles } from '../../lib/recipient-colors';
|
||||
import { FieldContent } from '../../primitives/document-flow/field-content';
|
||||
|
||||
const getRecipientDisplayText = (recipient: { name: string; email: string }) => {
|
||||
if (recipient.name && !isTemplateRecipientEmailPlaceholder(recipient.email)) {
|
||||
return `${recipient.name} (${recipient.email})`;
|
||||
}
|
||||
|
||||
if (recipient.name && isTemplateRecipientEmailPlaceholder(recipient.email)) {
|
||||
return recipient.name;
|
||||
}
|
||||
|
||||
return recipient.email;
|
||||
};
|
||||
|
||||
export type DocumentReadOnlyFieldsProps = {
|
||||
fields: DocumentField[];
|
||||
documentMeta?: Pick<DocumentMeta | TemplateMeta, 'dateFormat'>;
|
||||
@@ -145,9 +158,7 @@ export const DocumentReadOnlyFields = ({
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-1 text-center text-xs">
|
||||
{field.recipient.name
|
||||
? `${field.recipient.name} (${field.recipient.email})`
|
||||
: field.recipient.email}{' '}
|
||||
{getRecipientDisplayText(field.recipient)}
|
||||
</p>
|
||||
|
||||
<button
|
||||
|
||||
@@ -65,7 +65,11 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-background animate-in data-[state=open]:fade-in-90 sm:zoom-in-90 data-[state=open]:slide-in-from-bottom-10 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-50 grid w-full gap-4 rounded-b-lg border p-6 shadow-lg sm:max-w-lg sm:rounded-lg',
|
||||
'bg-background animate-in data-[state=open]:fade-in-90 sm:zoom-in-90 data-[state=open]:slide-in-from-bottom-10 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-50 grid w-full gap-4 border p-6 shadow-lg sm:max-w-lg sm:rounded-lg',
|
||||
{
|
||||
'rounded-b-xl': position === 'start',
|
||||
'rounded-t-xl': position === 'end',
|
||||
},
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -6,7 +6,10 @@ import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-emai
|
||||
export const ZAddSubjectFormSchema = z.object({
|
||||
meta: z.object({
|
||||
emailId: z.string().nullable(),
|
||||
emailReplyTo: z.string().email().optional(),
|
||||
emailReplyTo: z.preprocess(
|
||||
(val) => (val === '' ? undefined : val),
|
||||
z.string().email().optional(),
|
||||
),
|
||||
// emailReplyName: z.string().optional(),
|
||||
subject: z.string(),
|
||||
message: z.string(),
|
||||
|
||||
@@ -160,14 +160,14 @@ export const FieldContent = ({ field, documentMeta }: FieldIconProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
let textToDisplay = fieldMeta?.label || _(FRIENDLY_FIELD_TYPE[type]) || '';
|
||||
const labelToDisplay = fieldMeta?.label || _(FRIENDLY_FIELD_TYPE[type]) || '';
|
||||
let textToDisplay: string | undefined;
|
||||
|
||||
const isSignatureField =
|
||||
field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE;
|
||||
|
||||
// Trim default labels.
|
||||
if (textToDisplay.length > 20) {
|
||||
textToDisplay = textToDisplay.substring(0, 20) + '...';
|
||||
if (field.type === FieldType.TEXT && field.fieldMeta?.type === 'text' && field.fieldMeta?.text) {
|
||||
textToDisplay = field.fieldMeta.text;
|
||||
}
|
||||
|
||||
if (field.inserted) {
|
||||
@@ -190,18 +190,19 @@ export const FieldContent = ({ field, documentMeta }: FieldIconProps) => {
|
||||
const textAlign = fieldMeta && 'textAlign' in fieldMeta ? fieldMeta.textAlign : 'left';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'text-field-card-foreground flex h-full w-full items-center justify-center gap-x-1.5 overflow-clip whitespace-nowrap text-center text-[clamp(0.07rem,25cqw,0.825rem)]',
|
||||
{
|
||||
// Using justify instead of align because we also vertically center the text.
|
||||
'justify-start': field.inserted && !isSignatureField && textAlign === 'left',
|
||||
'justify-end': field.inserted && !isSignatureField && textAlign === 'right',
|
||||
'font-signature text-[clamp(0.07rem,25cqw,1.125rem)]': isSignatureField,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{textToDisplay}
|
||||
<div className="flex h-full w-full items-center overflow-hidden">
|
||||
<p
|
||||
className={cn(
|
||||
'text-foreground w-full whitespace-pre-wrap text-left text-[clamp(0.07rem,25cqw,0.825rem)] duration-200',
|
||||
{
|
||||
'!text-center': textAlign === 'center' || !textToDisplay,
|
||||
'!text-right': textAlign === 'right',
|
||||
'font-signature text-[clamp(0.07rem,25cqw,1.125rem)]': isSignatureField,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{textToDisplay || labelToDisplay}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -96,7 +96,7 @@ export const DocumentDropzone = ({
|
||||
return (
|
||||
<Button loading={loading} aria-disabled={disabled} {...getRootProps()} {...props}>
|
||||
<div className="flex items-center gap-2">
|
||||
<input {...getInputProps()} />
|
||||
<input data-testid="document-upload-input" {...getInputProps()} />
|
||||
{!loading && <Upload className="h-4 w-4" />}
|
||||
{disabled ? _(disabledMessage) : _(heading[type])}
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@ import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-c
|
||||
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
|
||||
import {
|
||||
type TFieldMetaSchema as FieldMeta,
|
||||
ZFieldMetaSchema,
|
||||
@@ -593,15 +594,21 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
selectedSignerStyles?.comboxBoxTrigger,
|
||||
)}
|
||||
>
|
||||
{selectedSigner?.email && (
|
||||
<span className="flex-1 truncate text-left">
|
||||
{selectedSigner?.name} ({selectedSigner?.email})
|
||||
</span>
|
||||
)}
|
||||
{selectedSigner?.email &&
|
||||
!isTemplateRecipientEmailPlaceholder(selectedSigner.email) && (
|
||||
<span className="flex-1 truncate text-left">
|
||||
{selectedSigner?.name} ({selectedSigner?.email})
|
||||
</span>
|
||||
)}
|
||||
|
||||
{selectedSigner?.email &&
|
||||
isTemplateRecipientEmailPlaceholder(selectedSigner.email) && (
|
||||
<span className="flex-1 truncate text-left">{selectedSigner?.name}</span>
|
||||
)}
|
||||
|
||||
{!selectedSigner?.email && (
|
||||
<span className="gradie flex-1 truncate text-left">
|
||||
{selectedSigner?.email}
|
||||
No recipient selected
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -657,15 +664,22 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
'text-foreground/80': recipient === selectedSigner,
|
||||
})}
|
||||
>
|
||||
{recipient.name && (
|
||||
<span title={`${recipient.name} (${recipient.email})`}>
|
||||
{recipient.name} ({recipient.email})
|
||||
</span>
|
||||
)}
|
||||
{recipient.name &&
|
||||
!isTemplateRecipientEmailPlaceholder(recipient.email) && (
|
||||
<span title={`${recipient.name} (${recipient.email})`}>
|
||||
{recipient.name} ({recipient.email})
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!recipient.name && (
|
||||
<span title={recipient.email}>{recipient.email}</span>
|
||||
)}
|
||||
{recipient.name &&
|
||||
isTemplateRecipientEmailPlaceholder(recipient.email) && (
|
||||
<span title={recipient.name}>{recipient.name}</span>
|
||||
)}
|
||||
|
||||
{!recipient.name &&
|
||||
!isTemplateRecipientEmailPlaceholder(recipient.email) && (
|
||||
<span title={recipient.email}>{recipient.email}</span>
|
||||
)}
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useFieldArray, useForm } from 'react-hook-form';
|
||||
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
|
||||
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { generateRecipientPlaceholder } from '@documenso/lib/utils/templates';
|
||||
@@ -247,62 +248,6 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
[form, watchedSigners, toast],
|
||||
);
|
||||
|
||||
const triggerDragAndDrop = useCallback(
|
||||
(fromIndex: number, toIndex: number) => {
|
||||
if (!$sensorApi.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const draggableId = signers[fromIndex].id;
|
||||
|
||||
const preDrag = $sensorApi.current.tryGetLock(draggableId);
|
||||
|
||||
if (!preDrag) {
|
||||
return;
|
||||
}
|
||||
|
||||
const drag = preDrag.snapLift();
|
||||
|
||||
setTimeout(() => {
|
||||
// Move directly to the target index
|
||||
if (fromIndex < toIndex) {
|
||||
for (let i = fromIndex; i < toIndex; i++) {
|
||||
drag.moveDown();
|
||||
}
|
||||
} else {
|
||||
for (let i = fromIndex; i > toIndex; i--) {
|
||||
drag.moveUp();
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
drag.drop();
|
||||
}, 500);
|
||||
}, 0);
|
||||
},
|
||||
[signers],
|
||||
);
|
||||
|
||||
const updateSigningOrders = useCallback(
|
||||
(newIndex: number, oldIndex: number) => {
|
||||
const updatedSigners = form.getValues('signers').map((signer, index) => {
|
||||
if (index === oldIndex) {
|
||||
return { ...signer, signingOrder: newIndex + 1 };
|
||||
} else if (index >= newIndex && index < oldIndex) {
|
||||
return { ...signer, signingOrder: (signer.signingOrder ?? index + 1) + 1 };
|
||||
} else if (index <= newIndex && index > oldIndex) {
|
||||
return { ...signer, signingOrder: Math.max(1, (signer.signingOrder ?? index + 1) - 1) };
|
||||
}
|
||||
return signer;
|
||||
});
|
||||
|
||||
updatedSigners.forEach((signer, index) => {
|
||||
form.setValue(`signers.${index}.signingOrder`, signer.signingOrder);
|
||||
});
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const handleSigningOrderChange = useCallback(
|
||||
(index: number, newOrderString: string) => {
|
||||
const trimmedOrderString = newOrderString.trim();
|
||||
@@ -592,7 +537,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
})}
|
||||
>
|
||||
{!showAdvancedSettings && index === 0 && (
|
||||
<FormLabel required>
|
||||
<FormLabel>
|
||||
<Trans>Email</Trans>
|
||||
</FormLabel>
|
||||
)}
|
||||
@@ -602,6 +547,11 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
type="email"
|
||||
placeholder={_(msg`Email`)}
|
||||
{...field}
|
||||
value={
|
||||
isTemplateRecipientEmailPlaceholder(field.value)
|
||||
? ''
|
||||
: field.value
|
||||
}
|
||||
disabled={
|
||||
field.disabled ||
|
||||
isSubmitting ||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX } from '@documenso/lib/constants/template';
|
||||
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
||||
|
||||
export const ZAddTemplatePlacholderRecipientsFormSchema = z
|
||||
@@ -10,7 +11,7 @@ export const ZAddTemplatePlacholderRecipientsFormSchema = z
|
||||
formId: z.string().min(1),
|
||||
nativeId: z.number().optional(),
|
||||
email: z.string().min(1).email(),
|
||||
name: z.string(),
|
||||
name: z.string().min(1, { message: 'Name is required' }),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||
@@ -21,12 +22,25 @@ export const ZAddTemplatePlacholderRecipientsFormSchema = z
|
||||
})
|
||||
.refine(
|
||||
(schema) => {
|
||||
const emails = schema.signers.map((signer) => signer.email.toLowerCase());
|
||||
const nonPlaceholderEmails = schema.signers
|
||||
.map((signer) => signer.email.toLowerCase())
|
||||
.filter((email) => !TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX.test(email));
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
return new Set(nonPlaceholderEmails).size === nonPlaceholderEmails.length;
|
||||
},
|
||||
// Dirty hack to handle errors when .root is populated for an array type
|
||||
{ message: 'Signers must have unique emails', path: ['signers__root'] },
|
||||
)
|
||||
.refine(
|
||||
/*
|
||||
Since placeholder emails are empty, we need to check that the names are unique.
|
||||
If we don't do this, the app will add duplicate signers and merge them in the next step, where you add fields.
|
||||
*/
|
||||
(schema) => {
|
||||
const names = schema.signers.map((signer) => signer.name.trim());
|
||||
return new Set(names).size === names.length;
|
||||
},
|
||||
{ message: 'Signers must have unique names', path: ['signers__root'] },
|
||||
);
|
||||
|
||||
export type TAddTemplatePlacholderRecipientsFormSchema = z.infer<
|
||||
|
||||
@@ -49,7 +49,10 @@ export const ZAddTemplateSettingsFormSchema = z.object({
|
||||
.optional()
|
||||
.default('en'),
|
||||
emailId: z.string().nullable(),
|
||||
emailReplyTo: z.string().optional(),
|
||||
emailReplyTo: z.preprocess(
|
||||
(val) => (val === '' ? undefined : val),
|
||||
z.string().email().optional(),
|
||||
),
|
||||
emailSettings: ZDocumentEmailSettingsSchema,
|
||||
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(1, {
|
||||
message: msg`At least one signature type must be enabled`.id,
|
||||
|
||||
Reference in New Issue
Block a user