DEV Community

Cover image for Nest.js Blog Step by Step: Add User System
Leapcell
Leapcell

Posted on

Nest.js Blog Step by Step: Add User System

Cover

In the previous tutorial, we built a basic blog that supports creating articles.

The danger, however, is that this blog has no authentication, meaning anyone can create or delete articles.

In the upcoming tutorials, we will make this blog more secure by gradually adding a user and authentication system.

Without further ado, let's get started.

Introduction to Authentication Methods

In web development, the two most common authentication methods are Token-based (e.g., JWT) and Session-based (Cookie).

  • JWT (JSON Web Tokens): This is the most popular authentication method today. After a user logs in, the server generates a token and returns it to the client. The client includes this token in subsequent requests, and the server simply verifies that the token has not been tampered with. Because the server does not store the user's login state, this method is very suitable for modern large-scale websites with countless servers, as it avoids the need for frequent synchronization of user status across multiple servers.
  • Session-Cookie: After a user logs in, the server creates a session and returns the Session ID to the browser via a cookie. The browser automatically includes this cookie in subsequent requests. The server then uses the Session ID from the cookie to find the corresponding session information it has stored, thereby identifying the user.

In this tutorial, we will choose the Session-Cookie method. Our blog is a simple application with a traditional architecture. Using Session-Cookies for authentication is the most direct, classic, and secure approach. Nest.js also provides excellent built-in support for it.

Create the User Module

Before we tackle authentication, let's first add a user system.

Similar to creating the posts module in the previous tutorial, use the Nest CLI to quickly generate the necessary files:

nest generate module users
nest generate controller users
nest generate service users
Enter fullscreen mode Exit fullscreen mode

Next, create a user entity file user.entity.ts in the src/users directory to map to the users table in the database.

// src/users/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ unique: true })
  username: string;

  @Column()
  password: string; // The stored password will be encrypted
}
Enter fullscreen mode Exit fullscreen mode

In UsersModule (src/users/users.module.ts), register TypeOrmModule so it can operate on the User entity. Note that you need to export UsersService so it can be used in other modules later (like the Auth module).

// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService], // Export UsersService for use in other modules
})
export class UsersModule {}
Enter fullscreen mode Exit fullscreen mode

Finally, execute the following SQL statement in the PostgreSQL database we created in the last tutorial to create the user table:

CREATE TABLE "user" (
    "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    "username" VARCHAR UNIQUE NOT NULL,
    "password" VARCHAR NOT NULL
);
Enter fullscreen mode Exit fullscreen mode

If your database was created on Leapcell,

Leapcell

you can easily execute SQL statements using the graphical interface. Just go to the Database management page on the website, paste the above statement into the SQL interface, and run it.

ImageP0

Implement User Registration

Modify src/users/users.service.ts to add the logic for creating and finding users.

Note: For security, user passwords need to be encrypted before being stored in the database. We will use the bcrypt library for password encryption.

Install the required dependencies:

npm install bcrypt
npm install -D @types/bcrypt
Enter fullscreen mode Exit fullscreen mode
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import * as bcrypt from 'bcrypt';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>
  ) {}

  async create(user: Partial<User>): Promise<User> {
    const saltRounds = 10;
    const hashedPassword = await bcrypt.hash(user.password || '', saltRounds);

    const newUser = this.usersRepository.create({
      username: user.username,
      password: hashedPassword,
    });

    return this.usersRepository.save(newUser);
  }

  async findOne(username: string): Promise<User | null> {
    return this.usersRepository.findOneBy({ username });
  }
}
Enter fullscreen mode Exit fullscreen mode

Create Auth Module and Login Logic Validation

We will place all authentication-related logic in a separate auth module. First, we will only implement the core "validate user" logic.

Use the CLI to create the auth module and service:

nest generate module auth
nest generate service auth
Enter fullscreen mode Exit fullscreen mode

Modify src/auth/auth.service.ts to add a validateUser method for verifying if the username and password are correct.

