mirror of
https://github.com/documenso/documenso.git
synced 2026-10-02 09:44:32 +10:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
584d5bd8ea | ||
|
|
e4b6d42672 | ||
|
|
78498793fa | ||
|
|
b2e916378d | ||
|
|
a52c19355a | ||
|
|
e67e96333b | ||
|
|
7aa75b0c64 | ||
|
|
5615627001 | ||
|
|
78c55875ef |
Binary file not shown.
@@ -39,7 +39,7 @@ export default function ContentPage({ params }: { params: { content: string } })
|
||||
const MDXContent = useMDXComponent(post.body.code);
|
||||
|
||||
return (
|
||||
<article className="prose prose-slate mx-auto">
|
||||
<article className="prose dark:prose-invert mx-auto">
|
||||
<MDXContent components={mdxComponents} />
|
||||
</article>
|
||||
);
|
||||
|
||||
Binary file not shown.
@@ -1,11 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { HTMLAttributes } from 'react';
|
||||
import { HTMLAttributes, useState } from 'react';
|
||||
|
||||
import { Share } from 'lucide-react';
|
||||
import { Copy, Share, Twitter } from 'lucide-react';
|
||||
|
||||
import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
|
||||
@@ -19,14 +28,36 @@ export const ShareButton = ({ token, documentId }: ShareButtonProps) => {
|
||||
const { toast } = useToast();
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const { mutateAsync: createOrGetShareLink, isLoading } =
|
||||
trpc.shareLink.createOrGetShareLink.useMutation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const onShareClick = async () => {
|
||||
const { slug } = await createOrGetShareLink({
|
||||
token: token,
|
||||
documentId,
|
||||
});
|
||||
const {
|
||||
mutateAsync: createOrGetShareLink,
|
||||
data: shareLink,
|
||||
isLoading,
|
||||
} = trpc.shareLink.createOrGetShareLink.useMutation();
|
||||
|
||||
const onOpenChange = (nextOpen: boolean) => {
|
||||
if (nextOpen) {
|
||||
void createOrGetShareLink({
|
||||
token,
|
||||
documentId,
|
||||
});
|
||||
}
|
||||
|
||||
setIsOpen(nextOpen);
|
||||
};
|
||||
|
||||
const onCopyClick = async () => {
|
||||
let { slug = '' } = shareLink || {};
|
||||
|
||||
if (!slug) {
|
||||
const result = await createOrGetShareLink({
|
||||
token,
|
||||
documentId,
|
||||
});
|
||||
|
||||
slug = result.slug;
|
||||
}
|
||||
|
||||
await copyToClipboard(`${window.location.origin}/share/${slug}`).catch(() => null);
|
||||
|
||||
@@ -34,18 +65,82 @@ export const ShareButton = ({ token, documentId }: ShareButtonProps) => {
|
||||
title: 'Copied to clipboard',
|
||||
description: 'The sharing link has been copied to your clipboard.',
|
||||
});
|
||||
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const onTweetClick = async () => {
|
||||
let { slug = '' } = shareLink || {};
|
||||
|
||||
if (!slug) {
|
||||
const result = await createOrGetShareLink({
|
||||
token,
|
||||
documentId,
|
||||
});
|
||||
|
||||
slug = result.slug;
|
||||
}
|
||||
|
||||
window.open(
|
||||
generateTwitterIntent(
|
||||
`I just ${token ? 'signed' : 'sent'} a document with @documenso. Check it out!`,
|
||||
`${window.location.origin}/share/${slug}`,
|
||||
),
|
||||
'_blank',
|
||||
);
|
||||
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
disabled={!token || !documentId}
|
||||
loading={isLoading}
|
||||
onClick={onShareClick}
|
||||
>
|
||||
{!isLoading && <Share className="mr-2 h-5 w-5" />}
|
||||
Share
|
||||
</Button>
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={!token || !documentId}
|
||||
className="flex-1"
|
||||
loading={isLoading}
|
||||
>
|
||||
{!isLoading && <Share className="mr-2 h-5 w-5" />}
|
||||
Share
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Share</DialogTitle>
|
||||
|
||||
<DialogDescription className="mt-4">Share your signing experience!</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="rounded-md border p-4">
|
||||
I just {token ? 'signed' : 'sent'} a document with{' '}
|
||||
<span className="font-medium text-blue-400">@documenso</span>
|
||||
. Check it out!
|
||||
<span className="mt-2 block" />
|
||||
<span className="font-medium text-blue-400">
|
||||
{window.location.origin}/share/{shareLink?.slug || '...'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" className="mt-4" onClick={onTweetClick}>
|
||||
<Twitter className="mr-2 h-4 w-4" />
|
||||
Tweet
|
||||
</Button>
|
||||
|
||||
<div className="relative flex items-center justify-center gap-x-4 py-4 text-xs uppercase">
|
||||
<div className="bg-border h-px flex-1" />
|
||||
<span className="text-muted-foreground bg-transparent">Or</span>
|
||||
<div className="bg-border h-px flex-1" />
|
||||
</div>
|
||||
|
||||
<Button variant="outline" onClick={onCopyClick}>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy Link
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
Generated
+53
@@ -1876,6 +1876,10 @@
|
||||
"resolved": "packages/prisma",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@documenso/signing": {
|
||||
"resolved": "packages/signing",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@documenso/tailwind-config": {
|
||||
"resolved": "packages/tailwind-config",
|
||||
"link": true
|
||||
@@ -6423,6 +6427,15 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.0.tgz",
|
||||
"integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A=="
|
||||
},
|
||||
"node_modules/@types/node-forge": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.4.tgz",
|
||||
"integrity": "sha512-08scBQriFsBbm/CuBWOXRMD1RG7ydFW01EDR6vGX27nxcj6E/jGSCOLdICNd8ETwQlLFXVBVA854RX6Y7vPSrQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/nodemailer": {
|
||||
"version": "6.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.9.tgz",
|
||||
@@ -14370,11 +14383,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||
"engines": {
|
||||
"node": ">= 6.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.13",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
|
||||
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
|
||||
},
|
||||
"node_modules/node-signpdf": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-signpdf/-/node-signpdf-2.0.0.tgz",
|
||||
"integrity": "sha512-B6fDvD8z2v0pRntQjgPO2ARqxh0pfNwfSn6YEbP7cv6xmPgcphFwcrMA3N47LztmpVbAM3vUFUslX32L6NRYDg==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"node-forge": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/node-sql-parser": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/node-sql-parser/-/node-sql-parser-4.9.0.tgz",
|
||||
@@ -19777,6 +19809,7 @@
|
||||
"@aws-sdk/signature-v4-crt": "^3.410.0",
|
||||
"@documenso/email": "*",
|
||||
"@documenso/prisma": "*",
|
||||
"@documenso/signing": "*",
|
||||
"@next-auth/prisma-adapter": "1.0.7",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@scure/base": "^1.1.3",
|
||||
@@ -19827,6 +19860,26 @@
|
||||
"typescript": "^5.1.6"
|
||||
}
|
||||
},
|
||||
"packages/signing": {
|
||||
"name": "@documenso/signing",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPLv3",
|
||||
"dependencies": {
|
||||
"@documenso/tsconfig": "*",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-signpdf": "^2.0.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"ts-pattern": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node-forge": "^1.3.4"
|
||||
}
|
||||
},
|
||||
"packages/signing/node_modules/ts-pattern": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.0.5.tgz",
|
||||
"integrity": "sha512-tL0w8U/pgaacOmkb9fRlYzWEUDCfVjjv9dD4wHTgZ61MjhuMt46VNWTG747NqW6vRzoWIKABVhFSOJ82FvXrfA=="
|
||||
},
|
||||
"packages/tailwind-config": {
|
||||
"name": "@documenso/tailwind-config",
|
||||
"version": "0.0.0",
|
||||
|
||||
@@ -17,7 +17,7 @@ export const TemplateFooter = ({ isDocument = true }: TemplateFooterProps) => {
|
||||
)}
|
||||
|
||||
<Text className="my-8 text-sm text-slate-400">
|
||||
Documenso
|
||||
Documenso, Inc.
|
||||
<br />
|
||||
2261 Market Street, #5211, San Francisco, CA 94114, USA
|
||||
</Text>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"@aws-sdk/signature-v4-crt": "^3.410.0",
|
||||
"@documenso/email": "*",
|
||||
"@documenso/prisma": "*",
|
||||
"@documenso/signing": "*",
|
||||
"@next-auth/prisma-adapter": "1.0.7",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@scure/base": "^1.1.3",
|
||||
@@ -38,4 +39,4 @@
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/luxon": "^3.3.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { PDFDocument } from 'pdf-lib';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
import { signPdf } from '@documenso/signing';
|
||||
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { putFile } from '../../universal/upload/put-file';
|
||||
@@ -71,12 +72,14 @@ export const sealDocument = async ({ documentId }: SealDocumentOptions) => {
|
||||
|
||||
const pdfBytes = await doc.save();
|
||||
|
||||
const pdfBuffer = await signPdf({ pdf: Buffer.from(pdfBytes) });
|
||||
|
||||
const { name, ext } = path.parse(document.title);
|
||||
|
||||
const { data: newData } = await putFile({
|
||||
name: `${name}_signed${ext}`,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(Buffer.from(pdfBytes)),
|
||||
arrayBuffer: async () => Promise.resolve(pdfBuffer),
|
||||
});
|
||||
|
||||
await prisma.documentData.update({
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export const generateTwitterIntent = (text: string, shareUrl: string) => {
|
||||
return `https://twitter.com/intent/tweet?text=${encodeURIComponent(
|
||||
text,
|
||||
)}%0A%0A${encodeURIComponent(shareUrl)}`;
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import signer from 'node-signpdf';
|
||||
import { PDFArray, PDFDocument, PDFHexString, PDFName, PDFNumber, PDFString } from 'pdf-lib';
|
||||
|
||||
export type AddSigningPlaceholderOptions = {
|
||||
pdf: Buffer;
|
||||
};
|
||||
|
||||
export const addSigningPlaceholder = async ({ pdf }: AddSigningPlaceholderOptions) => {
|
||||
const doc = await PDFDocument.load(pdf);
|
||||
const pages = doc.getPages();
|
||||
|
||||
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));
|
||||
|
||||
const signature = doc.context.obj({
|
||||
Type: 'Sig',
|
||||
Filter: 'Adobe.PPKLite',
|
||||
SubFilter: 'adbe.pkcs7.detached',
|
||||
ByteRange: byteRange,
|
||||
Contents: PDFHexString.fromText(' '.repeat(8192)),
|
||||
Reason: PDFString.of('Signed by Documenso'),
|
||||
M: PDFString.fromDate(new Date()),
|
||||
});
|
||||
|
||||
const signatureRef = doc.context.register(signature);
|
||||
|
||||
const widget = doc.context.obj({
|
||||
Type: 'Annot',
|
||||
Subtype: 'Widget',
|
||||
FT: 'Sig',
|
||||
Rect: [0, 0, 0, 0],
|
||||
V: signatureRef,
|
||||
T: PDFString.of('Signature1'),
|
||||
F: 4,
|
||||
P: pages[0].ref,
|
||||
});
|
||||
|
||||
const widgetRef = doc.context.register(widget);
|
||||
|
||||
let widgets = pages[0].node.get(PDFName.of('Annots'));
|
||||
|
||||
if (widgets instanceof PDFArray) {
|
||||
widgets.push(widgetRef);
|
||||
} else {
|
||||
const newWidgets = PDFArray.withContext(doc.context);
|
||||
|
||||
newWidgets.push(widgetRef);
|
||||
|
||||
pages[0].node.set(PDFName.of('Annots'), newWidgets);
|
||||
|
||||
widgets = pages[0].node.get(PDFName.of('Annots'));
|
||||
}
|
||||
|
||||
if (!widgets) {
|
||||
throw new Error('No widgets');
|
||||
}
|
||||
|
||||
pages[0].node.set(PDFName.of('Annots'), widgets);
|
||||
|
||||
doc.catalog.set(
|
||||
PDFName.of('AcroForm'),
|
||||
doc.context.obj({
|
||||
SigFlags: 3,
|
||||
Fields: [widgetRef],
|
||||
}),
|
||||
);
|
||||
|
||||
return Buffer.from(await doc.save({ useObjectStreams: false }));
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { signWithLocalCert } from './transports/local-cert';
|
||||
|
||||
export type SignOptions = {
|
||||
pdf: Buffer;
|
||||
};
|
||||
|
||||
export const signPdf = async ({ pdf }: SignOptions) => {
|
||||
const transport = process.env.NEXT_PRIVATE_SIGNING_TRANSPORT || 'local';
|
||||
|
||||
return await match(transport)
|
||||
.with('local', async () => signWithLocalCert({ pdf }))
|
||||
.otherwise(() => {
|
||||
throw new Error(`Unsupported signing transport: ${transport}`);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@documenso/signing",
|
||||
"version": "1.0.0",
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
"license": "AGPLv3",
|
||||
"files": [
|
||||
"transports/",
|
||||
"index.ts"
|
||||
],
|
||||
"scripts": {
|
||||
},
|
||||
"dependencies": {
|
||||
"@documenso/tsconfig": "*",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-signpdf": "^2.0.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"ts-pattern": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node-forge": "^1.3.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import signer from 'node-signpdf';
|
||||
import fs from 'node:fs';
|
||||
|
||||
import { addSigningPlaceholder } from '../helpers/addSigningPlaceholder';
|
||||
|
||||
export type SignWithLocalCertOptions = {
|
||||
pdf: Buffer;
|
||||
};
|
||||
|
||||
export const signWithLocalCert = async ({ pdf }: SignWithLocalCertOptions) => {
|
||||
const pdfWithPlaceholder = await addSigningPlaceholder({ pdf });
|
||||
|
||||
let p12Cert: Buffer | null = null;
|
||||
|
||||
if (process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS) {
|
||||
p12Cert = Buffer.from(process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS, 'base64');
|
||||
}
|
||||
|
||||
if (!p12Cert) {
|
||||
p12Cert = 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,
|
||||
});
|
||||
}
|
||||
|
||||
return signer.sign(pdfWithPlaceholder, p12Cert);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@documenso/tsconfig/react-library.json",
|
||||
"compilerOptions": {
|
||||
"types": ["@documenso/tsconfig/process-env.d.ts", "@types/node"]
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", "**/*.json"],
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
}
|
||||
Vendored
+6
@@ -21,6 +21,12 @@ declare namespace NodeJS {
|
||||
NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID?: string;
|
||||
NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY?: string;
|
||||
|
||||
NEXT_PRIVATE_SIGNING_TRANSPORT?: 'local' | 'http' | 'gcloud-hsm';
|
||||
NEXT_PRIVATE_SIGNING_PASSPHRASE?: string;
|
||||
NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH?: string;
|
||||
NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS?: string;
|
||||
NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING?: string;
|
||||
|
||||
NEXT_PRIVATE_SMTP_TRANSPORT?: 'mailchannels' | 'smtp-auth' | 'smtp-api';
|
||||
|
||||
NEXT_PRIVATE_MAILCHANNELS_API_KEY?: string;
|
||||
|
||||
+6
-1
@@ -44,6 +44,11 @@
|
||||
"NEXT_PRIVATE_UPLOAD_BUCKET",
|
||||
"NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID",
|
||||
"NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY",
|
||||
"NEXT_PRIVATE_SIGNING_TRANSPORT",
|
||||
"NEXT_PRIVATE_SIGNING_PASSPHRASE",
|
||||
"NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH",
|
||||
"NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS",
|
||||
"NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING",
|
||||
"NEXT_PRIVATE_SMTP_TRANSPORT",
|
||||
"NEXT_PRIVATE_MAILCHANNELS_API_KEY",
|
||||
"NEXT_PRIVATE_MAILCHANNELS_ENDPOINT",
|
||||
@@ -69,4 +74,4 @@
|
||||
"POSTGRES_PRISMA_URL",
|
||||
"POSTGRES_URL_NON_POOLING"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user