diff --git a/.env.example b/.env.example index 89afe2d8..b714672a 100644 --- a/.env.example +++ b/.env.example @@ -22,10 +22,12 @@ JWT_SECRET= JWT_EXPIRY_TIME=604800 GOOGLE_CLIENT_SECRET= GOOGLE_API_KEY= -SENDGRID_API_KEY= -SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID= -SENDGRID_FROM_NAME= -SENDGRID_FROM_EMAIL= +MAIL_FROM_NAME= +MAIL_FROM_EMAIL= +MAIL_HOST= +MAIL_PORT= +MAIL_USERNAME= +MAIL_PASSWORD= STORAGE_BUCKET= STORAGE_REGION= STORAGE_ENDPOINT= diff --git a/docker-compose.yml b/docker-compose.yml index 43b2eb42..3761a2b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,10 +6,7 @@ services: container_name: postgres ports: - 5432:5432 - environment: - - POSTGRES_DB=postgres - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=postgres + env_file: .env.docker volumes: - pgdata:/var/lib/postgresql/data healthcheck: @@ -38,7 +35,7 @@ services: context: . dockerfile: ./server/Dockerfile container_name: server - env_file: .env + env_file: .env.docker depends_on: - traefik - postgres @@ -57,7 +54,7 @@ services: context: . dockerfile: ./client/Dockerfile container_name: client - env_file: .env + env_file: .env.docker depends_on: - traefik - server diff --git a/docs/docs/source-code/env-vars.mdx b/docs/docs/source-code/env-vars.mdx index 86105f1b..c620bc96 100644 --- a/docs/docs/source-code/env-vars.mdx +++ b/docs/docs/source-code/env-vars.mdx @@ -136,30 +136,16 @@ You can get your own key here: https://developers.google.com/fonts/docs/develope If you do not have a Google API Key, it was make use of the cached response JSON that's stored within the project source. Please note that this cache is not updated and may not have all the latest fonts that Google Fonts has to offer. -## SendGrid +## Mail -The server makes use of SendGrid to send the password reset email to those who have forgotten their password. **This section is completely optional for those who do not require this functionality.** +The server makes use of SMTP to send the password reset email to those who have forgotten their password. **This section is completely optional for those who do not require this functionality.** -### `SENDGRID_API_KEY` - -**Required**: `no` -**Description:** SendGrid API Key - -Does not require any payment or credit card information to obtain an API key. - -You can get your own key here: https://docs.sendgrid.com/ui/account-and-settings/api-keys - -### `SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID` - -**Required**: `no` -**Description:** Dynamic Template ID for Forgot Password - -### `SENDGRID_FROM_NAME` +### `MAIL_FROM_NAME` **Required**: `no` **Description:** Sender's Name -### `SENDGRID_FROM_EMAIL` +### `MAIL_FROM_EMAIL` **Required**: `no` **Description:** Sender's Email Address diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d58a4db..d71891eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -221,13 +221,13 @@ importers: '@nestjs/terminus': ^9.1.1 '@nestjs/typeorm': ^9.0.1 '@reactive-resume/schema': workspace:* - '@sendgrid/mail': ^7.7.0 '@types/bcryptjs': ^2.4.2 '@types/cookie-parser': ^1.4.3 '@types/express': ^4.17.13 '@types/lodash': ^4.14.184 '@types/multer': ^1.4.7 '@types/node': ^18.7.9 + '@types/nodemailer': ^6.4.5 '@types/passport': ^1.0.10 '@types/passport-jwt': ^3.0.6 '@types/passport-local': ^1.0.34 @@ -244,6 +244,7 @@ importers: multer: ^1.4.4 nanoid: ^3.3.4 node-stream-zip: ^1.15.0 + nodemailer: ^6.7.8 passport: ^0.6.0 passport-jwt: ^4.0.0 passport-local: ^1.0.0 @@ -276,7 +277,6 @@ importers: '@nestjs/serve-static': 3.0.0_khr6mt6ojlxbw7bo55fknouh34 '@nestjs/terminus': 9.1.1_mr622mjrz7hsw5sxbt7k6brday '@nestjs/typeorm': 9.0.1_sp4gtzkbiyh24r2ydcb6yqstha - '@sendgrid/mail': 7.7.0 '@types/passport': 1.0.10 bcryptjs: 2.4.3 cache-manager: 4.1.0 @@ -291,6 +291,7 @@ importers: multer: 1.4.4 nanoid: 3.3.4 node-stream-zip: 1.15.0 + nodemailer: 6.7.8 passport: 0.6.0 passport-jwt: 4.0.0 passport-local: 1.0.0 @@ -312,6 +313,7 @@ importers: '@types/lodash': 4.14.184 '@types/multer': 1.4.7 '@types/node': 18.7.9 + '@types/nodemailer': 6.4.5 '@types/passport-jwt': 3.0.6 '@types/passport-local': 1.0.34 prettier: 2.7.1 @@ -4696,33 +4698,6 @@ packages: resolution: {integrity: sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==} dev: true - /@sendgrid/client/7.7.0: - resolution: {integrity: sha512-SxH+y8jeAQSnDavrTD0uGDXYIIkFylCo+eDofVmZLQ0f862nnqbC3Vd1ej6b7Le7lboyzQF6F7Fodv02rYspuA==} - engines: {node: 6.* || 8.* || >=10.*} - dependencies: - '@sendgrid/helpers': 7.7.0 - axios: 0.26.1 - transitivePeerDependencies: - - debug - dev: false - - /@sendgrid/helpers/7.7.0: - resolution: {integrity: sha512-3AsAxfN3GDBcXoZ/y1mzAAbKzTtUZ5+ZrHOmWQ279AuaFXUNCh9bPnRpN504bgveTqoW+11IzPg3I0WVgDINpw==} - engines: {node: '>= 6.0.0'} - dependencies: - deepmerge: 4.2.2 - dev: false - - /@sendgrid/mail/7.7.0: - resolution: {integrity: sha512-5+nApPE9wINBvHSUxwOxkkQqM/IAAaBYoP9hw7WwgDNQPxraruVqHizeTitVtKGiqWCKm2mnjh4XGN3fvFLqaw==} - engines: {node: 6.* || 8.* || >=10.*} - dependencies: - '@sendgrid/client': 7.7.0 - '@sendgrid/helpers': 7.7.0 - transitivePeerDependencies: - - debug - dev: false - /@sideway/address/4.1.4: resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} dependencies: @@ -5101,10 +5076,17 @@ packages: /@types/node/18.6.2: resolution: {integrity: sha512-KcfkBq9H4PI6Vpu5B/KoPeuVDAbmi+2mDBqGPGUgoL7yXQtcWGu2vJWmmRkneWK3Rh0nIAX192Aa87AqKHYChQ==} + dev: false /@types/node/18.7.9: resolution: {integrity: sha512-0N5Y1XAdcl865nDdjbO0m3T6FdmQ4ijE89/urOHLREyTXbpMWbSafx9y7XIsgWGtwUP2iYTinLyyW3FatAxBLQ==} + /@types/nodemailer/6.4.5: + resolution: {integrity: sha512-zuP3nBRQHI6M2PkXnGGy1Ww4VB+MyYHGgnfV2T+JR9KLkeWqPJuyVUgLpKXuFnA/b7pZaIDFh2sV4759B7jK1g==} + dependencies: + '@types/node': 18.7.9 + dev: true + /@types/normalize-package-data/2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} dev: true @@ -5961,14 +5943,6 @@ packages: - debug dev: false - /axios/0.26.1: - resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==} - dependencies: - follow-redirects: 1.14.9 - transitivePeerDependencies: - - debug - dev: false - /axios/0.27.2: resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} dependencies: @@ -9804,7 +9778,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 18.6.2 + '@types/node': 18.7.9 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -11069,6 +11043,11 @@ packages: engines: {node: '>=0.12.0'} dev: false + /nodemailer/6.7.8: + resolution: {integrity: sha512-2zaTFGqZixVmTxpJRCFC+Vk5eGRd/fYtvIR+dl5u9QXLTQWGIf48x/JXvo58g9sa0bU6To04XUv554Paykum3g==} + engines: {node: '>=6.0.0'} + dev: false + /normalize-package-data/2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: diff --git a/server/package.json b/server/package.json index 4eaecf63..fdec97b4 100644 --- a/server/package.json +++ b/server/package.json @@ -20,7 +20,6 @@ "@nestjs/serve-static": "^3.0.0", "@nestjs/terminus": "^9.1.1", "@nestjs/typeorm": "^9.0.1", - "@sendgrid/mail": "^7.7.0", "@types/passport": "^1.0.10", "bcryptjs": "^2.4.3", "cache-manager": "^4.1.0", @@ -35,6 +34,7 @@ "multer": "^1.4.4", "nanoid": "^3.3.4", "node-stream-zip": "^1.15.0", + "nodemailer": "^6.7.8", "passport": "^0.6.0", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", @@ -57,6 +57,7 @@ "@types/lodash": "^4.14.184", "@types/multer": "^1.4.7", "@types/node": "^18.7.9", + "@types/nodemailer": "^6.4.5", "@types/passport-jwt": "^3.0.6", "@types/passport-local": "^1.0.34", "prettier": "^2.7.1", diff --git a/server/src/config/config.module.ts b/server/src/config/config.module.ts index 0eabd1a7..b6e3bfd7 100644 --- a/server/src/config/config.module.ts +++ b/server/src/config/config.module.ts @@ -6,7 +6,7 @@ import appConfig from './app.config'; import authConfig from './auth.config'; import databaseConfig from './database.config'; import googleConfig from './google.config'; -import sendgridConfig from './sendgrid.config'; +import mailConfig from './mail.config'; import storageConfig from './storage.config'; const validationSchema = Joi.object({ @@ -36,11 +36,13 @@ const validationSchema = Joi.object({ GOOGLE_CLIENT_SECRET: Joi.string().allow(''), PUBLIC_GOOGLE_CLIENT_ID: Joi.string().allow(''), - // SendGrid - SENDGRID_API_KEY: Joi.string().allow(''), - SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID: Joi.string().allow(''), - SENDGRID_FROM_NAME: Joi.string().allow(''), - SENDGRID_FROM_EMAIL: Joi.string().allow(''), + // Mail + MAIL_FROM_NAME: Joi.string().allow(''), + MAIL_FROM_EMAIL: Joi.string().allow(''), + MAIL_HOST: Joi.string().allow(''), + MAIL_PORT: Joi.string().allow(''), + MAIL_USERNAME: Joi.string().allow(''), + MAIL_PASSWORD: Joi.string().allow(''), // Storage STORAGE_BUCKET: Joi.string().allow(''), @@ -54,7 +56,7 @@ const validationSchema = Joi.object({ @Module({ imports: [ NestConfigModule.forRoot({ - load: [appConfig, authConfig, databaseConfig, googleConfig, sendgridConfig, storageConfig], + load: [appConfig, authConfig, databaseConfig, googleConfig, mailConfig, storageConfig], validationSchema: validationSchema, }), ], diff --git a/server/src/config/mail.config.ts b/server/src/config/mail.config.ts new file mode 100644 index 00000000..4bcdfc2e --- /dev/null +++ b/server/src/config/mail.config.ts @@ -0,0 +1,12 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs('mail', () => ({ + from: { + name: process.env.MAIL_FROM_NAME, + email: process.env.MAIL_FROM_EMAIL, + }, + host: process.env.MAIL_HOST, + port: process.env.MAIL_PORT, + username: process.env.MAIL_USERNAME, + password: process.env.MAIL_PASSWORD, +})); diff --git a/server/src/config/sendgrid.config.ts b/server/src/config/sendgrid.config.ts deleted file mode 100644 index 7ca2f4d7..00000000 --- a/server/src/config/sendgrid.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { registerAs } from '@nestjs/config'; - -export default registerAs('sendgrid', () => ({ - apiKey: process.env.SENDGRID_API_KEY, - forgotPasswordTemplateId: process.env.SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID, - fromName: process.env.SENDGRID_FROM_NAME, - fromEmail: process.env.SENDGRID_FROM_EMAIL, -})); diff --git a/server/src/mail/dto/send-mail.dto.ts b/server/src/mail/dto/send-mail.dto.ts new file mode 100644 index 00000000..e8ebbd06 --- /dev/null +++ b/server/src/mail/dto/send-mail.dto.ts @@ -0,0 +1,30 @@ +import { Type } from 'class-transformer'; +import { IsDefined, IsNotEmpty, IsString } from 'class-validator'; + +export class MailRecipient { + @IsNotEmpty() + @IsString() + name: string; + + @IsNotEmpty() + @IsString() + email: string; +} + +export class SendMailDto { + @IsDefined() + @Type(() => MailRecipient) + from: MailRecipient; + + @IsDefined() + @Type(() => MailRecipient) + to: MailRecipient; + + @IsString() + @IsNotEmpty() + subject: string; + + @IsString() + @IsNotEmpty() + message: string; +} diff --git a/server/src/mail/mail.service.ts b/server/src/mail/mail.service.ts index c09e0b96..db02b31b 100644 --- a/server/src/mail/mail.service.ts +++ b/server/src/mail/mail.service.ts @@ -1,40 +1,56 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import SendGrid from '@sendgrid/mail'; +import { createTransport, Transporter } from 'nodemailer'; import { User } from '@/users/entities/user.entity'; +import { SendMailDto } from './dto/send-mail.dto'; + @Injectable() export class MailService { - constructor(private configService: ConfigService) { - const sendGridApiKey = this.configService.get('sendgrid.apiKey'); + transporter: Transporter; - if (sendGridApiKey) { - SendGrid.setApiKey(this.configService.get('sendgrid.apiKey')); - } + constructor(private configService: ConfigService) { + this.transporter = createTransport({ + host: this.configService.get('mail.host'), + port: this.configService.get('mail.port'), + pool: true, + secure: false, + tls: { ciphers: 'SSLv3' }, + auth: { + user: this.configService.get('mail.username'), + pass: this.configService.get('mail.password'), + }, + }); } - async sendEmail(mail: SendGrid.MailDataRequired) { - return SendGrid.send(mail); + async sendEmail(sendMailDto: SendMailDto) { + this.transporter.sendMail({ + from: `${sendMailDto.from.name} <${sendMailDto.from.email}>`, + to: `${sendMailDto.to.name} <${sendMailDto.to.email}>`, + subject: sendMailDto.subject, + text: sendMailDto.message, + html: sendMailDto.message, + }); } async sendForgotPasswordEmail(user: User, resetToken: string): Promise { const appUrl = this.configService.get('app.url'); const url = `${appUrl}?modal=auth.reset&resetToken=${resetToken}`; - const mailData: SendGrid.MailDataRequired = { + const sendMailDto: SendMailDto = { from: { - name: this.configService.get('sendgrid.fromName'), - email: this.configService.get('sendgrid.fromEmail'), + name: this.configService.get('mail.from.name'), + email: this.configService.get('mail.from.email'), }, - to: user.email, - hideWarnings: true, - dynamicTemplateData: { url }, - templateId: this.configService.get('sendgrid.forgotPasswordTemplateId'), + to: { + name: user.name, + email: user.email, + }, + subject: 'Reset your Reactive Resume password', + message: `

