fix: remove server actions (#684)

This commit is contained in:
Lucas Smith
2023-12-02 09:38:24 +11:00
committed by GitHub
parent 335684d0b7
commit 39c01f4e8d
44 changed files with 2711 additions and 3956 deletions

View File

@ -17,8 +17,8 @@
"@documenso/prisma": "*",
"luxon": "^3.4.0",
"micro": "^10.0.1",
"next": "14.0.0",
"next-auth": "4.24.3",
"next": "14.0.3",
"next-auth": "4.24.5",
"react": "18.2.0",
"ts-pattern": "^5.0.5",
"zod": "^3.22.4"

View File

@ -0,0 +1,17 @@
export * from '@react-email/body';
export * from '@react-email/button';
export * from '@react-email/column';
export * from '@react-email/container';
export * from '@react-email/font';
export * from '@react-email/head';
export * from '@react-email/heading';
export * from '@react-email/hr';
export * from '@react-email/html';
export * from '@react-email/img';
export * from '@react-email/link';
export * from '@react-email/preview';
export * from '@react-email/render';
export * from '@react-email/row';
export * from '@react-email/section';
export * from '@react-email/tailwind';
export * from '@react-email/text';

View File

@ -18,8 +18,23 @@
},
"dependencies": {
"@documenso/nodemailer-resend": "2.0.0",
"@react-email/components": "^0.0.11",
"@react-email/tailwind": "0.0.9",
"@react-email/body": "0.0.4",
"@react-email/button": "0.0.11",
"@react-email/column": "0.0.8",
"@react-email/container": "0.0.10",
"@react-email/font": "0.0.4",
"@react-email/head": "0.0.6",
"@react-email/heading": "0.0.9",
"@react-email/hr": "0.0.6",
"@react-email/html": "0.0.6",
"@react-email/img": "0.0.6",
"@react-email/link": "0.0.6",
"@react-email/preview": "0.0.7",
"@react-email/render": "0.0.9",
"@react-email/row": "0.0.6",
"@react-email/section": "0.0.10",
"@react-email/tailwind": "0.0.13-canary.1",
"@react-email/text": "0.0.6",
"nodemailer": "^6.9.3",
"react-email": "^1.9.5",
"resend": "^2.0.0"
@ -29,8 +44,5 @@
"@documenso/tsconfig": "*",
"@types/nodemailer": "^6.4.8",
"tsup": "^7.1.0"
},
"overrides": {
"@react-email/tailwind": "0.0.9"
}
}

View File

@ -1 +1 @@
export { render } from '@react-email/components';
export { render, renderAsync } from '@react-email/render';

View File

