Skip to content
This repository has been archived by the owner on Jan 16, 2025. It is now read-only.

Commit

Permalink
feat: introduce email-templates; redis; bullmq (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
dimitrisnl authored Oct 15, 2023
1 parent 693d9ec commit 2679783
Show file tree
Hide file tree
Showing 38 changed files with 4,464 additions and 1,213 deletions.
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ _shared
node_modules
tmp
dist
.cache
.cache
pnpm-lock.yaml
32 changes: 25 additions & 7 deletions apps/dashboard/app/database/db.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,31 @@ const envValidationSchema = Schema.struct({
// Throw on-load if missing
const config = Schema.parseSync(envValidationSchema)(process.env);

const pool = new Pool({
user: config.DB_USER,
host: config.DB_HOST,
database: config.DB_NAME,
password: config.DB_PASSWORD,
port: config.DB_PORT,
});
function makePool() {
return new Pool({
user: config.DB_USER,
host: config.DB_HOST,
database: config.DB_NAME,
password: config.DB_PASSWORD,
port: config.DB_PORT,
});
}

let pool: ReturnType<typeof makePool>;

declare global {
// eslint-disable-next-line no-var
var __pool: ReturnType<typeof makePool> | undefined;
}

if (process.env.NODE_ENV === 'production') {
pool = makePool();
} else {
if (!global.__pool) {
global.__pool = makePool();
}
pool = global.__pool;
}

pool.on('error', (err) => {
// don't let a pg restart kill the app
Expand Down
12 changes: 12 additions & 0 deletions apps/dashboard/app/mailer/build-template.server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {render} from '@white-label/email-templates';
import * as Effect from 'effect/Effect';

export function buildTemplate(template: React.ReactElement) {
return Effect.try({
// eslint-disable-next-line
try: () => render(template),
catch: () => {
return Effect.fail('Failed to render email template');
},
});
}
20 changes: 20 additions & 0 deletions apps/dashboard/app/mailer/config.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'dotenv/config';

import * as Schema from '@effect/schema/Schema';

const envValidationSchema = Schema.struct({
SMTP_HOST: Schema.string.pipe(Schema.minLength(2)),
SMTP_USER: Schema.string.pipe(Schema.minLength(2)),
SMTP_PASSWORD: Schema.string.pipe(Schema.minLength(2)),
SMTP_PORT: Schema.NumberFromString,
SMTP_SECURE: Schema.string.pipe(Schema.nonEmpty()),

EMAIL_FROM: Schema.string.pipe(Schema.minLength(5), Schema.endsWith('.com')),
DASHBOARD_URL: Schema.string.pipe(
Schema.minLength(5),
Schema.startsWith('http')
),
});

// Throw on-load if missing
export const config = Schema.parseSync(envValidationSchema)(process.env);
52 changes: 52 additions & 0 deletions apps/dashboard/app/mailer/emails/send-invitation-email.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {InvitationEmailTemplate} from '@white-label/email-templates';
import * as Effect from 'effect/Effect';
import {pipe} from 'effect/Function';

import {addEmailJob} from '@/queues/email-queue';

import {buildTemplate} from '../build-template.server';
import {config} from '../config.server';

export function sendInvitationEmail({
email,
orgName,
invitationTokenId,
}: {
email: string;
orgName: string;
invitationTokenId: string;
}) {
return Effect.gen(function* (_) {
yield* _(Effect.log('Mailer(invitation-email): Preparing email'));
// eslint-disable-next-line
const html = yield* _(
buildTemplate(
<InvitationEmailTemplate
orgName={orgName}
dashboardUrl={config.DASHBOARD_URL}
invitationDeclineUrl={`${config.DASHBOARD_URL}/invitation/decline?invitationId=${invitationTokenId}`}
/>
)
);

const payload = {
to: email,
subject: `You've been invited to join ${orgName}`,
content: html,
};

yield* _(addEmailJob('invitation-email', payload));
yield* _(Effect.log('Mailer(invitation-email): Sending email'));
}).pipe(
Effect.catchAll((error) =>
pipe(
Effect.log(
`Mailer(invitation-email): Failed to send email to ${email}`
),
Effect.flatMap(() => Effect.log(error)),
// suppress error
Effect.flatMap(() => Effect.unit)
)
)
);
}
49 changes: 49 additions & 0 deletions apps/dashboard/app/mailer/emails/send-password-reset-email.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {PasswordResetEmailTemplate} from '@white-label/email-templates';
import * as Effect from 'effect/Effect';
import {pipe} from 'effect/Function';

import {addEmailJob} from '@/queues/email-queue';

import {buildTemplate} from '../build-template.server';
import {config} from '../config.server';

export function sendPasswordResetEmail({
email,
passwordResetTokenId,
}: {
email: string;
passwordResetTokenId: string;
}) {
return Effect.gen(function* (_) {
yield* _(Effect.log('Mailer(password-reset-email): Preparing email'));
// eslint-disable-next-line
const html = yield* _(
buildTemplate(
<PasswordResetEmailTemplate
dashboardUrl={config.DASHBOARD_URL}
passwordResetUrl={`${config.DASHBOARD_URL}/password/reset-password?token=${passwordResetTokenId}`}
/>
)
);

const payload = {
to: email,
subject: `Reset your password`,
content: html,
};

yield* _(addEmailJob('password-reset-email', payload));
yield* _(Effect.log('Mailer(password-reset-email): Sending email'));
}).pipe(
Effect.catchAll((error) =>
pipe(
Effect.log(
`Mailer(password-reset-email): Failed to send email to ${email}`
),
Effect.flatMap(() => Effect.log(error)),
// suppress error
Effect.flatMap(() => Effect.unit)
)
)
);
}
51 changes: 51 additions & 0 deletions apps/dashboard/app/mailer/emails/send-verification-email.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {VerificationEmailTemplate} from '@white-label/email-templates';
import * as Effect from 'effect/Effect';
import {pipe} from 'effect/Function';

import {addEmailJob} from '@/queues/email-queue';

import {buildTemplate} from '../build-template.server';
import {config} from '../config.server';

export function sendVerificationEmail({
email,
verifyEmailTokenId,
}: {
email: string;
verifyEmailTokenId: string;
}) {
return Effect.gen(function* (_) {
yield* _(
Effect.log(`Mailer(verification-email): Sending email to ${email}`)
);
// eslint-disable-next-line
const html = yield* _(
buildTemplate(
<VerificationEmailTemplate
dashboardUrl={config.DASHBOARD_URL}
verificationUrl={`${config.DASHBOARD_URL}/email/verify-email?token=${verifyEmailTokenId}`}
/>
)
);

const payload = {
to: email,
subject: `Verify your email`,
content: html,
};

yield* _(addEmailJob('password-reset-email', payload));
yield* _(Effect.log(`Mailer(verification-email): Sent email to ${email}`));
}).pipe(
Effect.catchAll((error) =>
pipe(
Effect.log(
`Mailer(verification-email): Failed to send email to ${email}`
),
Effect.flatMap(() => Effect.log(error)),
// suppress error
Effect.flatMap(() => Effect.unit)
)
)
);
}
77 changes: 0 additions & 77 deletions apps/dashboard/app/mailer/index.ts

This file was deleted.

20 changes: 20 additions & 0 deletions apps/dashboard/app/mailer/send-email.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// import * as Effect from 'effect/Effect';

import {config} from './config.server';
import {transporter} from './transporter.server';

interface SendEmailProps {
to: string;
subject: string;
content: string;
}

// todo: pass through context
export function sendEmail(props: SendEmailProps) {
return transporter.sendMail({
from: config.EMAIL_FROM,
to: props.to,
subject: props.subject,
html: props.content,
});
}
34 changes: 34 additions & 0 deletions apps/dashboard/app/mailer/transporter.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {createTransport} from 'nodemailer';

import {config} from './config.server';

function makeTransporter() {
return createTransport({
host: config.SMTP_HOST,
port: config.SMTP_PORT,
secure: config.SMTP_SECURE === 'true',
auth: {
user: config.SMTP_USER,
pass: config.SMTP_PASSWORD,
},
pool: true,
});
}

let transporter: ReturnType<typeof makeTransporter>;

declare global {
// eslint-disable-next-line no-var
var __transporter: ReturnType<typeof makeTransporter> | undefined;
}

if (process.env.NODE_ENV === 'production') {
transporter = makeTransporter();
} else {
if (!global.__transporter) {
global.__transporter = makeTransporter();
}
transporter = global.__transporter;
}

export {transporter};
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ export function requestPasswordReset() {

const user = yield* _(User.dbRecordToDomain(userRecord));

const resetPasswordTokenId = yield* _(Uuid.generate());
yield* _(createPasswordResetToken(resetPasswordTokenId, user.id));
const passwordResetTokenId = yield* _(Uuid.generate());
yield* _(createPasswordResetToken(passwordResetTokenId, user.id));

return {email: user.email, resetPasswordTokenId};
return {email: user.email, passwordResetTokenId};
}).pipe(
Effect.catchTags({
DatabaseError: () => Effect.fail(new InternalServerError()),
Expand Down
Loading

0 comments on commit 2679783

Please sign in to comment.