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:
Ephraim Duncan
2025-04-28 01:30:09 +00:00
committed by GitHub
parent 6a41a37bd4
commit bdb0b0ea88
26 changed files with 423 additions and 31 deletions
@@ -22,6 +22,7 @@ import {
ZWebhookDocumentSchema,
mapDocumentToWebhookDocumentPayload,
} from '../../../types/webhook-payload';
import { prefixedId } from '../../../universal/id';
import { getFileServerSide } from '../../../universal/upload/get-file.server';
import { putPdfFileServerSide } from '../../../universal/upload/put-file.server';
import { fieldsContainUnsignedRequiredField } from '../../../utils/advanced-fields-helpers';
@@ -130,6 +131,17 @@ export const run = async ({
documentData.data = documentData.initialData;
}
if (!document.qrToken) {
await prisma.document.update({
where: {
id: document.id,
},
data: {
qrToken: prefixedId('qr'),
},
});
}
const pdfData = await getFileServerSide(documentData);
const certificateData =
@@ -13,7 +13,7 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { nanoid } from '@documenso/lib/universal/id';
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import type { TCreateDocumentV2Request } from '@documenso/trpc/server/document-router/schema';
@@ -142,6 +142,7 @@ export const createDocumentV2 = async ({
const document = await tx.document.create({
data: {
title,
qrToken: prefixedId('qr'),
externalId: data.externalId,
documentDataId,
userId,
@@ -13,6 +13,7 @@ import {
ZWebhookDocumentSchema,
mapDocumentToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import { prefixedId } from '../../universal/id';
import { getFileServerSide } from '../../universal/upload/get-file.server';
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
import { determineDocumentVisibility } from '../../utils/document-visibility';
@@ -115,6 +116,7 @@ export const createDocument = async ({
const document = await tx.document.create({
data: {
title,
qrToken: prefixedId('qr'),
externalId,
documentDataId,
userId,
@@ -3,6 +3,7 @@ import { DocumentSource, type Prisma } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { prefixedId } from '../../universal/id';
import { getDocumentWhereInput } from './get-document-by-id';
export interface DuplicateDocumentOptions {
@@ -56,6 +57,7 @@ export const duplicateDocument = async ({
const createDocumentArguments: Prisma.DocumentCreateArgs = {
data: {
title: document.title,
qrToken: prefixedId('qr'),
user: {
connect: {
id: document.userId,
@@ -0,0 +1,38 @@
import { prisma } from '@documenso/prisma';
export type GetDocumentByAccessTokenOptions = {
token: string;
};
export const getDocumentByAccessToken = async ({ token }: GetDocumentByAccessTokenOptions) => {
if (!token) {
throw new Error('Missing token');
}
const result = await prisma.document.findFirstOrThrow({
where: {
qrToken: token,
},
select: {
id: true,
title: true,
completedAt: true,
documentData: {
select: {
id: true,
type: true,
data: true,
initialData: true,
},
},
documentMeta: {
select: {
password: true,
},
},
recipients: true,
},
});
return result;
};
@@ -19,7 +19,7 @@ import { z } from 'zod';
import { mailer } from '@documenso/email/mailer';
import { DocumentCreatedFromDirectTemplateEmailTemplate } from '@documenso/email/templates/document-created-from-direct-template';
import { nanoid } from '@documenso/lib/universal/id';
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import type { TSignFieldWithTokenMutationSchema } from '@documenso/trpc/server/field-router/schema';
@@ -276,6 +276,7 @@ export const createDocumentFromDirectTemplate = async ({
// Create the document and non direct template recipients.
const document = await tx.document.create({
data: {
qrToken: prefixedId('qr'),
source: DocumentSource.TEMPLATE_DIRECT_LINK,
templateId: template.id,
userId: template.userId,
@@ -1,6 +1,6 @@
import { DocumentSource, type RecipientRole } from '@prisma/client';
import { nanoid } from '@documenso/lib/universal/id';
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
export type CreateDocumentFromTemplateLegacyOptions = {
@@ -70,6 +70,7 @@ export const createDocumentFromTemplateLegacy = async ({
const document = await prisma.document.create({
data: {
qrToken: prefixedId('qr'),
source: DocumentSource.TEMPLATE,
templateId: template.id,
userId,
@@ -11,7 +11,7 @@ import {
} from '@prisma/client';
import { match } from 'ts-pattern';
import { nanoid } from '@documenso/lib/universal/id';
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import type { SupportedLanguageCodes } from '../../constants/i18n';
@@ -372,6 +372,7 @@ export const createDocumentFromTemplate = async ({
return await prisma.$transaction(async (tx) => {
const document = await tx.document.create({
data: {
qrToken: prefixedId('qr'),
source: DocumentSource.TEMPLATE,
externalId: externalId || template.externalId,
templateId: template.id,
+4
View File
@@ -86,6 +86,8 @@ export const ZDocumentLiteSchema = DocumentSchema.pick({
useLegacyFieldInsertion: true,
});
export type TDocumentLite = z.infer<typeof ZDocumentLiteSchema>;
/**
* A version of the document response schema when returning multiple documents at once from a single API endpoint.
*/
@@ -119,3 +121,5 @@ export const ZDocumentManySchema = DocumentSchema.pick({
url: true,
}).nullable(),
});
export type TDocumentMany = z.infer<typeof ZDocumentManySchema>;
+6
View File
@@ -3,3 +3,9 @@ import { customAlphabet } from 'nanoid';
export const alphaid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 21);
export { nanoid } from 'nanoid';
export const fancyId = customAlphabet('abcdefhiklmnorstuvwxyz', 16);
export const prefixedId = (prefix: string, length = 16) => {
return `${prefix}_${fancyId(length)}`;
};