@ -1,7 +1,4 @@
import { Button, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config';
import { Button, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image';
export type TemplateConfirmationEmailProps = {
@ -14,15 +11,7 @@ export const TemplateConfirmationEmail = ({
assetBaseUrl,
}: TemplateConfirmationEmailProps) => {
return (
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<>
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Section className="flex-row items-center justify-center">
@ -47,6 +36,6 @@ export const TemplateConfirmationEmail = ({
</Text>
</Section>
</Section>
</Tailwind>
</>
);
};

View File

@ -1,7 +1,4 @@
import { Button, Column, Img, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config';
import { Button, Column, Img, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image';
export interface TemplateDocumentCompletedProps {
@ -20,15 +17,7 @@ export const TemplateDocumentCompleted = ({
};
return (
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<>
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Section>
@ -72,7 +61,7 @@ export const TemplateDocumentCompleted = ({
</Button>
</Section>
</Section>
</Tailwind>
</>
);
};

View File

@ -1,4 +1,4 @@
import { Column, Img, Row, Section } from '@react-email/components';
import { Column, Img, Row, Section } from '../components';
export interface TemplateDocumentImageProps {
assetBaseUrl: string;

View File

@ -1,7 +1,4 @@
import { Button, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config';
import { Button, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image';
export interface TemplateDocumentInviteProps {
@ -19,15 +16,7 @@ export const TemplateDocumentInvite = ({
assetBaseUrl,
}: TemplateDocumentInviteProps) => {
return (
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<>
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Section>
@ -49,7 +38,7 @@ export const TemplateDocumentInvite = ({
</Button>
</Section>
</Section>
</Tailwind>
</>
);
};

View File

@ -1,7 +1,4 @@
import { Column, Img, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config';
import { Column, Img, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image';
export interface TemplateDocumentPendingProps {
@ -18,15 +15,7 @@ export const TemplateDocumentPending = ({
};
return (
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<>
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Section>
@ -52,7 +41,7 @@ export const TemplateDocumentPending = ({
We'll notify you as soon as it's ready.
</Text>
</Section>
</Tailwind>
</>
);
};

View File

@ -1,7 +1,4 @@
import { Button, Column, Img, Link, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config';
import { Button, Column, Img, Link, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image';
export interface TemplateDocumentSelfSignedProps {
@ -20,15 +17,7 @@ export const TemplateDocumentSelfSigned = ({
};
return (
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<>
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Section className="flex-row items-center justify-center">
@ -84,7 +73,7 @@ export const TemplateDocumentSelfSigned = ({
</Button>
</Section>
</Section>
</Tailwind>
</>
);
};

View File

@ -1,4 +1,4 @@
import { Link, Section, Text } from '@react-email/components';
import { Link, Section, Text } from '../components';
export type TemplateFooterProps = {
isDocument?: boolean;

View File

@ -1,7 +1,4 @@
import { Button, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config';
import { Button, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image';
export type TemplateForgotPasswordProps = {
@ -14,15 +11,7 @@ export const TemplateForgotPassword = ({
assetBaseUrl,
}: TemplateForgotPasswordProps) => {
return (
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<>
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Section className="flex-row items-center justify-center">
@ -43,7 +32,7 @@ export const TemplateForgotPassword = ({
</Button>
</Section>
</Section>
</Tailwind>
</>
);
};

View File

@ -1,7 +1,4 @@
import { Button, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config';
import { Button, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image';
export interface TemplateResetPasswordProps {
@ -12,15 +9,7 @@ export interface TemplateResetPasswordProps {
export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordProps) => {
return (
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<>
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Section className="flex-row items-center justify-center">
@ -41,7 +30,7 @@ export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordPro
</Button>
</Section>
</Section>
</Tailwind>
</>
);
};

View File

@ -1,20 +1,8 @@
import {
Body,
Container,
Head,
Html,
Img,
Preview,
Section,
Tailwind,
} from '@react-email/components';
import config from '@documenso/tailwind-config';
import {
TemplateConfirmationEmail,
TemplateConfirmationEmailProps,
} from '../template-components/template-confirmation-email';
import { Body, Container, Head, Html, Img, Preview, Section, Tailwind } from '../components';
import type { TemplateConfirmationEmailProps } from '../template-components/template-confirmation-email';
import { TemplateConfirmationEmail } from '../template-components/template-confirmation-email';
import { TemplateFooter } from '../template-components/template-footer';
export const ConfirmEmailTemplate = ({

View File

@ -1,20 +1,8 @@
import {
Body,
Container,
Head,
Html,
Img,
Preview,
Section,
Tailwind,
} from '@react-email/components';
import config from '@documenso/tailwind-config';
import {
TemplateDocumentCompleted,
TemplateDocumentCompletedProps,
} from '../template-components/template-document-completed';
import { Body, Container, Head, Html, Img, Preview, Section, Tailwind } from '../components';
import type { TemplateDocumentCompletedProps } from '../template-components/template-document-completed';
import { TemplateDocumentCompleted } from '../template-components/template-document-completed';
import { TemplateFooter } from '../template-components/template-footer';
export type DocumentCompletedEmailTemplateProps = Partial<TemplateDocumentCompletedProps>;

View File

@ -1,3 +1,5 @@
import config from '@documenso/tailwind-config';
import {
Body,
Container,
@ -10,14 +12,9 @@ import {
Section,
Tailwind,
Text,
} from '@react-email/components';
import config from '@documenso/tailwind-config';
import {
TemplateDocumentInvite,
TemplateDocumentInviteProps,
} from '../template-components/template-document-invite';
} from '../components';
import type { TemplateDocumentInviteProps } from '../template-components/template-document-invite';
import { TemplateDocumentInvite } from '../template-components/template-document-invite';
import { TemplateFooter } from '../template-components/template-footer';
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & {

View File

@ -1,20 +1,8 @@
import {
Body,
Container,
Head,
Html,
Img,
Preview,
Section,
Tailwind,
} from '@react-email/components';
import config from '@documenso/tailwind-config';
import {
TemplateDocumentPending,
TemplateDocumentPendingProps,
} from '../template-components/template-document-pending';
import { Body, Container, Head, Html, Img, Preview, Section, Tailwind } from '../components';
import type { TemplateDocumentPendingProps } from '../template-components/template-document-pending';
import { TemplateDocumentPending } from '../template-components/template-document-pending';
import { TemplateFooter } from '../template-components/template-footer';
export type DocumentPendingEmailTemplateProps = Partial<TemplateDocumentPendingProps>;

View File

@ -1,20 +1,8 @@
import {
Body,
Container,
Head,
Html,
Img,
Preview,
Section,
Tailwind,
} from '@react-email/components';
import config from '@documenso/tailwind-config';
import {
TemplateDocumentSelfSigned,
TemplateDocumentSelfSignedProps,
} from '../template-components/template-document-self-signed';
import { Body, Container, Head, Html, Img, Preview, Section, Tailwind } from '../components';
import type { TemplateDocumentSelfSignedProps } from '../template-components/template-document-self-signed';
import { TemplateDocumentSelfSigned } from '../template-components/template-document-self-signed';
import { TemplateFooter } from '../template-components/template-footer';
export type DocumentSelfSignedTemplateProps = TemplateDocumentSelfSignedProps;

View File

@ -1,21 +1,9 @@
import {
Body,
Container,
Head,
Html,
Img,
Preview,
Section,
Tailwind,
} from '@react-email/components';
import config from '@documenso/tailwind-config';
import { Body, Container, Head, Html, Img, Preview, Section, Tailwind } from '../components';
import { TemplateFooter } from '../template-components/template-footer';
import {
TemplateForgotPassword,
TemplateForgotPasswordProps,
} from '../template-components/template-forgot-password';
import type { TemplateForgotPasswordProps } from '../template-components/template-forgot-password';
import { TemplateForgotPassword } from '../template-components/template-forgot-password';
export type ForgotPasswordTemplateProps = Partial<TemplateForgotPasswordProps>;

View File

@ -1,3 +1,5 @@
import config from '@documenso/tailwind-config';
import {
Body,
Container,
@ -10,15 +12,10 @@ import {
Section,
Tailwind,
Text,
} from '@react-email/components';
import config from '@documenso/tailwind-config';
} from '../components';
import { TemplateFooter } from '../template-components/template-footer';
import {
TemplateResetPassword,
TemplateResetPasswordProps,
} from '../template-components/template-reset-password';
import type { TemplateResetPasswordProps } from '../template-components/template-reset-password';
import { TemplateResetPassword } from '../template-components/template-reset-password';
export type ResetPasswordTemplateProps = Partial<TemplateResetPasswordProps>;

View File

@ -34,8 +34,8 @@
"bcrypt": "^5.1.0",
"luxon": "^3.4.0",
"nanoid": "^4.0.2",
"next": "14.0.0",
"next-auth": "4.24.3",
"next": "14.0.3",
"next-auth": "4.24.5",
"oslo": "^0.17.0",
"pdf-lib": "^1.17.1",
"react": "18.2.0",

View File

@ -54,6 +54,7 @@ export const signFieldWithToken = async ({
field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE;
let customText = !isSignatureField ? value : undefined;
const signatureImageAsBase64 = isSignatureField && isBase64 ? value : undefined;
const typedSignature = isSignatureField && !isBase64 ? value : undefined;
@ -61,29 +62,48 @@ export const signFieldWithToken = async ({
customText = DateTime.now().toFormat('yyyy-MM-dd hh:mm a');
}
await prisma.field.update({
where: {
id: field.id,
},
data: {
customText,
inserted: true,
Signature: isSignatureField
? {
upsert: {
create: {
recipientId: field.recipientId,
signatureImageAsBase64,
typedSignature,
},
update: {
recipientId: field.recipientId,
signatureImageAsBase64,
typedSignature,
},
},
}
: undefined,
},
if (isSignatureField && !signatureImageAsBase64 && !typedSignature) {
throw new Error('Signature field must have a signature');
}
return await prisma.$transaction(async (tx) => {
const updatedField = await tx.field.update({
where: {
id: field.id,
},
data: {
customText,
inserted: true,
},
});
if (isSignatureField) {
if (!field.recipientId) {
throw new Error('Field has no recipientId');
}
const signature = await tx.signature.upsert({
where: {
fieldId: field.id,
},
create: {
fieldId: field.id,
recipientId: field.recipientId,
signatureImageAsBase64: signatureImageAsBase64,
typedSignature: typedSignature,
},
update: {
signatureImageAsBase64: signatureImageAsBase64,
typedSignature: typedSignature,
},
});
// Dirty but I don't want to deal with type information
Object.assign(updatedField, {
Signature: signature,
});
}
return updatedField;
});
};

View File

@ -9,7 +9,7 @@
"dependencies": {
"autoprefixer": "^10.4.13",
"postcss": "^8.4.21",
"tailwindcss": "^3.2.7",
"tailwindcss": "3.3.2",
"tailwindcss-animate": "^1.0.5"
},
"devDependencies": {

View File

@ -1,6 +1,7 @@
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 { duplicateDocumentById } from '@documenso/lib/server-only/document/duplicate-document-by-id';
@ -119,7 +120,7 @@ export const documentRouter = router({
.input(ZSetTitleForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
const { documentId, title } = input;
const userId = ctx.user.id;
return await updateTitle({
@ -176,7 +177,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,

View File

@ -65,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({

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

@ -3,6 +3,7 @@ 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';
@ -13,6 +14,7 @@ export const appRouter = router({
profile: profileRouter,
document: documentRouter,
field: fieldRouter,
recipient: recipientRouter,
admin: adminRouter,
shareLink: shareLinkRouter,
singleplayer: singleplayerRouter,

View File

@ -3,7 +3,7 @@ import { createElement } from 'react';
import { PDFDocument } from 'pdf-lib';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
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';
@ -36,6 +36,7 @@ export const singleplayerRouter = router({
});
const doc = await PDFDocument.load(document);
const createdAt = new Date();
const isBase64 = signer.signature.startsWith('data:image/png;base64,');
@ -149,6 +150,11 @@ export const singleplayerRouter = router({
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: {
@ -160,8 +166,8 @@ export const singleplayerRouter = router({
address: FROM_ADDRESS,
},
subject: 'Document signed',
html: render(template),
text: render(template, { plainText: true }),
html,
text,
attachments: [{ content: signedPdfBuffer, filename: documentName }],
});

View File

@ -62,7 +62,7 @@
"framer-motion": "^10.12.8",
"lucide-react": "^0.279.0",
"luxon": "^3.4.2",
"next": "14.0.0",
"next": "14.0.3",
"pdfjs-dist": "3.6.172",
"react-day-picker": "^8.7.1",
"react-hook-form": "^7.45.4",