Merge branch 'feat/refresh' into feat/completed-share-link

This commit is contained in:
Lucas Smith
2023-09-20 12:42:30 +10:00
committed by GitHub
149 changed files with 6776 additions and 900 deletions

View File

@ -0,0 +1,26 @@
import { prisma } from '@documenso/prisma';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
export const getDocumentStats = async () => {
const counts = await prisma.document.groupBy({
by: ['status'],
_count: {
_all: true,
},
});
const stats: Record<Exclude<ExtendedDocumentStatus, 'INBOX'>, number> = {
[ExtendedDocumentStatus.DRAFT]: 0,
[ExtendedDocumentStatus.PENDING]: 0,
[ExtendedDocumentStatus.COMPLETED]: 0,
[ExtendedDocumentStatus.ALL]: 0,
};
counts.forEach((stat) => {
stats[stat.status] = stat._count._all;
stats.ALL += stat._count._all;
});
return stats;
};

View File

@ -0,0 +1,29 @@
import { prisma } from '@documenso/prisma';
import { ReadStatus, SendStatus, SigningStatus } from '@documenso/prisma/client';
export const getRecipientsStats = async () => {
const results = await prisma.recipient.groupBy({
by: ['readStatus', 'signingStatus', 'sendStatus'],
_count: true,
});
const stats = {
TOTAL_RECIPIENTS: 0,
[ReadStatus.OPENED]: 0,
[ReadStatus.NOT_OPENED]: 0,
[SigningStatus.SIGNED]: 0,
[SigningStatus.NOT_SIGNED]: 0,
[SendStatus.SENT]: 0,
[SendStatus.NOT_SENT]: 0,
};
results.forEach((result) => {
const { readStatus, signingStatus, sendStatus, _count } = result;
stats[readStatus] += _count;
stats[signingStatus] += _count;
stats[sendStatus] += _count;
stats.TOTAL_RECIPIENTS += _count;
});
return stats;
};

View File

@ -0,0 +1,18 @@
import { prisma } from '@documenso/prisma';
import { SubscriptionStatus } from '@documenso/prisma/client';
export const getUsersCount = async () => {
return await prisma.user.count();
};
export const getUsersWithSubscriptionsCount = async () => {
return await prisma.user.count({
where: {
Subscription: {
some: {
status: SubscriptionStatus.ACTIVE,
},
},
},
});
};

View File

