chore: merged main

This commit is contained in:
Catalin Pit
2023-12-06 13:18:59 +00:00
135 changed files with 6577 additions and 3984 deletions

View File

@ -1,10 +1,12 @@
import { TRPCError } from '@trpc/server';
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
import { compareSync } from '@documenso/lib/server-only/auth/hash';
import { createUser } from '@documenso/lib/server-only/user/create-user';
import { sendConfirmationToken } from '@documenso/lib/server-only/user/send-confirmation-token';
import { procedure, router } from '../trpc';
import { ZSignUpMutationSchema } from './schema';
import { authenticatedProcedure, procedure, router } from '../trpc';
import { ZSignUpMutationSchema, ZVerifyPasswordMutationSchema } from './schema';
export const authRouter = router({
signup: procedure.input(ZSignUpMutationSchema).mutation(async ({ input }) => {
@ -30,4 +32,23 @@ export const authRouter = router({
});
}
}),
verifyPassword: authenticatedProcedure
.input(ZVerifyPasswordMutationSchema)
.mutation(({ ctx, input }) => {
const user = ctx.user;
const { password } = input;
if (!user.password) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: ErrorCode.INCORRECT_PASSWORD,
});
}
const valid = compareSync(password, user.password);
return valid;
}),
});

View File

@ -8,3 +8,5 @@ export const ZSignUpMutationSchema = z.object({
});
export type TSignUpMutationSchema = z.infer<typeof ZSignUpMutationSchema>;
export const ZVerifyPasswordMutationSchema = ZSignUpMutationSchema.pick({ password: true });

View File

@ -1,13 +1,16 @@
import { TRPCError } from '@trpc/server';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import { createDocument } from '@documenso/lib/server-only/document/create-document';
import { deleteDraftDocument } from '@documenso/lib/server-only/document/delete-draft-document';
import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
import { duplicateDocumentById } from '@documenso/lib/server-only/document/duplicate-document-by-id';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { updateTitle } from '@documenso/lib/server-only/document/update-title';
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
@ -18,9 +21,11 @@ import {
ZGetDocumentByIdQuerySchema,
ZGetDocumentByTokenQuerySchema,
ZResendDocumentMutationSchema,
ZSearchDocumentsMutationSchema,
ZSendDocumentMutationSchema,
ZSetFieldsForDocumentMutationSchema,
ZSetRecipientsForDocumentMutationSchema,
ZSetTitleForDocumentMutationSchema,
} from './schema';
export const documentRouter = router({
@ -94,15 +99,15 @@ export const documentRouter = router({
}
}),
deleteDraftDocument: authenticatedProcedure
deleteDocument: authenticatedProcedure
.input(ZDeleteDraftDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { id } = input;
const { id, status } = input;
const userId = ctx.user.id;
return await deleteDraftDocument({ id, userId });
return await deleteDocument({ id, userId, status });
} catch (err) {
console.error(err);
@ -113,6 +118,20 @@ export const documentRouter = router({
}
}),
setTitleForDocument: authenticatedProcedure
.input(ZSetTitleForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
const { documentId, title } = input;
const userId = ctx.user.id;
return await updateTitle({
title,
userId,
documentId,
});
}),
setRecipientsForDocument: authenticatedProcedure
.input(ZSetRecipientsForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
@ -160,7 +179,15 @@ export const documentRouter = router({
.input(ZSendDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { documentId } = input;
const { documentId, email } = input;
if (email.message || email.subject) {
await upsertDocumentMeta({
documentId,
subject: email.subject,
message: email.message,
});
}
return await sendDocument({
userId: ctx.user.id,
@ -215,4 +242,23 @@ export const documentRouter = router({
});
}
}),
searchDocuments: authenticatedProcedure
.input(ZSearchDocumentsMutationSchema)
.query(async ({ input, ctx }) => {
const { query } = input;
try {
const documents = await searchDocumentsWithKeyword({
query,
userId: ctx.user.id,
});
return documents;
} catch (error) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We are unable to search for documents. Please try again later.',
});
}
}),
});

