feat: include audit trail log in the completed doc (#1916)

This change allows users to include the audit trail log in the completed
documents; similar to the signing certificate.


https://github.com/user-attachments/assets/d9ae236a-2584-4ad6-b7bc-27b3eb8c74d3

It also solves the issue with the text cutoff.
This commit is contained in:
Catalin Pit
2025-08-07 04:44:59 +03:00
committed by GitHub
parent f24b71f559
commit d1eb14ac16
21 changed files with 353 additions and 79 deletions

View File

@ -55,6 +55,7 @@ export type TDocumentPreferencesFormSchema = {
documentDateFormat: TDocumentMetaDateFormat | null; documentDateFormat: TDocumentMetaDateFormat | null;
includeSenderDetails: boolean | null; includeSenderDetails: boolean | null;
includeSigningCertificate: boolean | null; includeSigningCertificate: boolean | null;
includeAuditLog: boolean | null;
signatureTypes: DocumentSignatureType[]; signatureTypes: DocumentSignatureType[];
}; };
@ -66,6 +67,7 @@ type SettingsSubset = Pick<
| 'documentDateFormat' | 'documentDateFormat'
| 'includeSenderDetails' | 'includeSenderDetails'
| 'includeSigningCertificate' | 'includeSigningCertificate'
| 'includeAuditLog'
| 'typedSignatureEnabled' | 'typedSignatureEnabled'
| 'uploadSignatureEnabled' | 'uploadSignatureEnabled'
| 'drawSignatureEnabled' | 'drawSignatureEnabled'
@ -96,6 +98,7 @@ export const DocumentPreferencesForm = ({
documentDateFormat: ZDocumentMetaTimezoneSchema.nullable(), documentDateFormat: ZDocumentMetaTimezoneSchema.nullable(),
includeSenderDetails: z.boolean().nullable(), includeSenderDetails: z.boolean().nullable(),
includeSigningCertificate: z.boolean().nullable(), includeSigningCertificate: z.boolean().nullable(),
includeAuditLog: z.boolean().nullable(),
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(canInherit ? 0 : 1, { signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(canInherit ? 0 : 1, {
message: msg`At least one signature type must be enabled`.id, message: msg`At least one signature type must be enabled`.id,
}), }),
@ -112,6 +115,7 @@ export const DocumentPreferencesForm = ({
documentDateFormat: settings.documentDateFormat as TDocumentMetaDateFormat | null, documentDateFormat: settings.documentDateFormat as TDocumentMetaDateFormat | null,
includeSenderDetails: settings.includeSenderDetails, includeSenderDetails: settings.includeSenderDetails,
includeSigningCertificate: settings.includeSigningCertificate, includeSigningCertificate: settings.includeSigningCertificate,
includeAuditLog: settings.includeAuditLog,
signatureTypes: extractTeamSignatureSettings({ ...settings }), signatureTypes: extractTeamSignatureSettings({ ...settings }),
}, },
resolver: zodResolver(ZDocumentPreferencesFormSchema), resolver: zodResolver(ZDocumentPreferencesFormSchema),
@ -452,6 +456,56 @@ export const DocumentPreferencesForm = ({
)} )}
/> />
<FormField
control={form.control}
name="includeAuditLog"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>
<Trans>Include the Audit Logs in the Document</Trans>
</FormLabel>
<FormControl>
<Select
{...field}
value={field.value === null ? '-1' : field.value.toString()}
onValueChange={(value) =>
field.onChange(value === 'true' ? true : value === 'false' ? false : null)
}
>
<SelectTrigger className="bg-background text-muted-foreground">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">
<Trans>Yes</Trans>
</SelectItem>
<SelectItem value="false">
<Trans>No</Trans>
</SelectItem>
{canInherit && (
<SelectItem value={'-1'}>
<Trans>Inherit from organisation</Trans>
</SelectItem>
)}
</SelectContent>
</Select>
</FormControl>
<FormDescription>
<Trans>
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.
</Trans>
</FormDescription>
</FormItem>
)}
/>
<div className="flex flex-row justify-end space-x-4"> <div className="flex flex-row justify-end space-x-4">
<Button type="submit" loading={form.formState.isSubmitting}> <Button type="submit" loading={form.formState.isSubmitting}>
<Trans>Update</Trans> <Trans>Update</Trans>