@ -0,0 +1,53 @@
import { createElement } from 'react';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { ForgotPasswordTemplate } from '@documenso/email/templates/forgot-password';
import { prisma } from '@documenso/prisma';
export interface SendForgotPasswordOptions {
userId: number;
}
export const sendForgotPassword = async ({ userId }: SendForgotPasswordOptions) => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
include: {
PasswordResetToken: {
orderBy: {
createdAt: 'desc',
},
take: 1,
},
},
});
if (!user) {
throw new Error('User not found');
}
const token = user.PasswordResetToken[0].token;
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const resetPasswordLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/reset-password/${token}`;
const template = createElement(ForgotPasswordTemplate, {
assetBaseUrl,
resetPasswordLink,
});
return await mailer.sendMail({
to: {
address: user.email,
name: user.name || '',
},
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
},
subject: 'Forgot Password?',
html: render(template),
text: render(template, { plainText: true }),
});
};

View File

@ -0,0 +1,42 @@
import { createElement } from 'react';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { ResetPasswordTemplate } from '@documenso/email/templates/reset-password';
import { prisma } from '@documenso/prisma';
export interface SendResetPasswordOptions {
userId: number;
}
export const sendResetPassword = async ({ userId }: SendResetPasswordOptions) => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
});
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
console.log({ assetBaseUrl });
const template = createElement(ResetPasswordTemplate, {
assetBaseUrl,
userEmail: user.email,
userName: user.name || '',
});
return await mailer.sendMail({
to: {
address: user.email,
name: user.name || '',
},
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
},
subject: 'Password Reset Success!',
html: render(template),
text: render(template, { plainText: true }),
});
};

View File

@ -0,0 +1,19 @@
'use server';
import { prisma } from '@documenso/prisma';
import { DocumentDataType } from '@documenso/prisma/client';
export type CreateDocumentDataOptions = {
type: DocumentDataType;
data: string;
};
export const createDocumentData = async ({ type, data }: CreateDocumentDataOptions) => {
return await prisma.documentData.create({
data: {
type,
data,
initialData: data,
},
});
};

View File

@ -83,10 +83,7 @@ export const completeDocumentWithToken = async ({
},
});
console.log('documents', documents);
if (documents.count > 0) {
console.log('sealing document');
await sealDocument({ documentId: document.id });
}
};

View File

@ -0,0 +1,19 @@
'use server';
import { prisma } from '@documenso/prisma';
export type CreateDocumentOptions = {
title: string;
userId: number;
documentDataId: string;
};
export const createDocument = async ({ userId, title, documentDataId }: CreateDocumentOptions) => {
return await prisma.document.create({
data: {
title,
documentDataId,
userId,
},
});
};

View File

@ -32,7 +32,7 @@ export const findDocuments = async ({
},
});
const orderByColumn = orderBy?.column ?? 'created';
const orderByColumn = orderBy?.column ?? 'createdAt';
const orderByDirection = orderBy?.direction ?? 'desc';
const termFilters = !term

View File

@ -11,5 +11,8 @@ export const getDocumentById = async ({ id, userId }: GetDocumentByIdOptions) =>
id,
userId,
},
include: {
documentData: true,
},
});
};

View File

@ -17,6 +17,7 @@ export const getDocumentAndSenderByToken = async ({
},
include: {
User: true,
documentData: true,
},
});

View File

@ -1,10 +1,13 @@
'use server';
import path from 'node:path';
import { PDFDocument } from 'pdf-lib';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
import { getFile } from '../../universal/upload/get-file';
import { putFile } from '../../universal/upload/put-file';
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
export type SealDocumentOptions = {
@ -18,8 +21,17 @@ export const sealDocument = async ({ documentId }: SealDocumentOptions) => {
where: {
id: documentId,
},
include: {
documentData: true,
},
});
const { documentData } = document;
if (!documentData) {
throw new Error(`Document ${document.id} has no document data`);
}
if (document.status !== DocumentStatus.COMPLETED) {
throw new Error(`Document ${document.id} has not been completed`);
}
@ -48,27 +60,30 @@ export const sealDocument = async ({ documentId }: SealDocumentOptions) => {
}
// !: Need to write the fields onto the document as a hard copy
const { document: pdfData } = document;
const pdfData = await getFile(documentData);
const doc = await PDFDocument.load(pdfData);
for (const field of fields) {
console.log('inserting field', {
...field,
Signature: null,
});
await insertFieldInPDF(doc, field);
}
const pdfBytes = await doc.save();
await prisma.document.update({
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)),
});
await prisma.documentData.update({
where: {
id: document.id,
status: DocumentStatus.COMPLETED,
id: documentData.id,
},
data: {
document: Buffer.from(pdfBytes).toString('base64'),
data: newData,
},
});
};

View File

@ -48,8 +48,8 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
return;
}
const assetBaseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
const signDocumentLink = `${process.env.NEXT_PUBLIC_SITE_URL}/sign/${recipient.token}`;
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const signDocumentLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${recipient.token}`;
const template = createElement(DocumentInviteEmailTemplate, {
documentName: document.title,

View File

@ -35,15 +35,6 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
const fieldX = pageWidth * (Number(field.positionX) / 100);
const fieldY = pageHeight * (Number(field.positionY) / 100);
console.log({
fieldWidth,
fieldHeight,
fieldX,
fieldY,
pageWidth,
pageHeight,
});
const font = await pdf.embedFont(isSignatureField ? fontCaveat : StandardFonts.Helvetica);
if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) {
@ -75,15 +66,6 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
// Invert the Y axis since PDFs use a bottom-left coordinate system
imageY = pageHeight - imageY - imageHeight;
console.log({
initialDimensions,
scalingFactor,
imageWidth,
imageHeight,
imageX,
imageY,
});
page.drawImage(image, {
x: imageX,
y: imageY,
@ -107,17 +89,6 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
const textX = fieldX + (fieldWidth - textWidth) / 2;
let textY = fieldY + (fieldHeight - textHeight) / 2;
console.log({
initialDimensions,
scalingFactor,
textWidth,
textHeight,
textX,
textY,
pageWidth,
pageHeight,
});
// Invert the Y axis since PDFs use a bottom-left coordinate system
textY = pageHeight - textY - textHeight;

View File

@ -1,8 +1,8 @@
import { nanoid } from 'nanoid';
import { prisma } from '@documenso/prisma';
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
import { nanoid } from '../../universal/id';
export interface SetRecipientsForDocumentOptions {
userId: number;
documentId: number;

View File

@ -9,9 +9,10 @@ export interface CreateUserOptions {
name: string;
email: string;
password: string;
signature?: string | null;
}
export const createUser = async ({ name, email, password }: CreateUserOptions) => {
export const createUser = async ({ name, email, password, signature }: CreateUserOptions) => {
const hashedPassword = await hash(password, SALT_ROUNDS);
const userExists = await prisma.user.findFirst({
@ -29,6 +30,7 @@ export const createUser = async ({ name, email, password }: CreateUserOptions) =
name,
email: email.toLowerCase(),
password: hashedPassword,
signature,
identityProvider: IdentityProvider.DOCUMENSO,
},
});

View File

@ -0,0 +1,53 @@
import crypto from 'crypto';
import { prisma } from '@documenso/prisma';
import { TForgotPasswordFormSchema } from '@documenso/trpc/server/profile-router/schema';
import { ONE_DAY, ONE_HOUR } from '../../constants/time';
import { sendForgotPassword } from '../auth/send-forgot-password';
export const forgotPassword = async ({ email }: TForgotPasswordFormSchema) => {
const user = await prisma.user.findFirst({
where: {
email: {
equals: email,
mode: 'insensitive',
},
},
});
if (!user) {
return;
}
// Find a token that was created in the last hour and hasn't expired
const existingToken = await prisma.passwordResetToken.findFirst({
where: {
userId: user.id,
expiry: {
gt: new Date(),
},
createdAt: {
gt: new Date(Date.now() - ONE_HOUR),
},
},
});
if (existingToken) {
return;
}
const token = crypto.randomBytes(18).toString('hex');
await prisma.passwordResetToken.create({
data: {
token,
expiry: new Date(Date.now() + ONE_DAY),
userId: user.id,
},
});
await sendForgotPassword({
userId: user.id,
}).catch((err) => console.error(err));
};

View File

@ -0,0 +1,19 @@
import { prisma } from '@documenso/prisma';
type GetResetTokenValidityOptions = {
token: string;
};
export const getResetTokenValidity = async ({ token }: GetResetTokenValidityOptions) => {
const found = await prisma.passwordResetToken.findFirst({
select: {
id: true,
expiry: true,
},
where: {
token,
},
});
return !!found && found.expiry > new Date();
};

View File

@ -0,0 +1,62 @@
import { compare, hash } from 'bcrypt';
import { prisma } from '@documenso/prisma';
import { SALT_ROUNDS } from '../../constants/auth';
import { sendResetPassword } from '../auth/send-reset-password';
export type ResetPasswordOptions = {
token: string;
password: string;
};
export const resetPassword = async ({ token, password }: ResetPasswordOptions) => {
if (!token) {
throw new Error('Invalid token provided. Please try again.');
}
const foundToken = await prisma.passwordResetToken.findFirst({
where: {
token,
},
include: {
User: true,
},
});
if (!foundToken) {
throw new Error('Invalid token provided. Please try again.');
}
const now = new Date();
if (now > foundToken.expiry) {
throw new Error('Token has expired. Please try again.');
}
const isSamePassword = await compare(password, foundToken.User.password || '');
if (isSamePassword) {
throw new Error('Your new password cannot be the same as your old password.');
}
const hashedPassword = await hash(password, SALT_ROUNDS);
await prisma.$transaction([
prisma.user.update({
where: {
id: foundToken.userId,
},
data: {
password: hashedPassword,
},
}),
prisma.passwordResetToken.deleteMany({
where: {
userId: foundToken.userId,
},
}),
]);
await sendResetPassword({ userId: foundToken.userId });
};

View File

@ -6,12 +6,7 @@ export type UpdateProfileOptions = {
signature: string;
};
export const updateProfile = async ({
userId,
name,
// TODO: Actually use signature
signature: _signature,
}: UpdateProfileOptions) => {
export const updateProfile = async ({ userId, name, signature }: UpdateProfileOptions) => {
// Existence check
await prisma.user.findFirstOrThrow({
where: {
@ -25,7 +20,7 @@ export const updateProfile = async ({
},
data: {
name,
// signature,
signature,
},
});