- 1. API with NestJS #1. Controllers, routing and the module structure
- 2. API with NestJS #2. Setting up a PostgreSQL database with TypeORM
- 3. API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies
- 4. API with NestJS #4. Error handling and data validation
- 5. API with NestJS #5. Serializing the response with interceptors
- 6. API with NestJS #6. Looking into dependency injection and modules
- 7. API with NestJS #7. Creating relationships with Postgres and TypeORM
- 8. API with NestJS #8. Writing unit tests
- 9. API with NestJS #9. Testing services and controllers with integration tests
- 10. API with NestJS #10. Uploading public files to Amazon S3
- 11. API with NestJS #11. Managing private files with Amazon S3
- 12. API with NestJS #12. Introduction to Elasticsearch
- 13. API with NestJS #13. Implementing refresh tokens using JWT
- 14. API with NestJS #14. Improving performance of our Postgres database with indexes
- 15. API with NestJS #15. Defining transactions with PostgreSQL and TypeORM
- 16. API with NestJS #16. Using the array data type with PostgreSQL and TypeORM
- 17. API with NestJS #17. Offset and keyset pagination with PostgreSQL and TypeORM
- 18. API with NestJS #18. Exploring the idea of microservices
- 19. API with NestJS #19. Using RabbitMQ to communicate with microservices
- 20. API with NestJS #20. Communicating with microservices using the gRPC framework
- 21. API with NestJS #21. An introduction to CQRS
- 22. API with NestJS #22. Storing JSON with PostgreSQL and TypeORM
- 23. API with NestJS #23. Implementing in-memory cache to increase the performance
- 24. API with NestJS #24. Cache with Redis. Running the app in a Node.js cluster
- 25. API with NestJS #25. Sending scheduled emails with cron and Nodemailer
- 26. API with NestJS #26. Real-time chat with WebSockets
- 27. API with NestJS #27. Introduction to GraphQL. Queries, mutations, and authentication
- 28. API with NestJS #28. Dealing in the N + 1 problem in GraphQL
- 29. API with NestJS #29. Real-time updates with GraphQL subscriptions
- 30. API with NestJS #30. Scalar types in GraphQL
- 31. API with NestJS #31. Two-factor authentication
- 32. API with NestJS #32. Introduction to Prisma with PostgreSQL
- 33. API with NestJS #33. Managing PostgreSQL relationships with Prisma
- 34. API with NestJS #34. Handling CPU-intensive tasks with queues
- 35. API with NestJS #35. Using server-side sessions instead of JSON Web Tokens
- 36. API with NestJS #36. Introduction to Stripe with React
- 37. API with NestJS #37. Using Stripe to save credit cards for future use
- 38. API with NestJS #38. Setting up recurring payments via subscriptions with Stripe
- 39. API with NestJS #39. Reacting to Stripe events with webhooks
- 40. API with NestJS #40. Confirming the email address
- 41. API with NestJS #41. Verifying phone numbers and sending SMS messages with Twilio
- 42. API with NestJS #42. Authenticating users with Google
- 43. API with NestJS #43. Introduction to MongoDB
- 44. API with NestJS #44. Implementing relationships with MongoDB
- 45. API with NestJS #45. Virtual properties with MongoDB and Mongoose
- 46. API with NestJS #46. Managing transactions with MongoDB and Mongoose
- 47. API with NestJS #47. Implementing pagination with MongoDB and Mongoose
- 48. API with NestJS #48. Definining indexes with MongoDB and Mongoose
- 49. API with NestJS #49. Updating with PUT and PATCH with MongoDB and Mongoose
- 50. API with NestJS #50. Introduction to logging with the built-in logger and TypeORM
- 51. API with NestJS #51. Health checks with Terminus and Datadog
- 52. API with NestJS #52. Generating documentation with Compodoc and JSDoc
- 53. API with NestJS #53. Implementing soft deletes with PostgreSQL and TypeORM
- 54. API with NestJS #54. Storing files inside a PostgreSQL database
- 55. API with NestJS #55. Uploading files to the server
- 56. API with NestJS #56. Authorization with roles and claims
- 57. API with NestJS #57. Composing classes with the mixin pattern
- 58. API with NestJS #58. Using ETag to implement cache and save bandwidth
- 59. API with NestJS #59. Introduction to a monorepo with Lerna and Yarn workspaces
- 60. API with NestJS #60. The OpenAPI specification and Swagger
- 61. API with NestJS #61. Dealing with circular dependencies
- 62. API with NestJS #62. Introduction to MikroORM with PostgreSQL
- 63. API with NestJS #63. Relationships with PostgreSQL and MikroORM
- 64. API with NestJS #64. Transactions with PostgreSQL and MikroORM
- 65. API with NestJS #65. Implementing soft deletes using MikroORM and filters
- 66. API with NestJS #66. Improving PostgreSQL performance with indexes using MikroORM
- 67. API with NestJS #67. Migrating to TypeORM 0.3
- 68. API with NestJS #68. Interacting with the application through REPL
- 69. API with NestJS #69. Database migrations with TypeORM
- 70. API with NestJS #70. Defining dynamic modules
- 71. API with NestJS #71. Introduction to feature flags
- 72. API with NestJS #72. Working with PostgreSQL using raw SQL queries
- 73. API with NestJS #73. One-to-one relationships with raw SQL queries
- 74. API with NestJS #74. Designing many-to-one relationships using raw SQL queries
- 75. API with NestJS #75. Many-to-many relationships using raw SQL queries
- 76. API with NestJS #76. Working with transactions using raw SQL queries
- 77. API with NestJS #77. Offset and keyset pagination with raw SQL queries
- 78. API with NestJS #78. Generating statistics using aggregate functions in raw SQL
- 79. API with NestJS #79. Implementing searching with pattern matching and raw SQL
- 80. API with NestJS #80. Updating entities with PUT and PATCH using raw SQL queries
- 81. API with NestJS #81. Soft deletes with raw SQL queries
- 82. API with NestJS #82. Introduction to indexes with raw SQL queries
- 83. API with NestJS #83. Text search with tsvector and raw SQL
- 84. API with NestJS #84. Implementing filtering using subqueries with raw SQL
- 85. API with NestJS #85. Defining constraints with raw SQL
- 86. API with NestJS #86. Logging with the built-in logger when using raw SQL
- 87. API with NestJS #87. Writing unit tests in a project with raw SQL
- 88. API with NestJS #88. Testing a project with raw SQL using integration tests
- 89. API with NestJS #89. Replacing Express with Fastify
- 90. API with NestJS #90. Using various types of SQL joins
- 91. API with NestJS #91. Dockerizing a NestJS API with Docker Compose
- 92. API with NestJS #92. Increasing the developer experience with Docker Compose
- 93. API with NestJS #93. Deploying a NestJS app with Amazon ECS and RDS
- 94. API with NestJS #94. Deploying multiple instances on AWS with a load balancer
- 95. API with NestJS #95. CI/CD with Amazon ECS and GitHub Actions
- 96. API with NestJS #96. Running unit tests with CI/CD and GitHub Actions
- 97. API with NestJS #97. Introduction to managing logs with Amazon CloudWatch
- 98. API with NestJS #98. Health checks with Terminus and Amazon ECS
- 99. API with NestJS #99. Scaling the number of application instances with Amazon ECS
- 100. API with NestJS #100. The HTTPS protocol with Route 53 and AWS Certificate Manager
- 101. API with NestJS #101. Managing sensitive data using the AWS Secrets Manager
- 102. API with NestJS #102. Writing unit tests with Prisma
- 103. API with NestJS #103. Integration tests with Prisma
- 104. API with NestJS #104. Writing transactions with Prisma
- 105. API with NestJS #105. Implementing soft deletes with Prisma and middleware
- 106. API with NestJS #106. Improving performance through indexes with Prisma
- 107. API with NestJS #107. Offset and keyset pagination with Prisma
- 108. API with NestJS #108. Date and time with Prisma and PostgreSQL
- 109. API with NestJS #109. Arrays with PostgreSQL and Prisma
- 110. API with NestJS #110. Managing JSON data with PostgreSQL and Prisma
- 111. API with NestJS #111. Constraints with PostgreSQL and Prisma
- 112. API with NestJS #112. Serializing the response with Prisma
- 113. API with NestJS #113. Logging with Prisma
- 114. API with NestJS #114. Modifying data using PUT and PATCH methods with Prisma
- 115. API with NestJS #115. Database migrations with Prisma
- 116. API with NestJS #116. REST API versioning
- 117. API with NestJS #117. CORS – Cross-Origin Resource Sharing
- 118. API with NestJS #118. Uploading and streaming videos
- 119. API with NestJS #119. Type-safe SQL queries with Kysely and PostgreSQL
- 120. API with NestJS #120. One-to-one relationships with the Kysely query builder
- 121. API with NestJS #121. Many-to-one relationships with PostgreSQL and Kysely
In a lot of web applications, emails play a significant role. If we create an online ordering system, we need to be confident that our users get a confirmation email. When our services include a mailing list, we want to make sure that the provided email is valid. We also might want to implement the password resetting feature, for which the email address is essential. Requiring our users to confirm the email address might also serve as an additional layer of security against bots. Therefore, in this article, we look into confirming the email addresses.
Confirming the email address
First, we need a way to store the information about whether the email is confirmed. To do that, let’s expand on the entity of the user.
user.entity.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity() class User { @PrimaryGeneratedColumn() public id: number; @Column({ unique: true }) public email: string; @Column({ default: false }) public isEmailConfirmed: boolean; // ... } export default User; |
To confirm the email address, we aim to send an email message with an URL containing the JWT. To do that, we need additional environment variables.
If you want to know more about JWT, check out API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies
app.module.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from '@hapi/joi'; @Module({ imports: [ ConfigModule.forRoot({ validationSchema: Joi.object({ JWT_VERIFICATION_TOKEN_SECRET: Joi.string().required(), JWT_VERIFICATION_TOKEN_EXPIRATION_TIME: Joi.string().required(), EMAIL_CONFIRMATION_URL: Joi.string().required(), // ... }) }), // ... ], controllers: [], }) export class AppModule {} |
We’ve already used JWT in other parts of this series. To increase security, we want to use a different secret token to encode and decode JWT for email verification. We also want the token to expire after a few hours in case the email account of our user gets hijacked.
.env
1 2 3 |
JWT_VERIFICATION_TOKEN_SECRET=7AnEd5epXmdaJfUrokkQ JWT_VERIFICATION_TOKEN_EXPIRATION_TIME=21600 EMAIL_CONFIRMATION_URL=https://my-app.com/confirm-email |
Above, we define the expiration time in seconds.
Sending the verification link
To be able to send the verification link, we need to set up Nodemailer. We’ve already created the EmailService that does that in the 25th part of this series. Let’s reuse it in a new service that manages email confirmation.
emailConfirmation.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import VerificationTokenPayload from './verificationTokenPayload.interface'; import EmailService from '../email/email.service'; import { UsersService } from '../users/users.service'; @Injectable() export class EmailConfirmationService { constructor( private readonly jwtService: JwtService, private readonly configService: ConfigService, private readonly emailService: EmailService, ) {} public sendVerificationLink(email: string) { const payload: VerificationTokenPayload = { email }; const token = this.jwtService.sign(payload, { secret: this.configService.get('JWT_VERIFICATION_TOKEN_SECRET'), expiresIn: `${this.configService.get('JWT_VERIFICATION_TOKEN_EXPIRATION_TIME')}s` }); const url = `${this.configService.get('EMAIL_CONFIRMATION_URL')}?token=${token}`; const text = `Welcome to the application. To confirm the email address, click here: ${url}`; return this.emailService.sendMail({ to: email, subject: 'Email confirmation', text, }) } } |
verificationTokenPayload.interface.ts
1 2 3 4 5 |
interface VerificationTokenPayload { email: string; } export default VerificationTokenPayload; |
Let’s modify our AuthenticationController and use the above service.
authenticationController.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import { Body, Controller, Post, ClassSerializerInterceptor, UseInterceptors, } from '@nestjs/common'; import { AuthenticationService } from './authentication.service'; import RegisterDto from './dto/register.dto'; import { UsersService } from '../users/users.service'; import { EmailConfirmationService } from '../emailConfirmation/emailConfirmation.service'; @Controller('authentication') @UseInterceptors(ClassSerializerInterceptor) export class AuthenticationController { constructor( private readonly authenticationService: AuthenticationService, private readonly usersService: UsersService, private readonly emailConfirmationService: EmailConfirmationService ) {} @Post('register') async register(@Body() registrationData: RegisterDto) { const user = await this.authenticationService.register(registrationData); await this.emailConfirmationService.sendVerificationLink(registrationData.email); return user; } // ... } |
Now, as soon as users sign in, they receive a link through email.
Feel free to make the contents of the email more refined.
Confirming the email address
Once the user goes to the link above, our frontend application needs to get the token from the URL and send it to our API. To do support that, we need to create an endpoint for it.
emailConfirmation.controller.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import { Controller, ClassSerializerInterceptor, UseInterceptors, Post, Body, UseGuards, Req, } from '@nestjs/common'; import ConfirmEmailDto from './confirmEmail.dto'; import { EmailConfirmationService } from './emailConfirmation.service'; import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard'; import RequestWithUser from '../authentication/requestWithUser.interface'; @Controller('email-confirmation') @UseInterceptors(ClassSerializerInterceptor) export class EmailConfirmationController { constructor( private readonly emailConfirmationService: EmailConfirmationService ) {} @Post('confirm') async confirm(@Body() confirmationData: ConfirmEmailDto) { const email = await this.emailConfirmationService.decodeConfirmationToken(confirmationData.token); await this.emailConfirmationService.confirmEmail(email); } } |
confirmEmail.dto.ts
1 2 3 4 5 6 7 8 9 |
import { IsString, IsNotEmpty } from 'class-validator'; export class ConfirmEmailDto { @IsString() @IsNotEmpty() token: string; } export default ConfirmEmailDto; |
Above, a few notable things are happening. We expect the frontend application to send the token from the URL in the request body back to the API. We then decode it and confirm the email.
emailConfirmation.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
import { BadRequestException, Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import EmailService from '../email/email.service'; import { UsersService } from '../users/users.service'; @Injectable() export class EmailConfirmationService { constructor( private readonly jwtService: JwtService, private readonly configService: ConfigService, private readonly emailService: EmailService, private readonly usersService: UsersService, ) {} public async confirmEmail(email: string) { const user = await this.usersService.getByEmail(email); if (user.isEmailConfirmed) { throw new BadRequestException('Email already confirmed'); } await this.usersService.markEmailAsConfirmed(email); } public async decodeConfirmationToken(token: string) { try { const payload = await this.jwtService.verify(token, { secret: this.configService.get('JWT_VERIFICATION_TOKEN_SECRET'), }); if (typeof payload === 'object' && 'email' in payload) { return payload.email; } throw new BadRequestException(); } catch (error) { if (error?.name === 'TokenExpiredError') { throw new BadRequestException('Email confirmation token expired'); } throw new BadRequestException('Bad confirmation token'); } } // ... } |
Please notice, that we throw an error if the email is already confirmed. Therefore, our JWT can’t be used more than once.
If the confirmEmail method, we use the UsersService to mark the email as confirmed. We need to implement this functionality.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import User from './user.entity'; @Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository<User>, ) {} async markEmailAsConfirmed(email: string) { return this.usersRepository.update({ email }, { isEmailConfirmed: true }); } // ... } |
Resending the confirmation link
Since we set an expiration time for our tokens, the user might not use the token on time. Therefore, we should implement a feature of resending the link.
emailConfirmation.controller.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import { Controller, ClassSerializerInterceptor, UseInterceptors, Post, UseGuards, Req, } from '@nestjs/common'; import { EmailConfirmationService } from './emailConfirmation.service'; import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard'; import RequestWithUser from '../authentication/requestWithUser.interface'; @Controller('email-confirmation') @UseInterceptors(ClassSerializerInterceptor) export class EmailConfirmationController { constructor( private readonly emailConfirmationService: EmailConfirmationService ) {} @Post('resend-confirmation-link') @UseGuards(JwtAuthenticationGuard) async resendConfirmationLink(@Req() request: RequestWithUser) { await this.emailConfirmationService.resendConfirmationLink(request.user.id); } // ... } |
A thing worth noting is that we require the user to authenticate before resending the confirmation link. Thanks to that, users can’t require email confirmation for other people.
emailConfirmation.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { BadRequestException, Injectable } from '@nestjs/common'; import { UsersService } from '../users/users.service'; @Injectable() export class EmailConfirmationService { constructor( private readonly usersService: UsersService, ) {} public async resendConfirmationLink(userId: number) { const user = await this.usersService.getById(userId); if (user.isEmailConfirmed) { throw new BadRequestException('Email already confirmed'); } await this.sendVerificationLink(user.email); } // ... } |
A crucial thing to notice security-wise is that sending a new confirmation link doesn’t invalidate the previous links. If we would like to achieve that, we could, for example, store the most recent confirmation token in the database and check it before confirming.
Requiring the email address to be confirmed
Depending on the use case, we might want to prevent the user from accessing certain endpoints if the user didn’t confirm the email. To do that, we can create an additional guard.
emailConfirmation.guard.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; import RequestWithUser from '../authentication/requestWithUser.interface'; @Injectable() export class EmailConfirmationGuard implements CanActivate { canActivate( context: ExecutionContext, ) { const request: RequestWithUser = context.switchToHttp().getRequest(); if (!request.user?.isEmailConfirmed) { throw new UnauthorizedException('Confirm your email first'); } return true; } } |
For our guard to work, we need to attach it to an endpoint.
creditCards.controller.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import { Controller, Req, UseGuards, Get } from '@nestjs/common'; import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard'; import RequestWithUser from '../authentication/requestWithUser.interface'; import StripeService from '../stripe/stripe.service'; import { EmailConfirmationGuard } from '../emailConfirmation/emailConfirmation.guard'; @Controller('credit-cards') export default class CreditCardsController { constructor( private readonly stripeService: StripeService ) {} @Get() @UseGuards(EmailConfirmationGuard) @UseGuards(JwtAuthenticationGuard) async getCreditCards(@Req() request: RequestWithUser) { return this.stripeService.listCreditCards(request.user.stripeCustomerId); } // ... } |
In Typescript, decorators resolve from bottom to top. In our implementation, the EmailConfirmationGuard requires the request.user object to work properly. Because of that, the crucial thing is to first use the EmailConfirmationGuard, then apply the JwtAuthenticationGuard.
Summary
In this article, we’ve implemented the feature of confirming email addresses. To do that, we had to send emails containing JWT. We’ve also created a NestJS guard that we can use to block certain endpoints if the user didn’t confirm the email. With it, we’ve added a feature that might prove to be useful if we depend on email messaging a lot in our application.
There is a library for that !
https://github.com/vh13294/nestjs-url-generator/