From 940fc7e58318dc9326c2b6b78a4d98a460bf2f8a Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 27 Sep 2024 13:36:42 +0700 Subject: [PATCH] feat: choosable email --- .../backend/src/api/routes/auth.controller.ts | 8 ++- .../configuration/configuration.checker.ts | 1 - .../notifications/notification.service.ts | 4 ++ .../organizations/organization.repository.ts | 5 +- .../organizations/organization.service.ts | 2 +- .../src/emails/email.interface.ts | 5 ++ .../src/emails/empty.provider.ts | 9 ++++ .../src/emails/node.mailer.provider.ts | 40 +++++++++++++++ .../src/emails/resend.provider.ts | 19 +++++++ .../src/services/email.service.ts | 51 ++++++++++++++----- package-lock.json | 18 +++++++ package.json | 2 + 12 files changed, 144 insertions(+), 20 deletions(-) create mode 100644 libraries/nestjs-libraries/src/emails/email.interface.ts create mode 100644 libraries/nestjs-libraries/src/emails/empty.provider.ts create mode 100644 libraries/nestjs-libraries/src/emails/node.mailer.provider.ts create mode 100644 libraries/nestjs-libraries/src/emails/resend.provider.ts diff --git a/apps/backend/src/api/routes/auth.controller.ts b/apps/backend/src/api/routes/auth.controller.ts index 1a0a8af2..ba394cc4 100644 --- a/apps/backend/src/api/routes/auth.controller.ts +++ b/apps/backend/src/api/routes/auth.controller.ts @@ -8,11 +8,15 @@ import { ForgotReturnPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/for import { ForgotPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot.password.dto'; import { ApiTags } from '@nestjs/swagger'; import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management'; +import { EmailService } from '@gitroom/nestjs-libraries/services/email.service'; @ApiTags('Auth') @Controller('/auth') export class AuthController { - constructor(private _authService: AuthService) {} + constructor( + private _authService: AuthService, + private _emailService: EmailService + ) {} @Post('/register') async register( @Req() req: Request, @@ -30,7 +34,7 @@ export class AuthController { getOrgFromCookie ); - const activationRequired = body.provider === 'LOCAL' && !!process.env.RESEND_API_KEY; + const activationRequired = body.provider === 'LOCAL' && this._emailService.hasProvider(); if (activationRequired) { response.header('activate', 'true'); diff --git a/libraries/helpers/src/configuration/configuration.checker.ts b/libraries/helpers/src/configuration/configuration.checker.ts index b6a1ca6d..425de8ad 100644 --- a/libraries/helpers/src/configuration/configuration.checker.ts +++ b/libraries/helpers/src/configuration/configuration.checker.ts @@ -30,7 +30,6 @@ export class ConfigurationChecker { this.checkIsValidUrl('FRONTEND_URL') this.checkIsValidUrl('NEXT_PUBLIC_BACKEND_URL') this.checkIsValidUrl('BACKEND_INTERNAL_URL') - this.checkNonEmpty('RESEND_API_KEY', 'Needed to send user activation emails.') this.checkNonEmpty('CLOUDFLARE_ACCOUNT_ID', 'Needed to setup providers.') this.checkNonEmpty('CLOUDFLARE_ACCESS_KEY', 'Needed to setup providers.') this.checkNonEmpty('CLOUDFLARE_SECRET_ACCESS_KEY', 'Needed to setup providers.') diff --git a/libraries/nestjs-libraries/src/database/prisma/notifications/notification.service.ts b/libraries/nestjs-libraries/src/database/prisma/notifications/notification.service.ts index bbe8d5b7..52f9ce3c 100644 --- a/libraries/nestjs-libraries/src/database/prisma/notifications/notification.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/notifications/notification.service.ts @@ -40,4 +40,8 @@ export class NotificationService { async sendEmail(to: string, subject: string, html: string) { await this._emailService.sendEmail(to, subject, html); } + + hasEmailProvider() { + return this._emailService.hasProvider(); + } } diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts index fd62013a..85084600 100644 --- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts @@ -177,7 +177,8 @@ export class OrganizationRepository { } async createOrgAndUser( - body: Omit & { providerId?: string } + body: Omit & { providerId?: string }, + hasEmail: boolean ) { return this._organization.model.organization.create({ data: { @@ -187,7 +188,7 @@ export class OrganizationRepository { role: Role.SUPERADMIN, user: { create: { - activated: body.provider !== 'LOCAL' || !process.env.RESEND_API_KEY, + activated: body.provider !== 'LOCAL' || !hasEmail, email: body.email, password: body.password ? AuthService.hashPassword(body.password) diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts index 18c947fe..db239ac3 100644 --- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts @@ -17,7 +17,7 @@ export class OrganizationService { async createOrgAndUser( body: Omit & { providerId?: string } ) { - return this._organizationRepository.createOrgAndUser(body); + return this._organizationRepository.createOrgAndUser(body, this._notificationsService.hasEmailProvider()); } addUserToOrg( diff --git a/libraries/nestjs-libraries/src/emails/email.interface.ts b/libraries/nestjs-libraries/src/emails/email.interface.ts new file mode 100644 index 00000000..7e6acbc0 --- /dev/null +++ b/libraries/nestjs-libraries/src/emails/email.interface.ts @@ -0,0 +1,5 @@ +export interface EmailInterface { + name: string; + validateEnvKeys: string[]; + sendEmail(to: string, subject: string, html: string, emailFromName: string, emailFromAddress: string): Promise; +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/emails/empty.provider.ts b/libraries/nestjs-libraries/src/emails/empty.provider.ts new file mode 100644 index 00000000..c146c7ba --- /dev/null +++ b/libraries/nestjs-libraries/src/emails/empty.provider.ts @@ -0,0 +1,9 @@ +import { EmailInterface } from "./email.interface"; + +export class EmptyProvider implements EmailInterface { + name = 'no provider'; + validateEnvKeys = []; + async sendEmail(to: string, subject: string, html: string) { + return `No email provider found, email was supposed to be sent to ${to} with subject: ${subject} and ${html}, html`; + } +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/emails/node.mailer.provider.ts b/libraries/nestjs-libraries/src/emails/node.mailer.provider.ts new file mode 100644 index 00000000..b0b8ca65 --- /dev/null +++ b/libraries/nestjs-libraries/src/emails/node.mailer.provider.ts @@ -0,0 +1,40 @@ +import nodemailer from 'nodemailer'; +import { EmailInterface } from '@gitroom/nestjs-libraries/emails/email.interface'; + +const transporter = nodemailer.createTransport({ + host: process.env.EMAIL_HOST, + port: +process.env.EMAIL_PORT!, + secure: process.env.EMAIL_SECURE === 'true', + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, +}); + +export class NodeMailerProvider implements EmailInterface { + name = 'nodemailer'; + validateEnvKeys = [ + 'EMAIL_HOST', + 'EMAIL_PORT', + 'EMAIL_SECURE', + 'EMAIL_USER', + 'EMAIL_PASS', + ]; + async sendEmail( + to: string, + subject: string, + html: string, + emailFromName: string, + emailFromAddress: string + ) { + const sends = await transporter.sendMail({ + from:`${emailFromName} <${emailFromAddress}>`, // sender address + to: to, // list of receivers + subject: subject, // Subject line + text: html, // plain text body + html: html, // html body + }); + + return sends; + } +} diff --git a/libraries/nestjs-libraries/src/emails/resend.provider.ts b/libraries/nestjs-libraries/src/emails/resend.provider.ts new file mode 100644 index 00000000..15e34cf5 --- /dev/null +++ b/libraries/nestjs-libraries/src/emails/resend.provider.ts @@ -0,0 +1,19 @@ +import { Resend } from 'resend'; +import { EmailInterface } from '@gitroom/nestjs-libraries/emails/email.interface'; + +const resend = new Resend(process.env.RESEND_API_KEY || 're_132'); + +export class ResendProvider implements EmailInterface { + name = 'resend'; + validateEnvKeys = ['RESEND_API_KEY']; + async sendEmail(to: string, subject: string, html: string, emailFromName: string, emailFromAddress: string) { + const sends = await resend.emails.send({ + from: `${emailFromName} <${emailFromAddress}>`, + to, + subject, + html, + }); + + return sends; + } +} diff --git a/libraries/nestjs-libraries/src/services/email.service.ts b/libraries/nestjs-libraries/src/services/email.service.ts index 037a72fd..41f588c4 100644 --- a/libraries/nestjs-libraries/src/services/email.service.ts +++ b/libraries/nestjs-libraries/src/services/email.service.ts @@ -1,29 +1,52 @@ import { Injectable } from '@nestjs/common'; -import { Resend } from 'resend'; - -const resend = new Resend(process.env.RESEND_API_KEY || 're_132'); +import { EmailInterface } from '@gitroom/nestjs-libraries/emails/email.interface'; +import { ResendProvider } from '@gitroom/nestjs-libraries/emails/resend.provider'; +import { EmptyProvider } from '@gitroom/nestjs-libraries/emails/empty.provider'; +import { NodeMailerProvider } from '@gitroom/nestjs-libraries/emails/node.mailer.provider'; @Injectable() export class EmailService { + emailService: EmailInterface; + constructor() { + this.emailService = this.selectProvider(process.env.EMAIL_PROVIDER!); + console.log('Email service provider:', this.emailService.name); + for (const key of this.emailService.validateEnvKeys) { + if (!process.env[key]) { + console.error(`Missing environment variable: ${key}`); + } + } + } + + hasProvider() { + return !(this.emailService instanceof EmptyProvider); + } + + selectProvider(provider: string) { + switch (provider) { + case 'resend': + return new ResendProvider(); + case 'nodemailer': + return new NodeMailerProvider(); + default: + return new EmptyProvider(); + } + } + async sendEmail(to: string, subject: string, html: string) { - if (!process.env.RESEND_API_KEY) { - console.log('No Resend API Key found, skipping email sending'); - return; - } - if (!process.env.EMAIL_FROM_ADDRESS || !process.env.EMAIL_FROM_NAME) { - console.log('Email sender information not found in environment variables'); + console.log( + 'Email sender information not found in environment variables' + ); return; } - console.log('Sending email to', to); - const sends = await resend.emails.send({ - from: `${process.env.EMAIL_FROM_NAME} <${process.env.EMAIL_FROM_ADDRESS}>`, + const sends = await this.emailService.sendEmail( to, subject, html, - }); - + process.env.EMAIL_FROM_NAME, + process.env.EMAIL_FROM_ADDRESS + ); console.log(sends); } } diff --git a/package-lock.json b/package-lock.json index b410290d..d4a91654 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "@types/md5": "^2.3.5", "@types/mime-types": "^2.1.4", "@types/multer": "^1.4.11", + "@types/nodemailer": "^6.4.16", "@types/remove-markdown": "^0.3.4", "@types/sha256": "^0.2.2", "@types/stripe": "^8.0.417", @@ -90,6 +91,7 @@ "nestjs-command": "^3.1.4", "next": "14.2.3", "next-plausible": "^3.12.0", + "nodemailer": "^6.9.15", "nx": "19.7.2", "openai": "^4.47.1", "polotno": "^2.10.5", @@ -12973,6 +12975,14 @@ "@types/node": "*" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.16", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.16.tgz", + "integrity": "sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -29267,6 +29277,14 @@ "node": ">=6" } }, + "node_modules/nodemailer": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.15.tgz", + "integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", diff --git a/package.json b/package.json index 187bffbd..0019d3f5 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@types/md5": "^2.3.5", "@types/mime-types": "^2.1.4", "@types/multer": "^1.4.11", + "@types/nodemailer": "^6.4.16", "@types/remove-markdown": "^0.3.4", "@types/sha256": "^0.2.2", "@types/stripe": "^8.0.417", @@ -110,6 +111,7 @@ "nestjs-command": "^3.1.4", "next": "14.2.3", "next-plausible": "^3.12.0", + "nodemailer": "^6.9.15", "nx": "19.7.2", "openai": "^4.47.1", "polotno": "^2.10.5",