mirror of
https://github.com/documenso/documenso.git
synced 2026-07-24 17:04:12 +10:00
Merge branch 'main' into improve-lint
This commit is contained in:
@@ -1,11 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useTheme } from 'next-themes';
|
||||
import SwaggerUI from 'swagger-ui-react';
|
||||
import 'swagger-ui-react/swagger-ui.css';
|
||||
|
||||
import { OpenAPIV1 } from '@documenso/api/v1/openapi';
|
||||
|
||||
export const OpenApiDocsPage = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const body = document.body;
|
||||
|
||||
if (resolvedTheme === 'dark') {
|
||||
body.classList.add('swagger-dark-theme');
|
||||
} else {
|
||||
body.classList.remove('swagger-dark-theme');
|
||||
}
|
||||
|
||||
return () => {
|
||||
body.classList.remove('swagger-dark-theme');
|
||||
};
|
||||
}, [resolvedTheme]);
|
||||
|
||||
return <SwaggerUI spec={OpenAPIV1} displayOperationId={true} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ test('delete user', async ({ page }) => {
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Delete Account' }).click();
|
||||
await page.getByLabel('Confirm Email').fill(user.email);
|
||||
await expect(page.getByRole('button', { name: 'Confirm Deletion' })).not.toBeDisabled();
|
||||
await page.getByRole('button', { name: 'Confirm Deletion' }).click();
|
||||
|
||||
await page.waitForURL(`${WEBAPP_BASE_URL}/signin`);
|
||||
|
||||
@@ -17,7 +17,15 @@ module.exports = {
|
||||
|
||||
parser: '@typescript-eslint/parser',
|
||||
|
||||
parserOptions: { project: true },
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['../../tsconfig.eslint.json'],
|
||||
ecmaVersion: 2022,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
sourceType: 'module',
|
||||
},
|
||||
|
||||
rules: {
|
||||
'@next/next/no-html-link-for-pages': 'off',
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const URL_REGEX =
|
||||
/^(https?):\/\/(?:www\.)?[a-zA-Z0-9-]+\.[a-zA-Z0-9()]{2,}(?:\/[a-zA-Z0-9-._?&=/]*)?$/i;
|
||||
/^(https?):\/\/(?:www\.)?(?:[a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z0-9()]{2,}(?:\/[a-zA-Z0-9-._?&=/]*)?$/i;
|
||||
|
||||
@@ -24,6 +24,13 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
include: {
|
||||
documentData: true,
|
||||
Recipient: true,
|
||||
User: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -35,61 +42,116 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
const buffer = await getFile(document.documentData);
|
||||
const { User: owner } = document;
|
||||
|
||||
const completedDocument = await getFile(document.documentData);
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
let documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/documents/${document.id}`;
|
||||
|
||||
if (document.team?.url) {
|
||||
documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/t/${document.team.url}/documents/${
|
||||
document.id
|
||||
}`;
|
||||
}
|
||||
|
||||
// If the document owner is not a recipient then send the email to them separately
|
||||
if (!document.Recipient.find((recipient) => recipient.email === owner.email)) {
|
||||
const template = createElement(DocumentCompletedEmailTemplate, {
|
||||
documentName: document.title,
|
||||
assetBaseUrl,
|
||||
downloadLink: documentOwnerDownloadLink,
|
||||
});
|
||||
|
||||
await mailer.sendMail({
|
||||
to: [
|
||||
{
|
||||
name: owner.name || '',
|
||||
address: owner.email,
|
||||
},
|
||||
],
|
||||
from: {
|
||||
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
||||
},
|
||||
subject: 'Signing Complete!',
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
attachments: [
|
||||
{
|
||||
filename: document.title,
|
||||
content: Buffer.from(completedDocument),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
documentId: document.id,
|
||||
user: null,
|
||||
requestMetadata,
|
||||
data: {
|
||||
emailType: 'DOCUMENT_COMPLETED',
|
||||
recipientEmail: owner.email,
|
||||
recipientName: owner.name,
|
||||
recipientId: owner.id,
|
||||
recipientRole: 'OWNER',
|
||||
isResending: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
document.Recipient.map(async (recipient) => {
|
||||
const { email, name, token } = recipient;
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const downloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}/complete`;
|
||||
|
||||
const template = createElement(DocumentCompletedEmailTemplate, {
|
||||
documentName: document.title,
|
||||
assetBaseUrl,
|
||||
downloadLink: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${token}/complete`,
|
||||
downloadLink: recipient.email === owner.email ? documentOwnerDownloadLink : downloadLink,
|
||||
});
|
||||
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: email,
|
||||
name,
|
||||
},
|
||||
from: {
|
||||
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
||||
},
|
||||
subject: 'Signing Complete!',
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
attachments: [
|
||||
{
|
||||
filename: document.title,
|
||||
content: Buffer.from(buffer),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
documentId: document.id,
|
||||
user: null,
|
||||
requestMetadata,
|
||||
data: {
|
||||
emailType: 'DOCUMENT_COMPLETED',
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientId: recipient.id,
|
||||
recipientRole: recipient.role,
|
||||
isResending: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
await mailer.sendMail({
|
||||
to: [
|
||||
{
|
||||
name: recipient.name,
|
||||
address: recipient.email,
|
||||
},
|
||||
],
|
||||
from: {
|
||||
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
subject: 'Signing Complete!',
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
attachments: [
|
||||
{
|
||||
filename: document.title,
|
||||
content: Buffer.from(completedDocument),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
documentId: document.id,
|
||||
user: null,
|
||||
requestMetadata,
|
||||
data: {
|
||||
emailType: 'DOCUMENT_COMPLETED',
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientId: recipient.id,
|
||||
recipientRole: recipient.role,
|
||||
isResending: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -37,6 +37,12 @@ export const findTemplates = async ({
|
||||
where: whereFilter,
|
||||
include: {
|
||||
templateDocumentData: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
Field: true,
|
||||
Recipient: true,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// We use stars as a placeholder since it's easy to find and replace,
|
||||
// the length of the placeholder is to support larger pdf files
|
||||
export const BYTE_RANGE_PLACEHOLDER = '**********';
|
||||
+5
-4
@@ -1,4 +1,3 @@
|
||||
import signer from 'node-signpdf';
|
||||
import {
|
||||
PDFArray,
|
||||
PDFDocument,
|
||||
@@ -9,6 +8,8 @@ import {
|
||||
rectangle,
|
||||
} from 'pdf-lib';
|
||||
|
||||
import { BYTE_RANGE_PLACEHOLDER } from '../constants/byte-range';
|
||||
|
||||
export type AddSigningPlaceholderOptions = {
|
||||
pdf: Buffer;
|
||||
};
|
||||
@@ -20,9 +21,9 @@ export const addSigningPlaceholder = async ({ pdf }: AddSigningPlaceholderOption
|
||||
const byteRange = PDFArray.withContext(doc.context);
|
||||
|
||||
byteRange.push(PDFNumber.of(0));
|
||||
byteRange.push(PDFName.of(signer.byteRangePlaceholder));
|
||||
byteRange.push(PDFName.of(signer.byteRangePlaceholder));
|
||||
byteRange.push(PDFName.of(signer.byteRangePlaceholder));
|
||||
byteRange.push(PDFName.of(BYTE_RANGE_PLACEHOLDER));
|
||||
byteRange.push(PDFName.of(BYTE_RANGE_PLACEHOLDER));
|
||||
byteRange.push(PDFName.of(BYTE_RANGE_PLACEHOLDER));
|
||||
|
||||
const signature = doc.context.obj({
|
||||
Type: 'Sig',
|
||||
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { updateSigningPlaceholder } from './update-signing-placeholder';
|
||||
|
||||
describe('updateSigningPlaceholder', () => {
|
||||
const pdf = Buffer.from(`
|
||||
20 0 obj
|
||||
<<
|
||||
/Type /Sig
|
||||
/Filter /Adobe.PPKLite
|
||||
/SubFilter /adbe.pkcs7.detached
|
||||
/ByteRange [ 0 /********** /********** /********** ]
|
||||
/Contents <0000000000000000000000000000000000000000000000000000000>
|
||||
/Reason (Signed by Documenso)
|
||||
/M (D:20210101000000Z)
|
||||
>>
|
||||
endobj
|
||||
`);
|
||||
|
||||
it('should not throw an error', () => {
|
||||
expect(() => updateSigningPlaceholder({ pdf })).not.toThrowError();
|
||||
});
|
||||
|
||||
it('should not modify the original PDF', () => {
|
||||
const result = updateSigningPlaceholder({ pdf });
|
||||
|
||||
expect(result.pdf).not.toEqual(pdf);
|
||||
});
|
||||
|
||||
it('should return a PDF with the same length as the original', () => {
|
||||
const result = updateSigningPlaceholder({ pdf });
|
||||
|
||||
expect(result.pdf).toHaveLength(pdf.length);
|
||||
});
|
||||
|
||||
it('should update the byte range and return it', () => {
|
||||
const result = updateSigningPlaceholder({ pdf });
|
||||
|
||||
expect(result.byteRange).toEqual([0, 184, 241, 92]);
|
||||
});
|
||||
|
||||
it('should only update the last signature in the PDF', () => {
|
||||
const pdf = Buffer.from(`
|
||||
20 0 obj
|
||||
<<
|
||||
/Type /Sig
|
||||
/Filter /Adobe.PPKLite
|
||||
/SubFilter /adbe.pkcs7.detached
|
||||
/ByteRange [ 0 /********** /********** /********** ]
|
||||
/Contents <0000000000000000000000000000000000000000000000000000000>
|
||||
/Reason (Signed by Documenso)
|
||||
/M (D:20210101000000Z)
|
||||
>>
|
||||
endobj
|
||||
21 0 obj
|
||||
<<
|
||||
/Type /Sig
|
||||
/Filter /Adobe.PPKLite
|
||||
/SubFilter /adbe.pkcs7.detached
|
||||
/ByteRange [ 0 /********** /********** /********** ]
|
||||
/Contents <0000000000000000000000000000000000000000000000000000000>
|
||||
/Reason (Signed by Documenso)
|
||||
/M (D:20210101000000Z)
|
||||
>>
|
||||
endobj
|
||||
`);
|
||||
|
||||
const result = updateSigningPlaceholder({ pdf });
|
||||
|
||||
expect(result.byteRange).toEqual([0, 512, 569, 92]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
export type UpdateSigningPlaceholderOptions = {
|
||||
pdf: Buffer;
|
||||
};
|
||||
|
||||
export const updateSigningPlaceholder = ({ pdf }: UpdateSigningPlaceholderOptions) => {
|
||||
const length = pdf.length;
|
||||
|
||||
const byteRangePos = pdf.lastIndexOf('/ByteRange');
|
||||
const byteRangeStart = pdf.indexOf('[', byteRangePos);
|
||||
const byteRangeEnd = pdf.indexOf(']', byteRangePos);
|
||||
|
||||
const byteRangeSlice = pdf.subarray(byteRangeStart, byteRangeEnd + 1);
|
||||
|
||||
const signaturePos = pdf.indexOf('/Contents', byteRangeEnd);
|
||||
const signatureStart = pdf.indexOf('<', signaturePos);
|
||||
const signatureEnd = pdf.indexOf('>', signaturePos);
|
||||
|
||||
const signatureSlice = pdf.subarray(signatureStart, signatureEnd + 1);
|
||||
|
||||
const byteRange = [0, 0, 0, 0];
|
||||
|
||||
byteRange[1] = signatureStart;
|
||||
byteRange[2] = byteRange[1] + signatureSlice.length;
|
||||
byteRange[3] = length - byteRange[2];
|
||||
|
||||
const newByteRange = `[${byteRange.join(' ')}]`.padEnd(byteRangeSlice.length, ' ');
|
||||
|
||||
const updatedPdf = Buffer.concat([
|
||||
pdf.subarray(0, byteRangeStart),
|
||||
Buffer.from(newByteRange),
|
||||
pdf.subarray(byteRangeEnd + 1),
|
||||
]);
|
||||
|
||||
if (updatedPdf.length !== length) {
|
||||
throw new Error('Updated PDF length does not match original length');
|
||||
}
|
||||
|
||||
return { pdf: updatedPdf, byteRange };
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { signWithGoogleCloudHSM } from './transports/google-cloud-hsm';
|
||||
import { signWithLocalCert } from './transports/local-cert';
|
||||
|
||||
export type SignOptions = {
|
||||
@@ -11,6 +12,7 @@ export const signPdf = async ({ pdf }: SignOptions) => {
|
||||
|
||||
return await match(transport)
|
||||
.with('local', async () => signWithLocalCert({ pdf }))
|
||||
.with('gcloud-hsm', async () => signWithGoogleCloudHSM({ pdf }))
|
||||
.otherwise(() => {
|
||||
throw new Error(`Unsupported signing transport: ${transport}`);
|
||||
});
|
||||
|
||||
@@ -9,15 +9,15 @@
|
||||
"index.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@documenso/tsconfig": "*",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-signpdf": "^2.0.0",
|
||||
"@documenso/pdf-sign": "^0.1.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"ts-pattern": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node-forge": "^1.3.4"
|
||||
"vitest": "^1.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import fs from 'node:fs';
|
||||
|
||||
import { signWithGCloud } from '@documenso/pdf-sign';
|
||||
|
||||
import { addSigningPlaceholder } from '../helpers/add-signing-placeholder';
|
||||
import { updateSigningPlaceholder } from '../helpers/update-signing-placeholder';
|
||||
|
||||
export type SignWithGoogleCloudHSMOptions = {
|
||||
pdf: Buffer;
|
||||
};
|
||||
|
||||
export const signWithGoogleCloudHSM = async ({ pdf }: SignWithGoogleCloudHSMOptions) => {
|
||||
const keyPath = process.env.NEXT_PRIVATE_SIGNING_GCLOUD_HSM_KEY_PATH;
|
||||
|
||||
if (!keyPath) {
|
||||
throw new Error('No certificate path provided for Google Cloud HSM signing');
|
||||
}
|
||||
|
||||
// To handle hosting in serverless environments like Vercel we can supply the base64 encoded
|
||||
// application credentials as an environment variable and write it to a file if it doesn't exist
|
||||
if (
|
||||
process.env.GOOGLE_APPLICATION_CREDENTIALS &&
|
||||
process.env.NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS
|
||||
) {
|
||||
if (!fs.existsSync(process.env.GOOGLE_APPLICATION_CREDENTIALS)) {
|
||||
fs.writeFileSync(
|
||||
process.env.GOOGLE_APPLICATION_CREDENTIALS,
|
||||
Buffer.from(
|
||||
process.env.NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS,
|
||||
'base64',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const { pdf: pdfWithPlaceholder, byteRange } = updateSigningPlaceholder({
|
||||
pdf: await addSigningPlaceholder({ pdf }),
|
||||
});
|
||||
|
||||
const pdfWithoutSignature = Buffer.concat([
|
||||
pdfWithPlaceholder.subarray(0, byteRange[1]),
|
||||
pdfWithPlaceholder.subarray(byteRange[2]),
|
||||
]);
|
||||
|
||||
const signatureLength = byteRange[2] - byteRange[1];
|
||||
|
||||
let cert: Buffer | null = null;
|
||||
|
||||
if (process.env.NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_CONTENTS) {
|
||||
cert = Buffer.from(
|
||||
process.env.NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_CONTENTS,
|
||||
'base64',
|
||||
);
|
||||
}
|
||||
|
||||
if (!cert) {
|
||||
cert = Buffer.from(
|
||||
fs.readFileSync(
|
||||
process.env.NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_PATH || './example/cert.crt',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const signature = signWithGCloud({
|
||||
keyPath,
|
||||
cert,
|
||||
content: pdfWithoutSignature,
|
||||
});
|
||||
|
||||
const signatureAsHex = signature.toString('hex');
|
||||
|
||||
const signedPdf = Buffer.concat([
|
||||
pdfWithPlaceholder.subarray(0, byteRange[1]),
|
||||
Buffer.from(`<${signatureAsHex.padEnd(signatureLength - 2, '0')}>`),
|
||||
pdfWithPlaceholder.subarray(byteRange[2]),
|
||||
]);
|
||||
|
||||
return signedPdf;
|
||||
};
|
||||
@@ -1,32 +1,51 @@
|
||||
import signer from 'node-signpdf';
|
||||
import fs from 'node:fs';
|
||||
|
||||
import { addSigningPlaceholder } from '../helpers/addSigningPlaceholder';
|
||||
import { signWithP12 } from '@documenso/pdf-sign';
|
||||
|
||||
import { addSigningPlaceholder } from '../helpers/add-signing-placeholder';
|
||||
import { updateSigningPlaceholder } from '../helpers/update-signing-placeholder';
|
||||
|
||||
export type SignWithLocalCertOptions = {
|
||||
pdf: Buffer;
|
||||
};
|
||||
|
||||
export const signWithLocalCert = async ({ pdf }: SignWithLocalCertOptions) => {
|
||||
const pdfWithPlaceholder = await addSigningPlaceholder({ pdf });
|
||||
const { pdf: pdfWithPlaceholder, byteRange } = updateSigningPlaceholder({
|
||||
pdf: await addSigningPlaceholder({ pdf }),
|
||||
});
|
||||
|
||||
let p12Cert: Buffer | null = null;
|
||||
const pdfWithoutSignature = Buffer.concat([
|
||||
pdfWithPlaceholder.subarray(0, byteRange[1]),
|
||||
pdfWithPlaceholder.subarray(byteRange[2]),
|
||||
]);
|
||||
|
||||
const signatureLength = byteRange[2] - byteRange[1];
|
||||
|
||||
let cert: Buffer | null = null;
|
||||
|
||||
if (process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS) {
|
||||
p12Cert = Buffer.from(process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS, 'base64');
|
||||
cert = Buffer.from(process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS, 'base64');
|
||||
}
|
||||
|
||||
if (!p12Cert) {
|
||||
p12Cert = Buffer.from(
|
||||
if (!cert) {
|
||||
cert = Buffer.from(
|
||||
fs.readFileSync(process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH || './example/cert.p12'),
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.NEXT_PRIVATE_SIGNING_PASSPHRASE) {
|
||||
return signer.sign(pdfWithPlaceholder, p12Cert, {
|
||||
passphrase: process.env.NEXT_PRIVATE_SIGNING_PASSPHRASE,
|
||||
});
|
||||
}
|
||||
const signature = signWithP12({
|
||||
cert,
|
||||
content: pdfWithoutSignature,
|
||||
password: process.env.NEXT_PRIVATE_SIGNING_PASSPHRASE || undefined,
|
||||
});
|
||||
|
||||
return signer.sign(pdfWithPlaceholder, p12Cert);
|
||||
const signatureAsHex = signature.toString('hex');
|
||||
|
||||
const signedPdf = Buffer.concat([
|
||||
pdfWithPlaceholder.subarray(0, byteRange[1]),
|
||||
Buffer.from(`<${signatureAsHex.padEnd(signatureLength - 2, '0')}>`),
|
||||
pdfWithPlaceholder.subarray(byteRange[2]),
|
||||
]);
|
||||
|
||||
return signedPdf;
|
||||
};
|
||||
|
||||
Vendored
+4
@@ -30,6 +30,10 @@ declare namespace NodeJS {
|
||||
NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH?: string;
|
||||
NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS?: string;
|
||||
NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING?: string;
|
||||
NEXT_PRIVATE_SIGNING_GCLOUD_HSM_KEY_PATH?: string;
|
||||
NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_PATH?: string;
|
||||
NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_CONTENTS?: string;
|
||||
NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS?: string;
|
||||
|
||||
NEXT_PRIVATE_SMTP_TRANSPORT?: 'mailchannels' | 'resend' | 'smtp-auth' | 'smtp-api';
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ export function DataTablePagination<TData>({
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-x-6 gap-y-4 lg:gap-x-8">
|
||||
<div className="flex items-center text-sm font-medium md:justify-center">
|
||||
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
|
||||
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount() || 1}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-x-2">
|
||||
|
||||
@@ -115,7 +115,12 @@ export function DataTable<TData, TValue>({
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
style={{
|
||||
width: `${cell.column.getSize()}px`,
|
||||
}}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
|
||||
@@ -79,7 +79,7 @@ const ToastClose = React.forwardRef<
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-foreground/50 hover:text-foreground absolute right-2 top-2 rounded-md p-1 opacity-0 transition-opacity focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
|
||||
'text-foreground/50 hover:text-foreground absolute right-2 top-2 rounded-md p-1 opacity-100 transition-opacity focus:opacity-100 focus:outline-none focus:ring-2 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 md:opacity-0 group-hover:md:opacity-100',
|
||||
className,
|
||||
)}
|
||||
toast-close=""
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
--ring: 95.08 71.08% 67.45%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
|
||||
--warning: 54 96% 45%;
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,11 @@
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings: 'rlig' 1, 'calt' 1;
|
||||
@@ -124,3 +129,12 @@
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgb(100 116 139 / 0.5);
|
||||
}
|
||||
|
||||
/* Custom Swagger Dark Theme */
|
||||
.swagger-dark-theme .swagger-ui {
|
||||
filter: invert(88%) hue-rotate(180deg);
|
||||
}
|
||||
|
||||
.swagger-dark-theme .swagger-ui .microlight {
|
||||
filter: invert(100%) hue-rotate(180deg);
|
||||
}
|
||||
Reference in New Issue
Block a user