mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
feat: certificate qrcode (#1755)
Adds document access tokens and QR code functionality to enable secure document sharing via URLs. It includes a new document access page that allows viewing and downloading documents through tokenized links.
This commit is contained in:
@ -1,191 +0,0 @@
|
||||
import satori from 'satori';
|
||||
import sharp from 'sharp';
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { getRecipientOrSenderByShareLinkSlug } from '@documenso/lib/server-only/document/get-recipient-or-sender-by-share-link-slug';
|
||||
|
||||
import type { Route } from './+types/share.$slug.opengraph';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
const CARD_OFFSET_TOP = 173;
|
||||
const CARD_OFFSET_LEFT = 307;
|
||||
const CARD_WIDTH = 590;
|
||||
const CARD_HEIGHT = 337;
|
||||
|
||||
const IMAGE_SIZE = {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
};
|
||||
|
||||
export const loader = async ({ params }: Route.LoaderArgs) => {
|
||||
const { slug } = params;
|
||||
|
||||
const baseUrl = NEXT_PUBLIC_WEBAPP_URL();
|
||||
|
||||
const [interSemiBold, interRegular, caveatRegular] = await Promise.all([
|
||||
fetch(new URL(`${baseUrl}/fonts/inter-semibold.ttf`, import.meta.url)).then(async (res) =>
|
||||
res.arrayBuffer(),
|
||||
),
|
||||
fetch(new URL(`${baseUrl}/fonts/inter-regular.ttf`, import.meta.url)).then(async (res) =>
|
||||
res.arrayBuffer(),
|
||||
),
|
||||
fetch(new URL(`${baseUrl}/fonts/caveat-regular.ttf`, import.meta.url)).then(async (res) =>
|
||||
res.arrayBuffer(),
|
||||
),
|
||||
]);
|
||||
|
||||
const recipientOrSender = await getRecipientOrSenderByShareLinkSlug({
|
||||
slug,
|
||||
});
|
||||
|
||||
if ('error' in recipientOrSender) {
|
||||
return Response.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const isRecipient = 'Signature' in recipientOrSender;
|
||||
|
||||
const signatureImage = match(recipientOrSender)
|
||||
.with({ signatures: P.array(P._) }, (recipient) => {
|
||||
return recipient.signatures?.[0]?.signatureImageAsBase64 || null;
|
||||
})
|
||||
.otherwise((sender) => {
|
||||
return sender.signature || null;
|
||||
});
|
||||
|
||||
const signatureName = match(recipientOrSender)
|
||||
.with({ signatures: P.array(P._) }, (recipient) => {
|
||||
return recipient.name || recipient.email;
|
||||
})
|
||||
.otherwise((sender) => {
|
||||
return sender.name || sender.email;
|
||||
});
|
||||
|
||||
// Generate SVG using Satori
|
||||
const svg = await satori(
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
backgroundColor: 'white',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={`${baseUrl}/static/og-share-frame2.png`}
|
||||
alt="og-share-frame"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
/>
|
||||
|
||||
{signatureImage ? (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
padding: '24px 48px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
top: CARD_OFFSET_TOP,
|
||||
left: CARD_OFFSET_LEFT,
|
||||
width: CARD_WIDTH,
|
||||
height: CARD_HEIGHT,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={signatureImage}
|
||||
alt="signature"
|
||||
style={{
|
||||
opacity: 0.6,
|
||||
height: '100%',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<p
|
||||
style={{
|
||||
position: 'absolute',
|
||||
padding: '24px 48px',
|
||||
marginTop: '-8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
color: '#64748b',
|
||||
fontFamily: 'Caveat',
|
||||
fontSize: Math.max(Math.min((CARD_WIDTH * 1.5) / signatureName.length, 80), 36),
|
||||
top: CARD_OFFSET_TOP,
|
||||
left: CARD_OFFSET_LEFT,
|
||||
width: CARD_WIDTH,
|
||||
height: CARD_HEIGHT,
|
||||
}}
|
||||
>
|
||||
{signatureName}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
top: CARD_OFFSET_TOP - 78,
|
||||
left: CARD_OFFSET_LEFT,
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
color: '#828282',
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{isRecipient ? 'Document Signed!' : 'Document Sent!'}
|
||||
</h2>
|
||||
</div>
|
||||
</div>,
|
||||
{
|
||||
width: IMAGE_SIZE.width,
|
||||
height: IMAGE_SIZE.height,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Caveat',
|
||||
data: caveatRegular,
|
||||
style: 'italic',
|
||||
},
|
||||
{
|
||||
name: 'Inter',
|
||||
data: interRegular,
|
||||
weight: 400,
|
||||
},
|
||||
{
|
||||
name: 'Inter',
|
||||
data: interSemiBold,
|
||||
weight: 600,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
// Convert SVG to PNG using sharp
|
||||
const pngBuffer = await sharp(Buffer.from(svg)).toFormat('png').toBuffer();
|
||||
|
||||
return new Response(pngBuffer, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Length': pngBuffer.length.toString(),
|
||||
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,59 +0,0 @@
|
||||
import { redirect } from 'react-router';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
|
||||
import type { Route } from './+types/share.$slug';
|
||||
|
||||
export function meta({ params: { slug } }: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: 'Documenso - Share' },
|
||||
{ description: 'I just signed a document in style with Documenso!' },
|
||||
{
|
||||
property: 'og:title',
|
||||
content: 'Documenso - Join the open source signing revolution',
|
||||
},
|
||||
{
|
||||
property: 'og:description',
|
||||
content: 'I just signed with Documenso!',
|
||||
},
|
||||
{
|
||||
property: 'og:type',
|
||||
content: 'website',
|
||||
},
|
||||
{
|
||||
property: 'og:image',
|
||||
content: `${NEXT_PUBLIC_WEBAPP_URL()}/share/${slug}/opengraph`,
|
||||
},
|
||||
{
|
||||
name: 'twitter:site',
|
||||
content: '@documenso',
|
||||
},
|
||||
{
|
||||
name: 'twitter:card',
|
||||
content: 'summary_large_image',
|
||||
},
|
||||
{
|
||||
name: 'twitter:image',
|
||||
content: `${NEXT_PUBLIC_WEBAPP_URL()}/share/${slug}/opengraph`,
|
||||
},
|
||||
{
|
||||
name: 'twitter:description',
|
||||
content: 'I just signed with Documenso!',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export const loader = ({ request }: Route.LoaderArgs) => {
|
||||
const userAgent = request.headers.get('User-Agent') ?? '';
|
||||
|
||||
if (/bot|facebookexternalhit|WhatsApp|google|bing|duckduckbot|MetaInspector/i.test(userAgent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Is hardcoded because this whole meta is hardcoded anyway for Documenso.
|
||||
throw redirect('https://documenso.com');
|
||||
};
|
||||
|
||||
export default function SharePage() {
|
||||
return <div></div>;
|
||||
}
|
||||
Reference in New Issue
Block a user