Users and password hashing
So that only registered authors can write posts, the blog needs users. A password must not be stored in the database as is: if the database leaks, an attacker gets everyone's passwords. Instead a hash is stored — a string produced from the password by a one-way transformation. You can check a password against its hash, but you cannot recover the password from the hash.
Installation and the resource
npm i bcrypt
npm i -D @types/bcrypt
nest g resource usersAnswer n to the CRUD entry points question. bcrypt is a password hashing library; @types/bcrypt describes its types for TypeScript.
The user entity
// src/users/entities/user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column()
name: string;
@Column()
password: string;
@Column({ default: 'user' })
role: string;
}unique: true forbids two users with the same email. The role field is needed in the roles lesson. In UsersModule add imports: [TypeOrmModule.forFeature([User])] and exports: [UsersService] — the login module will need the users service.
The registration DTO
// src/users/dto/create-user.dto.ts
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@IsNotEmpty()
name: string;
@IsString()
@MinLength(8)
password: string;
}The service
// src/users/users.service.ts
import { ConflictException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import * as bcrypt from 'bcrypt';
import { Repository } from 'typeorm';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from './entities/user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
) {}
async create(dto: CreateUserDto): Promise<User> {
const exists = await this.usersRepository.existsBy({ email: dto.email });
if (exists) {
throw new ConflictException('This email is already registered');
}
const password = await bcrypt.hash(dto.password, 10);
return this.usersRepository.save(this.usersRepository.create({ ...dto, password }));
}
findByEmail(email: string): Promise<User | null> {
return this.usersRepository.findOneBy({ email });
}
}bcrypt.hash() turns the password into a hash. The number 10 is the cost: the higher it is, the longer the hash takes to compute and the harder it is to brute-force. A random "salt" is added every time, so identical passwords produce different hashes.
The registration route
// src/users/users.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
async register(@Body() dto: CreateUserDto) {
const user = await this.usersService.create(dto);
return { id: user.id, email: user.email, name: user.name };
}
}Send POST /users with email, name and password — you get the new user without the password. In the database the password column holds a string like $2b$10$.... Registering again with the same email returns 409.
Checking a password
At login the password is compared with the hash using bcrypt.compare():
// src/auth/auth.service.ts
import * as bcrypt from 'bcrypt';
const isValid = await bcrypt.compare('entered password', user.password);It returns true or false. The JWT lesson puts it to work.
Common mistake
A method returns the whole user entity, and the password hash goes to the client in the JSON. Even a hash must not be exposed: it can be brute-forced without limits on someone's own computer. For now return only the needed fields, as in the controller above; in the serialization lesson the password field is hidden automatically.