mirror of
https://github.com/documenso/documenso.git
synced 2025-11-19 03:01:59 +10:00
Compare commits
7 Commits
fix/incorr
...
fix/446-ca
| Author | SHA1 | Date | |
|---|---|---|---|
| af042a62cd | |||
| df8bdda718 | |||
| 873f99ae86 | |||
| 17fe135027 | |||
| 2eed0ae063 | |||
| f4ae0389d8 | |||
| 9bdff9a61f |
@ -50,7 +50,9 @@ NEXT_PRIVATE_SMTP_SECURE=
|
|||||||
NEXT_PRIVATE_SMTP_FROM_NAME="No Reply @ Documenso"
|
NEXT_PRIVATE_SMTP_FROM_NAME="No Reply @ Documenso"
|
||||||
# REQUIRED: Defines the email address to use as the from address.
|
# REQUIRED: Defines the email address to use as the from address.
|
||||||
NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@documenso.com"
|
NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@documenso.com"
|
||||||
# OPTIONAL: The API key to use for the MailChannels proxy endpoint.
|
# OPTIONAL: The API key to use for Resend.com
|
||||||
|
NEXT_PRIVATE_RESEND_API_KEY=
|
||||||
|
# OPTIONAL: The API key to use for MailChannels.
|
||||||
NEXT_PRIVATE_MAILCHANNELS_API_KEY=
|
NEXT_PRIVATE_MAILCHANNELS_API_KEY=
|
||||||
# OPTIONAL: The endpoint to use for the MailChannels API if using a proxy.
|
# OPTIONAL: The endpoint to use for the MailChannels API if using a proxy.
|
||||||
NEXT_PRIVATE_MAILCHANNELS_ENDPOINT=
|
NEXT_PRIVATE_MAILCHANNELS_ENDPOINT=
|
||||||
|
|||||||
@ -56,6 +56,14 @@ const config = {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
async rewrites() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/ingest/:path*',
|
||||||
|
destination: 'https://eu.posthog.com/:path*',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = withContentlayer(config);
|
module.exports = withContentlayer(config);
|
||||||
|
|||||||
@ -29,6 +29,14 @@ const config = {
|
|||||||
transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
|
transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
async rewrites() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/ingest/:path*',
|
||||||
|
destination: 'https://eu.posthog.com/:path*',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export default function Loading() {
|
|||||||
Documents
|
Documents
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl">
|
<h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl">
|
||||||
Loading Document...
|
Loading Document...
|
||||||
</h1>
|
</h1>
|
||||||
<div className="mt-8 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8">
|
<div className="mt-8 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8">
|
||||||
|
|||||||
@ -65,10 +65,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
|
|||||||
Documents
|
Documents
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<h1
|
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
|
||||||
className="mt-4 max-w-xs truncate text-2xl font-semibold md:text-3xl"
|
|
||||||
title={document.title}
|
|
||||||
>
|
|
||||||
{document.title}
|
{document.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export default function DocumentSentPage() {
|
|||||||
Documents
|
Documents
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl">
|
<h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl">
|
||||||
Loading Document...
|
Loading Document...
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -120,6 +120,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
26
package-lock.json
generated
26
package-lock.json
generated
@ -16958,6 +16958,29 @@
|
|||||||
"node": ">=0.10.5"
|
"node": ">=0.10.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/resend": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resend/-/resend-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-it8TIDVT+/gAiJsUlv2tdHuvzwCCv4Zwu+udDqIm/dIuByQwe68TtFDcPccxqpSVVrNCBxxXLzsdT1tsV+P3GA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-email/render": "0.0.7",
|
||||||
|
"type-fest": "3.13.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/resend/node_modules/type-fest": {
|
||||||
|
"version": "3.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.0.tgz",
|
||||||
|
"integrity": "sha512-Gur3yQGM9qiLNs0KPP7LPgeRbio2QTt4xXouobMCarR0/wyW3F+F/+OWwshg3NG0Adon7uQfSZBpB46NfhoF1A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.2",
|
"version": "1.22.2",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
|
||||||
@ -19773,7 +19796,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-email/components": "^0.0.7",
|
"@react-email/components": "^0.0.7",
|
||||||
"nodemailer": "^6.9.3",
|
"nodemailer": "^6.9.3",
|
||||||
"react-email": "^1.9.4"
|
"react-email": "^1.9.4",
|
||||||
|
"resend": "^1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { createTransport } from 'nodemailer';
|
import { createTransport } from 'nodemailer';
|
||||||
|
|
||||||
import { MailChannelsTransport } from './transports/mailchannels';
|
import { MailChannelsTransport } from './transports/mailchannels';
|
||||||
|
import { ResendTransport } from './transports/resend';
|
||||||
|
|
||||||
const getTransport = () => {
|
const getTransport = () => {
|
||||||
const transport = process.env.NEXT_PRIVATE_SMTP_TRANSPORT ?? 'smtp-auth';
|
const transport = process.env.NEXT_PRIVATE_SMTP_TRANSPORT ?? 'smtp-auth';
|
||||||
@ -14,6 +15,14 @@ const getTransport = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (transport === 'resend') {
|
||||||
|
return createTransport(
|
||||||
|
ResendTransport.makeTransport({
|
||||||
|
apiKey: process.env.NEXT_PRIVATE_RESEND_API_KEY || '',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (transport === 'smtp-api') {
|
if (transport === 'smtp-api') {
|
||||||
if (!process.env.NEXT_PRIVATE_SMTP_HOST || !process.env.NEXT_PRIVATE_SMTP_APIKEY) {
|
if (!process.env.NEXT_PRIVATE_SMTP_HOST || !process.env.NEXT_PRIVATE_SMTP_APIKEY) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@ -19,7 +19,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-email/components": "^0.0.7",
|
"@react-email/components": "^0.0.7",
|
||||||
"nodemailer": "^6.9.3",
|
"nodemailer": "^6.9.3",
|
||||||
"react-email": "^1.9.4"
|
"react-email": "^1.9.4",
|
||||||
|
"resend": "^1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
|
|||||||
145
packages/email/transports/resend.ts
Normal file
145
packages/email/transports/resend.ts
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import { type SentMessageInfo, type Transport } from 'nodemailer';
|
||||||
|
import type Mail from 'nodemailer/lib/mailer';
|
||||||
|
import type MailMessage from 'nodemailer/lib/mailer/mail-message';
|
||||||
|
import { Resend } from 'resend';
|
||||||
|
|
||||||
|
const VERSION = '1.0.0';
|
||||||
|
|
||||||
|
type ResendTransportOptions = {
|
||||||
|
apiKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResendResponseError = {
|
||||||
|
statusCode: number;
|
||||||
|
name: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isResendResponseError = (error: unknown): error is ResendResponseError => {
|
||||||
|
// We could use Zod here, but it's not worth the extra bundle size
|
||||||
|
return (
|
||||||
|
typeof error === 'object' &&
|
||||||
|
error !== null &&
|
||||||
|
'statusCode' in error &&
|
||||||
|
typeof error.statusCode === 'number' &&
|
||||||
|
'name' in error &&
|
||||||
|
typeof error.name === 'string' &&
|
||||||
|
'message' in error &&
|
||||||
|
typeof error.message === 'string'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transport for sending email via the Resend SDK.
|
||||||
|
*/
|
||||||
|
export class ResendTransport implements Transport<SentMessageInfo> {
|
||||||
|
public name = 'ResendMailTransport';
|
||||||
|
public version = VERSION;
|
||||||
|
|
||||||
|
private _client: Resend;
|
||||||
|
private _options: ResendTransportOptions;
|
||||||
|
|
||||||
|
public static makeTransport(options: Partial<ResendTransportOptions>) {
|
||||||
|
return new ResendTransport(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options: Partial<ResendTransportOptions>) {
|
||||||
|
const { apiKey = '' } = options;
|
||||||
|
|
||||||
|
this._options = {
|
||||||
|
apiKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
this._client = new Resend(apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public send(mail: MailMessage, callback: (_err: Error | null, _info: SentMessageInfo) => void) {
|
||||||
|
if (!mail.data.to || !mail.data.from) {
|
||||||
|
return callback(new Error('Missing required fields "to" or "from"'), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._client
|
||||||
|
.sendEmail({
|
||||||
|
subject: mail.data.subject ?? '',
|
||||||
|
from: this.toResendFromAddress(mail.data.from),
|
||||||
|
to: this.toResendAddresses(mail.data.to),
|
||||||
|
cc: this.toResendAddresses(mail.data.cc),
|
||||||
|
bcc: this.toResendAddresses(mail.data.bcc),
|
||||||
|
html: mail.data.html?.toString() || '',
|
||||||
|
text: mail.data.text?.toString() || '',
|
||||||
|
attachments: this.toResendAttachments(mail.data.attachments),
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (isResendResponseError(response)) {
|
||||||
|
throw new Error(`[${response.statusCode}]: ${response.name} ${response.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
callback(error, null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private toResendAddresses(addresses: Mail.Options['to']) {
|
||||||
|
if (!addresses) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof addresses === 'string') {
|
||||||
|
return [addresses];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(addresses)) {
|
||||||
|
return addresses.map((address) => {
|
||||||
|
if (typeof address === 'string') {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
return address.address;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [addresses.address];
|
||||||
|
}
|
||||||
|
|
||||||
|
private toResendFromAddress(address: Mail.Options['from']) {
|
||||||
|
if (!address) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof address === 'string') {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${address.name} <${address.address}>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private toResendAttachments(attachments: Mail.Options['attachments']) {
|
||||||
|
if (!attachments) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return attachments.map((attachment) => {
|
||||||
|
if (!attachment.filename || !attachment.content) {
|
||||||
|
throw new Error('Attachment is missing filename or content');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof attachment.content === 'string') {
|
||||||
|
return {
|
||||||
|
filename: attachment.filename,
|
||||||
|
content: Buffer.from(attachment.content),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachment.content instanceof Buffer) {
|
||||||
|
return {
|
||||||
|
filename: attachment.filename,
|
||||||
|
content: attachment.content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Attachment content must be a string or a buffer');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,10 @@
|
|||||||
{
|
{
|
||||||
"extends": "@documenso/tsconfig/react-library.json",
|
"extends": "@documenso/tsconfig/react-library.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": [
|
||||||
|
"@documenso/tsconfig/process-env.d.ts",
|
||||||
|
]
|
||||||
|
},
|
||||||
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", "**/*.json"],
|
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", "**/*.json"],
|
||||||
"exclude": ["dist", "build", "node_modules"]
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { APP_BASE_URL } from './app';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The flag name for global session recording feature flag.
|
* The flag name for global session recording feature flag.
|
||||||
*/
|
*/
|
||||||
@ -23,7 +25,7 @@ export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
|
|||||||
*/
|
*/
|
||||||
export function extractPostHogConfig(): { key: string; host: string } | null {
|
export function extractPostHogConfig(): { key: string; host: string } | null {
|
||||||
const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
||||||
const postHogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST;
|
const postHogHost = `${APP_BASE_URL}/ingest`;
|
||||||
|
|
||||||
if (!postHogKey || !postHogHost) {
|
if (!postHogKey || !postHogHost) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export const getStats = async ({ user }: GetStatsInput) => {
|
|||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
status: {
|
status: {
|
||||||
in: [ExtendedDocumentStatus.DRAFT, ExtendedDocumentStatus.PENDING],
|
not: ExtendedDocumentStatus.DRAFT,
|
||||||
},
|
},
|
||||||
Recipient: {
|
Recipient: {
|
||||||
some: {
|
some: {
|
||||||
|
|||||||
@ -14,7 +14,6 @@ export const viewedDocument = async ({ token }: ViewedDocumentOptions) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!recipient) {
|
if (!recipient) {
|
||||||
console.warn(`No recipient found for token ${token}`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -57,12 +57,13 @@ export const setRecipientsForDocument = async ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...recipient,
|
...recipient,
|
||||||
...existing,
|
_persisted: existing,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((recipient) => {
|
.filter((recipient) => {
|
||||||
return (
|
return (
|
||||||
recipient.sendStatus !== SendStatus.SENT && recipient.signingStatus !== SigningStatus.SIGNED
|
recipient._persisted?.sendStatus !== SendStatus.SENT &&
|
||||||
|
recipient._persisted?.signingStatus !== SigningStatus.SIGNED
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -72,7 +73,7 @@ export const setRecipientsForDocument = async ({
|
|||||||
linkedRecipients.map((recipient) =>
|
linkedRecipients.map((recipient) =>
|
||||||
prisma.recipient.upsert({
|
prisma.recipient.upsert({
|
||||||
where: {
|
where: {
|
||||||
id: recipient.id ?? -1,
|
id: recipient._persisted?.id ?? -1,
|
||||||
documentId,
|
documentId,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
|
|||||||
4
packages/tsconfig/process-env.d.ts
vendored
4
packages/tsconfig/process-env.d.ts
vendored
@ -27,7 +27,9 @@ declare namespace NodeJS {
|
|||||||
NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS?: string;
|
NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS?: string;
|
||||||
NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING?: string;
|
NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING?: string;
|
||||||
|
|
||||||
NEXT_PRIVATE_SMTP_TRANSPORT?: 'mailchannels' | 'smtp-auth' | 'smtp-api';
|
NEXT_PRIVATE_SMTP_TRANSPORT?: 'mailchannels' | 'resend' | 'smtp-auth' | 'smtp-api';
|
||||||
|
|
||||||
|
NEXT_PRIVATE_RESEND_API_KEY?: string;
|
||||||
|
|
||||||
NEXT_PRIVATE_MAILCHANNELS_API_KEY?: string;
|
NEXT_PRIVATE_MAILCHANNELS_API_KEY?: string;
|
||||||
NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN?: string;
|
NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN?: string;
|
||||||
|
|||||||
@ -50,6 +50,7 @@
|
|||||||
"NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS",
|
"NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS",
|
||||||
"NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING",
|
"NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING",
|
||||||
"NEXT_PRIVATE_SMTP_TRANSPORT",
|
"NEXT_PRIVATE_SMTP_TRANSPORT",
|
||||||
|
"NEXT_PRIVATE_RESEND_API_KEY",
|
||||||
"NEXT_PRIVATE_MAILCHANNELS_API_KEY",
|
"NEXT_PRIVATE_MAILCHANNELS_API_KEY",
|
||||||
"NEXT_PRIVATE_MAILCHANNELS_ENDPOINT",
|
"NEXT_PRIVATE_MAILCHANNELS_ENDPOINT",
|
||||||
"NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN",
|
"NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN",
|
||||||
|
|||||||
Reference in New Issue
Block a user