Compare commits

..
Author SHA1 Message Date
Mythie 584d5bd8ea fix: update share preview 2023-09-26 15:58:43 +10:00
Mythie e4b6d42672 fix: styling updates 2023-09-26 15:56:11 +10:00
Ephraim Atta-Duncan 78498793fa chore: refactor function 2023-09-25 11:21:39 +00:00
Ephraim Atta-Duncan b2e916378d feat: add dropdown to tweet or copy signing link 2023-09-25 11:15:31 +00:00
Mythie a52c19355a chore: sign document 2023-09-25 15:57:10 +10:00
Mythie e67e96333b fix: dark mode for generic mdx content 2023-09-25 11:31:26 +10:00
Lucas Smith 7aa75b0c64 Merge pull request #403 from documenso/feat/single-player-mode
feat: single player mode
2023-09-25 11:25:58 +10:00
Timur Ercan 5615627001 Merge pull request #419 from documenso/fix/incorporation
fix: company name
2023-09-24 21:53:10 +02:00
Timur Ercan 78c55875ef fix: company name 2023-09-24 21:52:09 +02:00
16 changed files with 345 additions and 24 deletions
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>
);
};
+53
View File
@@ -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>
+2 -1
View File
@@ -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 }));
};
+17
View File
@@ -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}`);
});
};
+23
View File
@@ -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"
}
}
+32
View File
@@ -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);
};
+8
View File
@@ -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"]
}
+6
View File
@@ -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
View File
@@ -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"
]
}
}