Auth Service with JWT token and mail module, Part 2

depak379mandal

depak379mandal

Posted on May 29, 2024

Auth Service with JWT token and mail module, Part 2

Love to work with you, You can hire me on Upwork.

We have looked into email and auth service now is the time to dive into mail and user module. Let me give you what is structure of mail module we have

src/modules/mail
├── mail.interface.ts
├── mailerConfig.service.ts
└── templates
    └── auth
        └── registration.hbs
Enter fullscreen mode Exit fullscreen mode

mail.interface.ts is a simple file that does include required interface for mail in system. Mail module does contain our email templates in templates folder, and we got mailerConfig.service.ts for mail module to be used as config factory. Let me just paste them one by one.

// src/modules/mail/mail.interface.ts

export interface MailData<T = never> {
  to: string;
  data: T;
}
Enter fullscreen mode Exit fullscreen mode
// src/modules/mail/templates/auth/registration.hbs

<html lang='en'>
  <head>
    <meta charset='UTF-8' />
    <meta name='viewport' content='width=device-width, initial-scale=' />
    <title>{{title}}</title>
  </head>

  <body style='margin:0;font-family:arial'>
    <table style='border:0;width:100%'>
      <tr style='background:#eeeeee'>
        <td
          style='padding:20px;color:#808080;text-align:center;font-size:40px;font-weight:600'
        >
          {{app_name}}
        </td>
      </tr>
      <tr>
        <td style='padding:20px;color:#808080;font-size:16px;font-weight:100'>
          Thank You for registration, Please verify to activate account.<br />
        </td>
      </tr>
      <tr>
        <td style='text-align:center'>
          <a
            href='{{url}}'
            style='display:inline-block;padding:20px;background:#00838f;text-decoration:none;color:#ffffff'
          >{{actionTitle}}</a>
        </td>
      </tr>
    </table>
  </body>

</html>
Enter fullscreen mode Exit fullscreen mode
// src/modules/mail/mailConfig.service.ts

import { MailerOptions, MailerOptionsFactory } from '@nestjs-modules/mailer';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
import * as path from 'path';

@Injectable()
export class MailerConfigClass implements MailerOptionsFactory {
  constructor(private configService: ConfigService) {}

