feat: use server-actions for authoring flow

This change actually makes the authoring flow work for
the most part by tying in emailing and more.

We have also done a number of quality of life updates to
simplify the codebase overall making it easier to continue
work on the refresh.
This commit is contained in:
Mythie
2023-07-26 18:52:53 +10:00
parent 5e3752cdbf
commit 12d8cebd4c
54 changed files with 2890 additions and 860 deletions

View File

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

50
packages/email/mailer.ts Normal file
View File

@ -0,0 +1,50 @@
import { createTransport } from 'nodemailer';
import { MailChannelsTransport } from './transports/mailchannels';
const getTransport = () => {
const transport = process.env.NEXT_PRIVATE_SMTP_TRANSPORT ?? 'smtp-auth';
if (transport === 'mailchannels') {
return createTransport(
MailChannelsTransport.makeTransport({
apiKey: process.env.NEXT_PRIVATE_MAILCHANNELS_API_KEY,
endpoint: process.env.NEXT_PRIVATE_MAILCHANNELS_ENDPOINT,
}),
);
}
if (transport === 'smtp-api') {
if (!process.env.NEXT_PRIVATE_SMTP_HOST || !process.env.NEXT_PRIVATE_SMTP_APIKEY) {
throw new Error(
'SMTP API transport requires NEXT_PRIVATE_SMTP_HOST and NEXT_PRIVATE_SMTP_APIKEY',
);
}
return createTransport({
host: process.env.NEXT_PRIVATE_SMTP_HOST,
port: Number(process.env.NEXT_PRIVATE_SMTP_PORT) || 587,
secure: process.env.NEXT_PRIVATE_SMTP_SECURE === 'true',
auth: {
user: process.env.NEXT_PRIVATE_SMTP_APIKEY_USER ?? 'apikey',
pass: process.env.NEXT_PRIVATE_SMTP_APIKEY ?? '',
},
});
}
if (!process.env.NEXT_PRIVATE_SMTP_HOST) {
throw new Error('SMTP transport requires NEXT_PRIVATE_SMTP_HOST');
}
return createTransport({
host: process.env.NEXT_PRIVATE_SMTP_HOST,
port: Number(process.env.NEXT_PRIVATE_SMTP_PORT) || 587,
secure: process.env.NEXT_PRIVATE_SMTP_SECURE === 'true',
auth: {
user: process.env.NEXT_PRIVATE_SMTP_USERNAME ?? '',
pass: process.env.NEXT_PRIVATE_SMTP_PASSWORD ?? '',
},
});
};
export const mailer = getTransport();

View File

@ -5,18 +5,26 @@
"types": "./index.ts",
"license": "MIT",
"files": [
"templates/"
"templates/",
"transports/",
"mailer.ts",
"render.ts",
"index.ts"
],
"scripts": {
"dev": "email dev --port 3002 --dir templates"
"dev": "email dev --port 3002 --dir templates",
"worker:test": "tsup worker/index.ts --format esm"
},
"dependencies": {
"@documenso/tsconfig": "*",
"@documenso/tailwind-config": "*",
"@documenso/ui": "*",
"@react-email/components": "^0.0.7"
"@react-email/components": "^0.0.7",
"nodemailer": "^6.9.3"
},
"devDependencies": {
"react-email": "^1.9.4"
"@types/nodemailer": "^6.4.8",
"react-email": "^1.9.4",
"tsup": "^7.1.0"
}
}

1
packages/email/render.ts Normal file
View File

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

View File

@ -1,6 +1,5 @@
import {
Body,
Button,
Container,
Head,
Html,

View File

@ -0,0 +1,154 @@
import { SentMessageInfo, Transport } from 'nodemailer';
import type { Address } from 'nodemailer/lib/mailer';
import type MailMessage from 'nodemailer/lib/mailer/mail-message';
const VERSION = '1.0.0';
type NodeMailerAddress = string | Address | Array<string | Address> | undefined;
interface MailChannelsAddress {
email: string;
name?: string;
}
interface MailChannelsTransportOptions {
apiKey: string;
endpoint: string;
}
/**
* Transport for sending email through MailChannels via Cloudflare Workers.
*
* Optionally allows specifying a custom endpoint and API key so you can setup a worker
* to proxy requests to MailChannels with added security.
*
* @see https://blog.cloudflare.com/sending-email-from-workers-with-mailchannels/
*/
export class MailChannelsTransport implements Transport<SentMessageInfo> {
public name = 'CloudflareMailTransport';
public version = VERSION;
private _options: MailChannelsTransportOptions;
public static makeTransport(options: Partial<MailChannelsTransportOptions>) {
return new MailChannelsTransport(options);
}
constructor(options: Partial<MailChannelsTransportOptions>) {
const { apiKey = '', endpoint = 'https://api.mailchannels.net/tx/v1/send' } = options;
this._options = {
apiKey,
endpoint,
};
}
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);
}
const mailTo = this.toMailChannelsAddresses(mail.data.to);
const mailCc = this.toMailChannelsAddresses(mail.data.cc);
const mailBcc = this.toMailChannelsAddresses(mail.data.bcc);
const from: MailChannelsAddress =
typeof mail.data.from === 'string'
? { email: mail.data.from }
: {
email: mail.data.from?.address,
name: mail.data.from?.name,
};
const requestHeaders: Record<string, string> = {
'Content-Type': 'application/json',
};
if (this._options.apiKey) {
requestHeaders['X-Auth-Token'] = this._options.apiKey;
}
fetch(this._options.endpoint, {
method: 'POST',
headers: requestHeaders,
body: JSON.stringify({
from: from,
subject: mail.data.subject,
personalizations: [
{
to: mailTo,
cc: mailCc.length > 0 ? mailCc : undefined,
bcc: mailBcc.length > 0 ? mailBcc : undefined,
dkim_domain: process.env.NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN || undefined,
dkim_selector: process.env.NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR || undefined,
dkim_private_key: process.env.NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY || undefined,
},
],
content: [
{
type: 'text/plain',
value: mail.data.text?.toString('utf-8') ?? '',
},
{
type: 'text/html',
value: mail.data.html?.toString('utf-8') ?? '',
},
],
}),
})
.then((res) => {
if (res.status >= 200 && res.status <= 299) {
return callback(null, {
messageId: '',
envelope: {
from: mail.data.from,
to: mail.data.to,
},
accepted: mail.data.to,
rejected: [],
pending: [],
});
}
res.json().then((data) => {
return callback(new Error(`MailChannels error: ${data.message}`), null);
});
})
.catch((err) => {
return callback(err, null);
});
}
/**
* Converts a nodemailer address(s) to an array of MailChannel compatible address.
*/
private toMailChannelsAddresses(address: NodeMailerAddress): Array<MailChannelsAddress> {
if (!address) {
return [];
}
if (typeof address === 'string') {
return [{ email: address }];
}
if (Array.isArray(address)) {
return address.map((address) => {
if (typeof address === 'string') {
return { email: address };
}
return {
email: address.address,
name: address.name,
};
});
}
return [
{
email: address.address,
name: address.name,
},
];
}
}