View File

@ -164,7 +164,7 @@ export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownP
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link to={`${documentsPath}/${document.id}/logs`}> <Link to={`${documentsPath}/${document.id}/logs`}>
<ScrollTextIcon className="mr-2 h-4 w-4" /> <ScrollTextIcon className="mr-2 h-4 w-4" />
<Trans>Audit Log</Trans> <Trans>Audit Logs</Trans>
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>

View File

@ -1,20 +1,18 @@
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
import type { DateTimeFormatOptions } from 'luxon'; import type { DateTimeFormatOptions } from 'luxon';
import { DateTime } from 'luxon';
import { P, match } from 'ts-pattern';
import { UAParser } from 'ua-parser-js'; import { UAParser } from 'ua-parser-js';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n'; import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import type { TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
import { import {
Table, DOCUMENT_AUDIT_LOG_TYPE,
TableBody, type TDocumentAuditLog,
TableCell, } from '@documenso/lib/types/document-audit-logs';
TableHead, import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
TableHeader, import { cn } from '@documenso/ui/lib/utils';
TableRow, import { Card, CardContent } from '@documenso/ui/primitives/card';
} from '@documenso/ui/primitives/table';
export type AuditLogDataTableProps = { export type AuditLogDataTableProps = {
logs: TDocumentAuditLog[]; logs: TDocumentAuditLog[];
@ -25,71 +23,129 @@ const dateFormat: DateTimeFormatOptions = {
hourCycle: 'h12', hourCycle: 'h12',
}; };
/**
* Get the color indicator for the audit log type
*/
const getAuditLogIndicatorColor = (type: string) =>
match(type)
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED, () => 'bg-green-500')
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED, () => 'bg-red-500')
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT, () => 'bg-orange-500')
.with(
P.union(
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED,
),
() => 'bg-blue-500',
)
.otherwise(() => 'bg-muted');
/** /**
* DO NOT USE TRANS. YOU MUST USE _ FOR THIS FILE AND ALL CHILDREN COMPONENTS. * DO NOT USE TRANS. YOU MUST USE _ FOR THIS FILE AND ALL CHILDREN COMPONENTS.
*/ */
const formatUserAgent = (userAgent: string | null | undefined, userAgentInfo: UAParser.IResult) => {
if (!userAgent) {
return msg`N/A`;
}
const browser = userAgentInfo.browser.name;
const version = userAgentInfo.browser.version;
const os = userAgentInfo.os.name;
// If we can parse meaningful browser info, format it nicely
if (browser && os) {
const browserInfo = version ? `${browser} ${version}` : browser;
return msg`${browserInfo} on ${os}`;
}
return msg`${userAgent}`;
};
export const InternalAuditLogTable = ({ logs }: AuditLogDataTableProps) => { export const InternalAuditLogTable = ({ logs }: AuditLogDataTableProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const parser = new UAParser(); const parser = new UAParser();
const uppercaseFistLetter = (text: string) => {
return text.charAt(0).toUpperCase() + text.slice(1);
};
return ( return (
<Table overflowHidden> <div className="space-y-4">
<TableHeader> {logs.map((log, index) => {
<TableRow> parser.setUA(log.userAgent || '');
<TableHead>{_(msg`Time`)}</TableHead> const formattedAction = formatDocumentAuditLogAction(_, log);
<TableHead>{_(msg`User`)}</TableHead> const userAgentInfo = parser.getResult();
<TableHead>{_(msg`Action`)}</TableHead>
<TableHead>{_(msg`IP Address`)}</TableHead>
<TableHead>{_(msg`Browser`)}</TableHead>
</TableRow>
</TableHeader>
<TableBody className="print:text-xs"> return (
{logs.map((log, i) => ( <Card
<TableRow className="break-inside-avoid" key={i}> key={index}
<TableCell> // Add top margin for the first card to ensure it's not cut off from the 2nd page onwards
{DateTime.fromJSDate(log.createdAt) className={`border shadow-sm ${index > 0 ? 'print:mt-8' : ''}`}
.setLocale(APP_I18N_OPTIONS.defaultLocale) style={{
.toLocaleString(dateFormat)} pageBreakInside: 'avoid',
</TableCell> breakInside: 'avoid',
}}
>
<CardContent className="p-4">
{/* Header Section with indicator, event type, and timestamp */}
<div className="mb-3 flex items-start justify-between">
<div className="flex items-baseline gap-3">
<div
className={cn(`h-2 w-2 rounded-full`, getAuditLogIndicatorColor(log.type))}
/>
<TableCell> <div>
{log.name || log.email ? ( <div className="text-muted-foreground text-sm font-medium uppercase tracking-wide print:text-[8pt]">
<div> {log.type.replace(/_/g, ' ')}
{log.name && ( </div>
<p className="break-all" title={log.name}>
{log.name}
</p>
)}
{log.email && ( <div className="text-foreground text-sm font-medium print:text-[8pt]">
<p className="text-muted-foreground break-all" title={log.email}> {formattedAction.description}
{log.email} </div>
</p> </div>
)}
</div> </div>
) : (
<p>N/A</p>
)}
</TableCell>
<TableCell> <div className="text-muted-foreground text-sm print:text-[8pt]">
{uppercaseFistLetter(formatDocumentAuditLogAction(_, log).description)} {DateTime.fromJSDate(log.createdAt)
</TableCell> .setLocale(APP_I18N_OPTIONS.defaultLocale)
.toLocaleString(dateFormat)}
</div>
</div>
<TableCell>{log.ipAddress}</TableCell> <hr className="my-4" />
<TableCell> {/* Details Section - Two column layout */}
{log.userAgent ? parser.setUA(log.userAgent).getBrowser().name : 'N/A'} <div className="grid grid-cols-2 gap-x-8 gap-y-2 text-xs print:text-[6pt]">
</TableCell> <div>
</TableRow> <div className="text-muted-foreground/70 font-medium uppercase tracking-wide">
))} {_(msg`User`)}
</TableBody> </div>
</Table>
<div className="text-foreground mt-1 font-mono">{log.email || 'N/A'}</div>
</div>
<div className="text-right">
<div className="text-muted-foreground/70 font-medium uppercase tracking-wide">
{_(msg`IP Address`)}
</div>
<div className="text-foreground mt-1 font-mono">{log.ipAddress || 'N/A'}</div>
</div>
<div className="col-span-2">
<div className="text-muted-foreground/70 font-medium uppercase tracking-wide">
{_(msg`User Agent`)}
</div>
<div className="text-foreground mt-1">
{_(formatUserAgent(log.userAgent, userAgentInfo))}
</div>
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
); );
}; };

View File

@ -46,6 +46,7 @@ export default function OrganisationSettingsDocumentPage() {
documentDateFormat, documentDateFormat,
includeSenderDetails, includeSenderDetails,
includeSigningCertificate, includeSigningCertificate,
includeAuditLog,
signatureTypes, signatureTypes,
} = data; } = data;
@ -54,7 +55,8 @@ export default function OrganisationSettingsDocumentPage() {
documentLanguage === null || documentLanguage === null ||
documentDateFormat === null || documentDateFormat === null ||
includeSenderDetails === null || includeSenderDetails === null ||
includeSigningCertificate === null includeSigningCertificate === null ||
includeAuditLog === null
) { ) {
throw new Error('Should not be possible.'); throw new Error('Should not be possible.');
} }
@ -68,6 +70,7 @@ export default function OrganisationSettingsDocumentPage() {
documentDateFormat, documentDateFormat,
includeSenderDetails, includeSenderDetails,
includeSigningCertificate, includeSigningCertificate,
includeAuditLog,
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE), typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD), uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW), drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),