  createMailerOptions(): MailerOptions {
    return {
      transport: {
        host: this.configService.get('mail.host'),
        port: this.configService.get('mail.port'),
        ignoreTLS: this.configService.get('mail.ignoreTLS'),
        secure: this.configService.get('mail.secure'),
        requireTLS: this.configService.get('mail.requireTLS'),
        auth: {
          user: this.configService.get('mail.user'),
          pass: this.configService.get('mail.password'),
        },
      },
      defaults: {
        from: `"${this.configService.get(
          'mail.defaultName',
        )}" <${this.configService.get('mail.defaultEmail')}>`,
      },
      template: {
        dir: path.join(
          this.configService.get('app.workingDirectory'),
          'src',
          'modules',
          'mail',
          'templates',
        ),
        adapter: new HandlebarsAdapter(),
        options: {
          strict: true,
        },
      },
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

In above, we are using handlebars as our templating engine that get included with our installed module for mailer. Above does our all setup so we can use mailer config and use the MailerService everywhere as our MailerModule is global module and exports MailerService, So it is available everywhere as ConfigService. Now we can move towards AppModule where we are importing AuthModule, MailerModule and UserModule. We are going to discuss more in ⁣UserModule.

// src/modules/app/app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { configLoads } from '../config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeORMConfigFactory } from '../database/typeorm.factory';
import { AuthModule } from '../auth/auth.module';
import { UserModule } from '../user/user.module';
import { MailerModule } from '@nestjs-modules/mailer';
import { MailerConfigClass } from '../mail/mailerConfig.service';

const modules = [AuthModule, UserModule];

export const global_modules = [
  ...
  MailerModule.forRootAsync({
    useClass: MailerConfigClass,
  }),
];

@Module({
  imports: [...global_modules, ...modules],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

In src/modules/config/app.config.ts the file, we have included new variable

frontendDomain: process.env.FRONTEND_DOMAIN,
Enter fullscreen mode Exit fullscreen mode

same goes for src/modules/config/mail.config.ts we have added new config variables

// src/modules/config/mail.config.ts

import { registerAs } from '@nestjs/config';

export default registerAs('mail', () => ({
  port: parseInt(process.env.MAIL_PORT, 10),
  host: process.env.MAIL_HOST,
  user: process.env.MAIL_USER,
  password: process.env.MAIL_PASSWORD,
  defaultEmail: process.env.MAIL_DEFAULT_EMAIL,
  defaultName: process.env.MAIL_DEFAULT_NAME,
  ignoreTLS: process.env.MAIL_IGNORE_TLS === 'true',
  secure: process.env.MAIL_SECURE === 'true',
  requireTLS: process.env.MAIL_REQUIRE_TLS === 'true',
}));
Enter fullscreen mode Exit fullscreen mode

We are now ready to move into UserModule, we have seen all the essential parts that needed. Just last part to complete all those APIs that will start working.

// src/modules/user/services/token.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/entities/user.entity';
import { Token, TokenType } from 'src/entities/user_token.entity';
import { Repository } from 'typeorm';

@Injectable()
export class TokenService {
  constructor(
    @InjectRepository(Token)
    private readonly tokenRepository: Repository<Token>,
  ) {}

  async create(
    user: User,
    type: keyof typeof TokenType = 'REGISTER_VERIFY',
    expires_at: Date = new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
  ) {
    const token = Token.create({
      user_id: user.id,
      type: TokenType[type],
      expires_at,
    });
    return this.tokenRepository.save(token);
  }

  async verify(token: string, type: keyof typeof TokenType) {
    const tokenEntity = await this.tokenRepository.findOne({
      relations: ['user'],
      loadEagerRelations: true,
      where: { token, type: TokenType[type], is_used: false },
    });
    if (!tokenEntity) {
      throw new Error('Token not found');
    }
    if (tokenEntity.expires_at < new Date()) {
      throw new Error('Token expired');
    }
    tokenEntity.is_used = true;
    await tokenEntity.save();
    return tokenEntity.user;
  }
}
Enter fullscreen mode Exit fullscreen mode

Very explanatory in terms of program, one function to create token and another to verify. The token expires in a week, But you can see that expires_at is very dynamic and can be changed after passed as third argument. Now, time for UserService that does nothing but creates a user.

// src/modules/user/services/user.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from 'src/entities/user.entity';
import { RegisterDto } from 'src/modules/auth/email.dto';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}

  async create(userCreateDto: RegisterDto) {
    const user = User.create({ ...userCreateDto });
    return this.userRepository.save(user);
  }
}
Enter fullscreen mode Exit fullscreen mode

At the end, we just import them as providers and exports them to be used in other modules.

// src/modules/user/user.module.ts

import { Module } from '@nestjs/common';
import { UserService } from './services/user.service';
import { TokenService } from './services/token.service';
import { Token } from 'src/entities/user_token.entity';
import { User } from 'src/entities/user.entity';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [TypeOrmModule.forFeature([User, Token])],
  providers: [UserService, TokenService],
  exports: [UserService, TokenService],
})
export class UserModule {}
Enter fullscreen mode Exit fullscreen mode

Now we can open our terminal and run the npm run start:dev command and see our output. But even before everything, we need a logger that logs our request and response. To do that, we have morgan we can install it using npm i morgan and types by npm i -D @types/morgan. That can be included in createApplication in bootstrap file.

// src/utils/bootstrap.ts

import * as morgan from 'morgan';

export const createApplication = (app: INestApplication) => {
  ...
  app.use(morgan('dev'));

  return app;
};
Enter fullscreen mode Exit fullscreen mode

Now it will give us more insight on certain API calls. So after running npm run start:dev. We get something like below that loads all the modules with mapped routes.

[3:14:58 AM] File change detected. Starting incremental compilation...

[3:14:58 AM] Found 0 errors. Watching for file changes.

Debugger attached.
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [NestFactory] Starting Nest application...
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [InstanceLoader] AppModule dependencies initialized +27ms
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [InstanceLoader] TypeOrmModule dependencies initialized +0ms
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [InstanceLoader] MailerModule dependencies initialized +1ms
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [InstanceLoader] ConfigHostModule dependencies initialized +0ms
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [InstanceLoader] ConfigModule dependencies initialized +11ms
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [InstanceLoader] JwtModule dependencies initialized +5ms
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [InstanceLoader] MailerCoreModule dependencies initialized +0ms
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [InstanceLoader] TypeOrmCoreModule dependencies initialized +71ms
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [InstanceLoader] TypeOrmModule dependencies initialized +0ms
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [InstanceLoader] TypeOrmModule dependencies initialized +0ms
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [InstanceLoader] UserModule dependencies initialized +0ms
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [InstanceLoader] AuthModule dependencies initialized +1ms
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [RoutesResolver] EmailController {/auth/email} (version: 1): +14ms
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [RouterExplorer] Mapped {/auth/email/register, POST} (version: 1) route +2ms
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [RouterExplorer] Mapped {/auth/email/verify, POST} (version: 1) route +0ms
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [RouterExplorer] Mapped {/auth/email/login, POST} (version: 1) route +0ms
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [RouterExplorer] Mapped {/auth/email/send-verify-email, POST} (version: 1) route +1ms
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [RouterExplorer] Mapped {/auth/email/reset-password-request, POST} (version: 1) route +0ms
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [RouterExplorer] Mapped {/auth/email/reset-password, POST} (version: 1) route +0ms
[Nest] 78996  - 04/09/2024, 3:15:01 AM     LOG [NestApplication] Nest application successfully started +3ms
Enter fullscreen mode Exit fullscreen mode

And above that we can test the routes, I am using mailtrap.io as mail testing platform. You can just go with anything you like. Update .env with below provided sample

NODE_ENV=development
APP_PORT=8000
APP_NAME="NestJS Series"
FRONTEND_DOMAIN=https://example.com

# Database Configuration
DATABASE_URL=postgresql://danimai:Danimai@localhost:5432/nest-series

# Mail confgiuration
MAIL_HOST=
MAIL_PORT=2525
MAIL_USER=
MAIL_PASSWORD=
MAIL_IGNORE_TLS=true
MAIL_SECURE=false
MAIL_REQUIRE_TLS=false
MAIL_DEFAULT_EMAIL=noreply@example.com
MAIL_DEFAULT_NAME=Danimai

# JWT
AUTH_JWT_SECRET=random-gibberish-token-for-jwt
AUTH_JWT_TOKEN_EXPIRES_IN=90d
Enter fullscreen mode Exit fullscreen mode

So I have completed my testing and works very fine for my side. If having any issues on your side let me know in further, We also have comment section to discuss and get into more depth. In the next tutorial, we will be learning how to update User profile and Authorisation guard.

Thank you very much for reading, See you in the next article.

💖 💪 🙅 🚩
depak379mandal
depak379mandal

Posted on May 29, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related