// src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';

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

  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && (await bcrypt.compare(pass, user.password))) {
      const { password, ...result } = user;
      return result; // Validation successful, return user info without password
    }
    return null; // Validation failed
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, import UsersModule into AuthModule so that AuthService can use UsersService.

// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [UsersModule],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}
Enter fullscreen mode Exit fullscreen mode

Create Login and Registration Pages

We need to provide an interface for user interaction. In the views folder, create login.ejs and register.ejs.

register.ejs

<%- include('_header', { title: 'Register' }) %>

<form action="/users/register" method="POST" class="post-form">
  <h2>Register</h2>
  <div class="form-group">
    <label for="username">Username</label>
    <input type="text" id="username" name="username" required />
  </div>
  <div class="form-group">
    <label for="password">Password</label>
    <input type="password" id="password" name="password" required />
  </div>
  <button type="submit">Register</button>
</form>

<%- include('_footer') %>
Enter fullscreen mode Exit fullscreen mode

login.ejs

<%- include('_header', { title: 'Login' }) %>

<form action="/auth/login" method="POST" class="post-form">
  <h2>Login</h2>
  <div class="form-group">
    <label for="username">Username</label>
    <input type="text" id="username" name="username" required />
  </div>
  <div class="form-group">
    <label for="password">Password</label>
    <input type="password" id="password" name="password" required />
  </div>
  <button type="submit">Login</button>
</form>

<%- include('_footer') %>
Enter fullscreen mode Exit fullscreen mode

Implement Routing and Controller Logic

Now, we will create the routes to handle registration and login requests.

Registration Route

Update src/users/users.controller.ts to handle displaying the registration page and form submission:

// src/users/users.controller.ts
import { Controller, Get, Post, Render, Body, Res } from '@nestjs/common';
import { UsersService } from './users.service';
import { Response } from 'express';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get('register')
  @Render('register')
  showRegisterForm() {
    return;
  }

  @Post('register')
  async register(@Body() body: any, @Res() res: Response) {
    // For simplicity, no complex validation here
    await this.usersService.create(body);
    res.redirect('/auth/login'); // Redirect to login page after successful registration
  }
}
Enter fullscreen mode Exit fullscreen mode

Login Validation Route

Create a new auth.controller.ts to handle login requests.

nest generate controller auth
Enter fullscreen mode Exit fullscreen mode

Edit src/auth/auth.controller.ts. Here, we will call the validateUser method. If validation is successful, we'll redirect to the blog's homepage.

If it fails, for simplicity's sake, we won't handle it specially for now. In a real application, you would typically return an error message.

// src/auth/auth.controller.ts
import { Controller, Get, Post, Render, Body, Res, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
import { Response } from 'express';

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

  @Get('login')
  @Render('login')
  showLoginForm() {
    return;
  }

  @Post('login')
  async login(@Body() body: any, @Res() res: Response) {
    const user = await this.authService.validateUser(body.username, body.password);
    if (!user) {
      throw new UnauthorizedException();
    }
    // Validation successful
    res.redirect('/posts');
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, import UsersModule and AuthModule into app.module.ts.

// src/app.module.ts
// ... imports
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';

@Module({
  imports: [
    // ... TypeOrmModule.forRoot(...)
    PostsModule,
    UsersModule,
    AuthModule,
  ],
  // ...
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

At this point, we have completed a basic user registration system and login validation logic. Users can create accounts, and the application can verify their identity.

Let's test it out. Start your project:

npm run start:dev
Enter fullscreen mode Exit fullscreen mode

Visit http://localhost:3000/users/register to register.

ImageP1

After successful registration, you will be automatically redirected to http://localhost:3000/auth/login to log in.

ImageP2

You can test the results of entering correct and incorrect account credentials. For example, if you enter the wrong information, the page will show a 401 Unauthorized error.

ImageP3

However, the current login is just a one-time process, purely for experiencing the login authentication flow; the server does not remember the user's login state.

In the next article, we will complete the login authentication logic to achieve true user session persistence and restrict access to pages and features based on user permissions.


Follow us on X: @LeapcellHQ


Read on our blog

Related Posts:

Top comments (0)