View File

@ -170,7 +170,7 @@ export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps)
<ul className="text-muted-foreground list-inside list-disc"> <ul className="text-muted-foreground list-inside list-disc">
{recipients.map((recipient) => ( {recipients.map((recipient) => (
<li key={`recipient-${recipient.id}`}> <li key={`recipient-${recipient.id}`}>
<span className="-ml-2">{formatRecipientText(recipient)}</span> <span>{formatRecipientText(recipient)}</span>
</li> </li>
))} ))}
</ul> </ul>

View File

@ -38,6 +38,7 @@ export default function TeamsSettingsPage() {
documentDateFormat, documentDateFormat,
includeSenderDetails, includeSenderDetails,
includeSigningCertificate, includeSigningCertificate,
includeAuditLog,
signatureTypes, signatureTypes,
} = data; } = data;
@ -50,6 +51,7 @@ export default function TeamsSettingsPage() {
documentDateFormat, documentDateFormat,
includeSenderDetails, includeSenderDetails,
includeSigningCertificate, includeSigningCertificate,
includeAuditLog,
...(signatureTypes.length === 0 ...(signatureTypes.length === 0
? { ? {
typedSignatureEnabled: null, typedSignatureEnabled: null,

View File

@ -0,0 +1,5 @@
@media print {
html {
font-size: 10pt;
}
}

View File

@ -12,10 +12,17 @@ import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-
import { getTranslations } from '@documenso/lib/utils/i18n'; import { getTranslations } from '@documenso/lib/utils/i18n';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Card, CardContent } from '@documenso/ui/primitives/card';
import appStylesheet from '~/app.css?url';
import { BrandingLogo } from '~/components/general/branding-logo'; import { BrandingLogo } from '~/components/general/branding-logo';
import { InternalAuditLogTable } from '~/components/tables/internal-audit-log-table'; import { InternalAuditLogTable } from '~/components/tables/internal-audit-log-table';
import type { Route } from './+types/audit-log'; import type { Route } from './+types/audit-log';
import auditLogStylesheet from './audit-log.print.css?url';
export const links: Route.LinksFunction = () => [
{ rel: 'stylesheet', href: appStylesheet },
{ rel: 'stylesheet', href: auditLogStylesheet },
];
export async function loader({ request }: Route.LoaderArgs) { export async function loader({ request }: Route.LoaderArgs) {
const d = new URL(request.url).searchParams.get('d'); const d = new URL(request.url).searchParams.get('d');
@ -76,8 +83,8 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
return ( return (
<div className="print-provider pointer-events-none mx-auto max-w-screen-md"> <div className="print-provider pointer-events-none mx-auto max-w-screen-md">
<div className="flex items-center"> <div className="mb-6 border-b pb-4">
<h1 className="my-8 text-2xl font-bold">{_(msg`Version History`)}</h1> <h1 className="text-xl font-semibold">{_(msg`Audit Log`)}</h1>
</div> </div>
<Card> <Card>
@ -157,11 +164,9 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
</CardContent> </CardContent>
</Card> </Card>
<Card className="mt-8"> <div className="mt-8">
<CardContent className="p-0"> <InternalAuditLogTable logs={auditLogs} />
<InternalAuditLogTable logs={auditLogs} /> </div>
</CardContent>
</Card>
<div className="my-8 flex-row-reverse"> <div className="my-8 flex-row-reverse">
<div className="flex items-end justify-end gap-x-4"> <div className="flex items-end justify-end gap-x-4">

View File

@ -9,6 +9,7 @@ import { signPdf } from '@documenso/signing';
import { AppError, AppErrorCode } from '../../../errors/app-error'; import { AppError, AppErrorCode } from '../../../errors/app-error';
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email'; import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client'; 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 { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
import { addRejectionStampToPdf } from '../../../server-only/pdf/add-rejection-stamp-to-pdf'; import { addRejectionStampToPdf } from '../../../server-only/pdf/add-rejection-stamp-to-pdf';
import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations'; import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
@ -145,7 +146,24 @@ export const run = async ({
? await getCertificatePdf({ ? await getCertificatePdf({
documentId, documentId,
language: document.documentMeta?.language, 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; : null;
const newDataId = await io.runTask('decorate-and-sign-pdf', async () => { 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) { for (const field of fields) {
if (field.inserted) { if (field.inserted) {
document.useLegacyFieldInsertion document.useLegacyFieldInsertion

View File

@ -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({ await tx.documentAuditLog.create({
data: createDocumentAuditLogData({ data: createDocumentAuditLogData({

View File

@ -17,6 +17,7 @@ import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getFileServerSide } from '../../universal/upload/get-file.server'; import { getFileServerSide } from '../../universal/upload/get-file.server';
import { putPdfFileServerSide } from '../../universal/upload/put-file.server'; import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers'; import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers';
import { getAuditLogsPdf } from '../htmltopdf/get-audit-logs-pdf';
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf'; import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
import { addRejectionStampToPdf } from '../pdf/add-rejection-stamp-to-pdf'; import { addRejectionStampToPdf } from '../pdf/add-rejection-stamp-to-pdf';
import { flattenAnnotations } from '../pdf/flatten-annotations'; import { flattenAnnotations } from '../pdf/flatten-annotations';
@ -125,6 +126,18 @@ export const sealDocument = async ({
}) })
: 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 doc = await PDFDocument.load(pdfData); const doc = await PDFDocument.load(pdfData);
// Normalize and flatten layers that could cause issues with the signature // 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) { for (const field of fields) {
document.useLegacyFieldInsertion document.useLegacyFieldInsertion
? await legacy_insertFieldInPDF(doc, field) ? await legacy_insertFieldInPDF(doc, field)

View File

@ -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;
};

View File

@ -72,6 +72,7 @@ export const getCertificatePdf = async ({ documentId, language }: GetCertificate
const result = await page.pdf({ const result = await page.pdf({
format: 'A4', format: 'A4',
printBackground: true,
}); });
await browserContext.close(); await browserContext.close();

View File

@ -120,6 +120,7 @@ export const generateDefaultOrganisationSettings = (): Omit<
includeSenderDetails: true, includeSenderDetails: true,
includeSigningCertificate: true, includeSigningCertificate: true,
includeAuditLog: false,
typedSignatureEnabled: true, typedSignatureEnabled: true,
uploadSignatureEnabled: true, uploadSignatureEnabled: true,

View File

@ -170,6 +170,7 @@ export const generateDefaultTeamSettings = (): Omit<TeamGlobalSettings, 'id' | '
includeSenderDetails: null, includeSenderDetails: null,
includeSigningCertificate: null, includeSigningCertificate: null,
includeAuditLog: null,
typedSignatureEnabled: null, typedSignatureEnabled: null,
uploadSignatureEnabled: null, uploadSignatureEnabled: null,

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "OrganisationGlobalSettings" ADD COLUMN "includeAuditLog" BOOLEAN NOT NULL DEFAULT false;
-- AlterTable
ALTER TABLE "TeamGlobalSettings" ADD COLUMN "includeAuditLog" BOOLEAN;

View File

@ -734,13 +734,13 @@ model OrganisationGlobalSettings {
id String @id id String @id
organisation Organisation? organisation Organisation?
documentVisibility DocumentVisibility @default(EVERYONE) documentVisibility DocumentVisibility @default(EVERYONE)
documentLanguage String @default("en") documentLanguage String @default("en")
documentTimezone String? // Nullable to allow using local timezones if not set. includeSenderDetails Boolean @default(true)
documentDateFormat String @default("yyyy-MM-dd hh:mm a") includeSigningCertificate Boolean @default(true)
includeAuditLog Boolean @default(false)
includeSenderDetails Boolean @default(true) documentTimezone String? // Nullable to allow using local timezones if not set.
includeSigningCertificate Boolean @default(true) documentDateFormat String @default("yyyy-MM-dd hh:mm a")
typedSignatureEnabled Boolean @default(true) typedSignatureEnabled Boolean @default(true)
uploadSignatureEnabled Boolean @default(true) uploadSignatureEnabled Boolean @default(true)
@ -771,6 +771,7 @@ model TeamGlobalSettings {
includeSenderDetails Boolean? includeSenderDetails Boolean?
includeSigningCertificate Boolean? includeSigningCertificate Boolean?
includeAuditLog Boolean?
typedSignatureEnabled Boolean? typedSignatureEnabled Boolean?
uploadSignatureEnabled Boolean? uploadSignatureEnabled Boolean?

View File

@ -30,6 +30,7 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
documentDateFormat, documentDateFormat,
includeSenderDetails, includeSenderDetails,
includeSigningCertificate, includeSigningCertificate,
includeAuditLog,
typedSignatureEnabled, typedSignatureEnabled,
uploadSignatureEnabled, uploadSignatureEnabled,
drawSignatureEnabled, drawSignatureEnabled,
@ -117,6 +118,7 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
documentDateFormat, documentDateFormat,
includeSenderDetails, includeSenderDetails,
includeSigningCertificate, includeSigningCertificate,
includeAuditLog,
typedSignatureEnabled, typedSignatureEnabled,
uploadSignatureEnabled, uploadSignatureEnabled,
drawSignatureEnabled, drawSignatureEnabled,

View File

@ -19,6 +19,7 @@ export const ZUpdateOrganisationSettingsRequestSchema = z.object({
documentDateFormat: ZDocumentMetaDateFormatSchema.optional(), documentDateFormat: ZDocumentMetaDateFormatSchema.optional(),
includeSenderDetails: z.boolean().optional(), includeSenderDetails: z.boolean().optional(),
includeSigningCertificate: z.boolean().optional(), includeSigningCertificate: z.boolean().optional(),
includeAuditLog: z.boolean().optional(),
typedSignatureEnabled: z.boolean().optional(), typedSignatureEnabled: z.boolean().optional(),
uploadSignatureEnabled: z.boolean().optional(), uploadSignatureEnabled: z.boolean().optional(),
drawSignatureEnabled: z.boolean().optional(), drawSignatureEnabled: z.boolean().optional(),

View File

@ -32,6 +32,7 @@ export const updateTeamSettingsRoute = authenticatedProcedure
documentDateFormat, documentDateFormat,
includeSenderDetails, includeSenderDetails,
includeSigningCertificate, includeSigningCertificate,
includeAuditLog,
typedSignatureEnabled, typedSignatureEnabled,
uploadSignatureEnabled, uploadSignatureEnabled,
drawSignatureEnabled, drawSignatureEnabled,
@ -110,6 +111,7 @@ export const updateTeamSettingsRoute = authenticatedProcedure
documentDateFormat, documentDateFormat,
includeSenderDetails, includeSenderDetails,
includeSigningCertificate, includeSigningCertificate,
includeAuditLog,
typedSignatureEnabled, typedSignatureEnabled,
uploadSignatureEnabled, uploadSignatureEnabled,
drawSignatureEnabled, drawSignatureEnabled,

View File

@ -23,6 +23,7 @@ export const ZUpdateTeamSettingsRequestSchema = z.object({
documentDateFormat: ZDocumentMetaDateFormatSchema.nullish(), documentDateFormat: ZDocumentMetaDateFormatSchema.nullish(),
includeSenderDetails: z.boolean().nullish(), includeSenderDetails: z.boolean().nullish(),
includeSigningCertificate: z.boolean().nullish(), includeSigningCertificate: z.boolean().nullish(),
includeAuditLog: z.boolean().nullish(),
typedSignatureEnabled: z.boolean().nullish(), typedSignatureEnabled: z.boolean().nullish(),
uploadSignatureEnabled: z.boolean().nullish(), uploadSignatureEnabled: z.boolean().nullish(),
drawSignatureEnabled: z.boolean().nullish(), drawSignatureEnabled: z.boolean().nullish(),