DEV Community

loading...

NestJS authentication for server side rendering with Handlebars

boobo94 profile image Bogdan Alexandru Militaru Originally published at boobo94.xyz on ・4 min read

The post NestJS authentication for server-side rendering with Handlebars appeared first on boobo94.xyz. So if you want to read more articles like this please subscribe to my newsletter or follow me here

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 { }
Enter fullscreen mode Exit fullscreen mode

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') } }
Enter fullscreen mode Exit fullscreen mode

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(); } }
Enter fullscreen mode Exit fullscreen mode

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 } }
Enter fullscreen mode Exit fullscreen mode

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 ); } }
Enter fullscreen mode Exit fullscreen mode

user.dto.ts

import { IsNotEmpty } from "class-validator"; export class UserDto { id?: number; @IsNotEmpty() username: string; @IsNotEmpty() password: String; }
Enter fullscreen mode Exit fullscreen mode

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 } } }) } }
Enter fullscreen mode Exit fullscreen mode

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') } } }
Enter fullscreen mode Exit fullscreen mode

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); }); }
Enter fullscreen mode Exit fullscreen mode

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}}
Enter fullscreen mode Exit fullscreen mode

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

The post NestJS authentication for server side rendering with Handlebars appeared first on boobo94.xyz. So if you want to read more articles like this please subscribe to my newsletter or follow me here

Discussion (0)

pic
Editor guide