NestJS authentication for server side rendering with Handlebars

Here is how I managed NestJS authentication for server side rendering with Handlebars. These days I had to implement a simple authentication system for login for a project with NestJS and the templates rendered with Handlebars.

Below you can see the folder structure that I want to present in the following lines:

└── src
│ ├── main.ts
│ ├── app.module.ts
│ ├── auth
│ │ ├── auth.controller.ts
│ │ ├── auth.module.ts
│ │ ├── auth.middleware.ts
│ │ ├── auth.filters.ts
│ │ └── auth.service.ts
│ ├── users
│ │ ├── user.dto.ts
│ │ └── user.service.ts
│ └── views
│ │ └── login.hbs

auth.module.ts

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersService } from '../users/users.service';
import { DatabaseModule } from '../database/database.module';
import { AuthController } from './auth.controller';

@Module({
    imports: [
        DatabaseModule,
    ],
    providers: [
        UsersService,
        AuthService,
    ],
    controllers: [AuthController],
})
export class AuthModule { }

auth.controller.ts

import { Controller, Get, UseGuards, Render, Post, Body, Res, Req } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UserDto } from '../users/user.dto';
import { ValidateRequest } from '../validation/validation';
import { Response, Request } from 'express';

@Controller('auth')
export class AuthController {
    constructor(private readonly authService: AuthService) { }

    @Get('login')
    @Render('auth/login')
    async loginView() { }

    @Post('login')
    async login(@Body() userDto: UserDto, @Req() request, @Res() response: Response) {
        const payload = await ValidateRequest(UserDto, userDto)

        if (payload) {
            return response.render('auth/login', {
                ...payload,
            })
        }

        try {
            const user = await this.authService.login(userDto);

            // store the username in session
            request.session.username = user.username;

        } catch (err) {
            return response.render('auth/login', {
                data: userDto,
                globalError: err.message,
            })
        }

        return response.redirect(`/`)
    }

    @Get('logout')
    logout(@Req() request, @Res() response: Response) {
        request.session.destroy();
        return response.redirect('/auth/login')
    }

}

auth.middleware.ts

import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common';
import { Response } from 'express';

@Injectable()
export class AuthMiddleware implements NestMiddleware {

    use(req, res: Response, next: Function) {

        if (req.session && req.session.username)
            return next();

        throw new UnauthorizedException();
    }
}

auth.service.ts

import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { UserDto } from 'src/users/user.dto';
const bcrypt = require('bcrypt');

@Injectable()
export class AuthService {
    constructor(
        private readonly usersService: UsersService,
    ) { }

    async login(payload: UserDto): Promise<UserDto> {
        const user = await this.usersService.findByUsername(payload.username);

        if (!user)
            throw new Error('User is not registered')

        const match = await bcrypt.compare(payload.password, user.password);

        if (!match)
            throw new Error('Invalid username or password')

        return user
    }
}

app.module.ts

@Module({
  imports: [
    DatabaseModule,
    AuthModule,
    UsersModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule implements NestModule {

  // add authorization middleware for the following routes
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(AuthMiddleware)
      .forRoutes(

        'demo',
        'demo/create',
        'demo/edit/:id',
        'demo/delete/:id',

        // add here all the routes that you want to be protected
      );
  }
}

user.dto.ts

import { IsNotEmpty } from "class-validator";

export class UserDto {
    id?: number;

    @IsNotEmpty()
    username: string;

    @IsNotEmpty()
    password: String;
}

user.service.ts

import { Injectable, Inject } from '@nestjs/common';
import { Op } from 'sequelize';
import { newTransaction } from '../../../src/database';
import { UserDto } from './user.dto';

@Injectable()
export class UsersService {
    private model

    constructor(
        @Inject('SEQUELIZE') private readonly sequelize,
    ) {
        this.model = this.sequelize.models.users
    }

    async findByUsername(username: string): Promise<UserDto> {
        return await this.model.findOne({
            where: {
                username: {
                    [Op.eq]: username
                }
            }
        })
    }

}

auth.filters.ts

import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
    catch(exception: HttpException, host: ArgumentsHost) {

        if (exception.getStatus() === HttpStatus.UNAUTHORIZED) {
            const ctx = host.switchToHttp();
            const response = ctx.getResponse<Response>();

            return response.redirect('/auth/login')
        }
    }
}

main.ts

require('dotenv').config({ path: '../.env' }) // initialize dotenv

import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './auth/auth.filters';
import * as session from 'express-session'

const fs = require('fs');
const hbs = require('hbs');


async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);

  // session

  app.use(session({
    secret: process.env.SESSION_KEY,
    cookie: {
      maxAge: 86400000, // 24h
    }
  }))

  app.useGlobalFilters(new HttpExceptionFilter());

  // initialize handlebars js
  app.useStaticAssets(join(__dirname, '..', 'public'));
  app.setBaseViewsDir(join(__dirname, '..', 'views'));
  app.setViewEngine('hbs');

  // load partials
  loadComponents('templates');
  loadComponents('auth');

  require('handlebars-form-helpers').register(hbs.handlebars);

  await app.listen(3000);
}
bootstrap();

function loadComponents(pathName: string) {
  const partialsDir = __dirname + `/../views/${pathName}`;
  const filenames = fs.readdirSync(partialsDir);

  filenames.forEach(function (filename) {
    var matches = /^([^.]+).hbs$/.exec(filename);
    if (!matches) {
      return;
    }
    const name = `${pathName}_${matches[1]}`;
    const template = fs.readFileSync(partialsDir + '/' + filename, 'utf8');
    hbs.registerPartial(name, template);
  });

}

auth/login.hbs

{{#> templates_auth_layout titlePage="Login" }}

{{#form ""}}

<div class="form-group">
    {{label_validation 'username' 'Username *' errors class="control-label"}}
    {{input 'username' data.username class="form-control"}}

    <small class="form-text text-danger">{{#field_errors "username" errors}}{{this}}{{/field_errors}}</small>
</div>

<div class="form-group">
    {{label_validation 'password' 'Password *' errors class="control-label"}}
    {{input 'password' data.password class="form-control"}}

    <small class="form-text text-danger">{{#field_errors "password" errors}}{{this}}{{/field_errors}}</small>
</div>

{{submit 'submit' 'Login' class="btn btn-primary"}}
{{/form}}

{{/templates_auth_layout}}

I hope this was useful. If you have any question for how I implemented authentication please let me know right here in comments.

Leave a Reply

Your email address will not be published.