View File

@ -1,6 +1,6 @@
import { z } from 'zod';
import { FieldType } from '@documenso/prisma/client';
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
export const ZGetDocumentByIdQuerySchema = z.object({
id: z.number().min(1),
@ -21,6 +21,13 @@ export const ZCreateDocumentMutationSchema = z.object({
export type TCreateDocumentMutationSchema = z.infer<typeof ZCreateDocumentMutationSchema>;
export const ZSetTitleForDocumentMutationSchema = z.object({
documentId: z.number(),
title: z.string().min(1),
});
export type TSetTitleForDocumentMutationSchema = z.infer<typeof ZSetTitleForDocumentMutationSchema>;
export const ZSetRecipientsForDocumentMutationSchema = z.object({
documentId: z.number(),
recipients: z.array(
@ -58,6 +65,10 @@ export type TSetFieldsForDocumentMutationSchema = z.infer<
export const ZSendDocumentMutationSchema = z.object({
documentId: z.number(),
email: z.object({
subject: z.string(),
message: z.string(),
}),
});
export const ZResendDocumentMutationSchema = z.object({
@ -69,6 +80,11 @@ export type TSendDocumentMutationSchema = z.infer<typeof ZSendDocumentMutationSc
export const ZDeleteDraftDocumentMutationSchema = z.object({
id: z.number().min(1),
status: z.nativeEnum(DocumentStatus),
});
export type TDeleteDraftDocumentMutationSchema = z.infer<typeof ZDeleteDraftDocumentMutationSchema>;
export const ZSearchDocumentsMutationSchema = z.object({
query: z.string(),
});

View File

@ -1,15 +1,47 @@
import { TRPCError } from '@trpc/server';
import { removeSignedFieldWithToken } from '@documenso/lib/server-only/field/remove-signed-field-with-token';
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
import { signFieldWithToken } from '@documenso/lib/server-only/field/sign-field-with-token';
import { procedure, router } from '../trpc';
import { authenticatedProcedure, procedure, router } from '../trpc';
import {
ZAddFieldsMutationSchema,
ZRemovedSignedFieldWithTokenMutationSchema,
ZSignFieldWithTokenMutationSchema,
} from './schema';
export const fieldRouter = router({
addFields: authenticatedProcedure
.input(ZAddFieldsMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { documentId, fields } = input;
return await setFieldsForDocument({
documentId,
userId: ctx.user.id,
fields: fields.map((field) => ({
id: field.nativeId,
signerEmail: field.signerEmail,
type: field.type,
pageNumber: field.pageNumber,
pageX: field.pageX,
pageY: field.pageY,
pageWidth: field.pageWidth,
pageHeight: field.pageHeight,
})),
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to sign this field. Please try again later.',
});
}
}),
signFieldWithToken: procedure
.input(ZSignFieldWithTokenMutationSchema)
.mutation(async ({ input }) => {

View File

@ -1,5 +1,26 @@
import { z } from 'zod';
import { FieldType } from '@documenso/prisma/client';
export const ZAddFieldsMutationSchema = z.object({
documentId: z.number(),
fields: z.array(
z.object({
formId: z.string().min(1),
nativeId: z.number().optional(),
type: z.nativeEnum(FieldType),
signerEmail: z.string().min(1),
pageNumber: z.number().min(1),
pageX: z.number().min(0),
pageY: z.number().min(0),
pageWidth: z.number().min(0),
pageHeight: z.number().min(0),
}),
),
});
export type TAddFieldsMutationSchema = z.infer<typeof ZAddFieldsMutationSchema>;
export const ZSignFieldWithTokenMutationSchema = z.object({
token: z.string(),
fieldId: z.number(),

View File

@ -0,0 +1,54 @@
import { TRPCError } from '@trpc/server';
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
import { authenticatedProcedure, procedure, router } from '../trpc';
import { ZAddSignersMutationSchema, ZCompleteDocumentWithTokenMutationSchema } from './schema';
export const recipientRouter = router({
addSigners: authenticatedProcedure
.input(ZAddSignersMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { documentId, signers } = input;
return await setRecipientsForDocument({
userId: ctx.user.id,
documentId,
recipients: signers.map((signer) => ({
id: signer.nativeId,
email: signer.email,
name: signer.name,
})),
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to sign this field. Please try again later.',
});
}
}),
completeDocumentWithToken: procedure
.input(ZCompleteDocumentWithTokenMutationSchema)
.mutation(async ({ input }) => {
try {
const { token, documentId } = input;
return await completeDocumentWithToken({
token,
documentId,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to sign this field. Please try again later.',
});
}
}),
});

View File

@ -0,0 +1,33 @@
import { z } from 'zod';
export const ZAddSignersMutationSchema = z
.object({
documentId: z.number(),
signers: z.array(
z.object({
nativeId: z.number().optional(),
email: z.string().email().min(1),
name: z.string(),
}),
),
})
.refine(
(schema) => {
const emails = schema.signers.map((signer) => signer.email.toLowerCase());
return new Set(emails).size === emails.length;
},
// Dirty hack to handle errors when .root is populated for an array type
{ message: 'Signers must have unique emails', path: ['signers__root'] },
);
export type TAddSignersMutationSchema = z.infer<typeof ZAddSignersMutationSchema>;
export const ZCompleteDocumentWithTokenMutationSchema = z.object({
token: z.string(),
documentId: z.number(),
});
export type TCompleteDocumentWithTokenMutationSchema = z.infer<
typeof ZCompleteDocumentWithTokenMutationSchema
>;

View File

@ -4,17 +4,23 @@ import { authRouter } from './auth-router/router';
import { documentRouter } from './document-router/router';
import { fieldRouter } from './field-router/router';
import { profileRouter } from './profile-router/router';
import { recipientRouter } from './recipient-router/router';
import { shareLinkRouter } from './share-link-router/router';
import { singleplayerRouter } from './singleplayer-router/router';
import { router } from './trpc';
import { twoFactorAuthenticationRouter } from './two-factor-authentication-router/router';
export const appRouter = router({
auth: authRouter,
profile: profileRouter,
document: documentRouter,
field: fieldRouter,
recipient: recipientRouter,
admin: adminRouter,
shareLink: shareLinkRouter,
apiToken: apiTokenRouter,
singleplayer: singleplayerRouter,
twoFactorAuthentication: twoFactorAuthenticationRouter,
});
export type AppRouter = typeof appRouter;

View File

@ -0,0 +1,37 @@
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
import { FieldType, Prisma } from '@documenso/prisma/client';
import type { TCreateSinglePlayerDocumentMutationSchema } from './schema';
/**
* Map the fields provided by the user to fields compatible with Prisma.
*
* Signature fields are handled separately.
*
* @param field The field passed in by the user.
* @param signer The details of the person who is signing this document.
* @returns A field compatible with Prisma.
*/
export const mapField = (
field: TCreateSinglePlayerDocumentMutationSchema['fields'][number],
signer: TCreateSinglePlayerDocumentMutationSchema['signer'],
) => {
const customText = match(field.type)
.with(FieldType.DATE, () => DateTime.now().toFormat('yyyy-MM-dd hh:mm a'))
.with(FieldType.EMAIL, () => signer.email)
.with(FieldType.NAME, () => signer.name)
.otherwise(() => '');
return {
type: field.type,
page: field.page,
positionX: new Prisma.Decimal(field.positionX),
positionY: new Prisma.Decimal(field.positionY),
width: new Prisma.Decimal(field.width),
height: new Prisma.Decimal(field.height),
customText,
inserted: true,
};
};

View File

@ -0,0 +1,176 @@
import { createElement } from 'react';
import { PDFDocument } from 'pdf-lib';
import { mailer } from '@documenso/email/mailer';
import { renderAsync } from '@documenso/email/render';
import { DocumentSelfSignedEmailTemplate } from '@documenso/email/templates/document-self-signed';
import { FROM_ADDRESS, FROM_NAME, SERVICE_USER_EMAIL } from '@documenso/lib/constants/email';
import { insertFieldInPDF } from '@documenso/lib/server-only/pdf/insert-field-in-pdf';
import { alphaid } from '@documenso/lib/universal/id';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { putFile } from '@documenso/lib/universal/upload/put-file';
import { prisma } from '@documenso/prisma';
import {
DocumentStatus,
FieldType,
ReadStatus,
SendStatus,
SigningStatus,
} from '@documenso/prisma/client';
import { signPdf } from '@documenso/signing';
import { procedure, router } from '../trpc';
import { mapField } from './helper';
import { ZCreateSinglePlayerDocumentMutationSchema } from './schema';
export const singleplayerRouter = router({
createSinglePlayerDocument: procedure
.input(ZCreateSinglePlayerDocumentMutationSchema)
.mutation(async ({ input }) => {
const { signer, fields, documentData, documentName } = input;
const document = await getFile({
data: documentData.data,
type: documentData.type,
});
const doc = await PDFDocument.load(document);
const createdAt = new Date();
const isBase64 = signer.signature.startsWith('data:image/png;base64,');
const signatureImageAsBase64 = isBase64 ? signer.signature : null;
const typedSignature = !isBase64 ? signer.signature : null;
// Update the document with the fields inserted.
for (const field of fields) {
const isSignatureField = field.type === FieldType.SIGNATURE;
await insertFieldInPDF(doc, {
...mapField(field, signer),
Signature: isSignatureField
? {
created: createdAt,
signatureImageAsBase64,
typedSignature,
// Dummy data.
id: -1,
recipientId: -1,
fieldId: -1,
}
: null,
// Dummy data.
id: -1,
documentId: -1,
recipientId: -1,
});
}
const unsignedPdfBytes = await doc.save();
const signedPdfBuffer = await signPdf({ pdf: Buffer.from(unsignedPdfBytes) });
const { token } = await prisma.$transaction(
async (tx) => {
const token = alphaid();
// Fetch service user who will be the owner of the document.
const serviceUser = await tx.user.findFirstOrThrow({
where: {
email: SERVICE_USER_EMAIL,
},
});
const { id: documentDataId } = await putFile({
name: `${documentName}.pdf`,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(signedPdfBuffer),
});
// Create document.
const document = await tx.document.create({
data: {
title: documentName,
status: DocumentStatus.COMPLETED,
documentDataId,
userId: serviceUser.id,
createdAt,
},
});
// Create recipient.
const recipient = await tx.recipient.create({
data: {
documentId: document.id,
name: signer.name,
email: signer.email,
token,
signedAt: createdAt,
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
sendStatus: SendStatus.SENT,
},
});
// Create fields and signatures.
await Promise.all(
fields.map(async (field) => {
const insertedField = await tx.field.create({
data: {
documentId: document.id,
recipientId: recipient.id,
...mapField(field, signer),
},
});
if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) {
await tx.signature.create({
data: {
fieldId: insertedField.id,
signatureImageAsBase64,
typedSignature,
recipientId: recipient.id,
},
});
}
}),
);
return { document, token };
},
{
maxWait: 5000,
timeout: 30000,
},
);
const template = createElement(DocumentSelfSignedEmailTemplate, {
documentName: documentName,
assetBaseUrl: process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000',
});
const [html, text] = await Promise.all([
renderAsync(template),
renderAsync(template, { plainText: true }),
]);
// Send email to signer.
await mailer.sendMail({
to: {
address: signer.email,
name: signer.name,
},
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: 'Document signed',
html,
text,
attachments: [{ content: signedPdfBuffer, filename: documentName }],
});
return token;
}),
});

View File

@ -0,0 +1,30 @@
import { z } from 'zod';
import { DocumentDataType, FieldType } from '@documenso/prisma/client';
export const ZCreateSinglePlayerDocumentMutationSchema = z.object({
documentData: z.object({
data: z.string(),
type: z.nativeEnum(DocumentDataType),
}),
documentName: z.string(),
signer: z.object({
email: z.string().email().min(1),
name: z.string(),
signature: z.string(),
}),
fields: z.array(
z.object({
page: z.number(),
type: z.nativeEnum(FieldType),
positionX: z.number(),
positionY: z.number(),
width: z.number(),
height: z.number(),
}),
),
});
export type TCreateSinglePlayerDocumentMutationSchema = z.infer<
typeof ZCreateSinglePlayerDocumentMutationSchema
>;

View File

@ -0,0 +1,105 @@
import { TRPCError } from '@trpc/server';
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
import { disableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/disable-2fa';
import { enableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/enable-2fa';
import { getBackupCodes } from '@documenso/lib/server-only/2fa/get-backup-code';
import { setupTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/setup-2fa';
import { compareSync } from '@documenso/lib/server-only/auth/hash';
import { authenticatedProcedure, router } from '../trpc';
import {
ZDisableTwoFactorAuthenticationMutationSchema,
ZEnableTwoFactorAuthenticationMutationSchema,
ZSetupTwoFactorAuthenticationMutationSchema,
ZViewRecoveryCodesMutationSchema,
} from './schema';
export const twoFactorAuthenticationRouter = router({
setup: authenticatedProcedure
.input(ZSetupTwoFactorAuthenticationMutationSchema)
.mutation(async ({ ctx, input }) => {
const user = ctx.user;
const { password } = input;
return await setupTwoFactorAuthentication({ user, password });
}),
enable: authenticatedProcedure
.input(ZEnableTwoFactorAuthenticationMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
const user = ctx.user;
const { code } = input;
return await enableTwoFactorAuthentication({ user, code });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to enable two-factor authentication. Please try again later.',
});
}
}),
disable: authenticatedProcedure
.input(ZDisableTwoFactorAuthenticationMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
const user = ctx.user;
const { password, backupCode } = input;
return await disableTwoFactorAuthentication({ user, password, backupCode });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to disable two-factor authentication. Please try again later.',
});
}
}),
viewRecoveryCodes: authenticatedProcedure
.input(ZViewRecoveryCodesMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
const user = ctx.user;
const { password } = input;
if (!user.twoFactorEnabled) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: ErrorCode.TWO_FACTOR_SETUP_REQUIRED,
});
}
if (!user.password || !compareSync(password, user.password)) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: ErrorCode.INCORRECT_PASSWORD,
});
}
const recoveryCodes = await getBackupCodes({ user });
return { recoveryCodes };
} catch (err) {
console.error(err);
if (err instanceof TRPCError) {
throw err;
}
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to view your recovery codes. Please try again later.',
});
}
}),
});

View File

@ -0,0 +1,32 @@
import { z } from 'zod';
export const ZSetupTwoFactorAuthenticationMutationSchema = z.object({
password: z.string().min(1),
});
export type TSetupTwoFactorAuthenticationMutationSchema = z.infer<
typeof ZSetupTwoFactorAuthenticationMutationSchema
>;
export const ZEnableTwoFactorAuthenticationMutationSchema = z.object({
code: z.string().min(6).max(6),
});
export type TEnableTwoFactorAuthenticationMutationSchema = z.infer<
typeof ZEnableTwoFactorAuthenticationMutationSchema
>;
export const ZDisableTwoFactorAuthenticationMutationSchema = z.object({
password: z.string().min(6).max(72),
backupCode: z.string().trim(),
});
export type TDisableTwoFactorAuthenticationMutationSchema = z.infer<
typeof ZDisableTwoFactorAuthenticationMutationSchema
>;
export const ZViewRecoveryCodesMutationSchema = z.object({
password: z.string().min(6).max(72),
});
export type TViewRecoveryCodesMutationSchema = z.infer<typeof ZViewRecoveryCodesMutationSchema>;