Hey ${user.name}!

You can reset your password by visiting this link: ${url}.

But hurry, because it will expire in 30 minutes.

`, }; - await SendGrid.send(mailData); - - return; + await this.sendEmail(sendMailDto); } } diff --git a/server/src/users/users.service.ts b/server/src/users/users.service.ts index 140dc53d..017154a0 100644 --- a/server/src/users/users.service.ts +++ b/server/src/users/users.service.ts @@ -2,7 +2,7 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { SchedulerRegistry } from '@nestjs/schedule'; import { InjectRepository } from '@nestjs/typeorm'; import { randomBytes } from 'crypto'; -import { Connection, Repository } from 'typeorm'; +import { DataSource, Repository } from 'typeorm'; import { MailService } from '@/mail/mail.service'; @@ -19,7 +19,7 @@ export class UsersService { @InjectRepository(User) private userRepository: Repository, private schedulerRegistry: SchedulerRegistry, private mailService: MailService, - private connection: Connection + private dataSource: DataSource ) {} async findById(id: number): Promise { @@ -93,7 +93,7 @@ export class UsersService { const user = await this.findByEmail(email); const resetToken = randomBytes(32).toString('hex'); - const queryRunner = this.connection.createQueryRunner(); + const queryRunner = this.dataSource.createQueryRunner(); const timeout = setTimeout(async () => { await this.userRepository.update(user.id, { resetToken: null });