Sistema de autenticación con NestJS – Semana 2

nestjs with auth

Antes de empezar

Esta es la semana 2 de la serie Sistema de autenticación con NestJS. Puedes encontrar todas las semanas publicadas aquí.

Puedes encontrar todo el código de esta serie en el repositorio.

Y recuerda que siguiendo la serie de youtube vas a tener explicaciones mucho más detalladas.

Y si quieres estar al día con nuestros contenidos suscríbete a nuestro canal de youtube y síguenos en twitter

Lanzar el servidor de desarrollo

Como mencionamos la semana pasada hemos dejado la opción synchronize a true mientras que desarrollamos la aplicación.

Ahora vamos a lanzar el servidor de desarrollo de nestjs mediante el siguiente comando y ver comó nestjs actualiza la base de datos:

				
					yarn start:dev
				
			

Como ves el coando falla con un error que dice: Custom repository UserRepository not found.

Esto es porque no hemos añadido el decorator a nuestro user repository. Entonces añadimos el EntityRepository decorator a nuestro repositorio.

				
					// src/auth/users.repository.ts
import { EntityRepository, Repository } from 'typeorm';
import { User } from './user.entity';

@EntityRepository(User)
export class UsersRepository extends Repository<User> {}
				
			

Ahora vemos que el comando que hemos ejecutado falla con otro error que es debido a falta de atributos en nuestra entidad de usuario.

Atributos de la entidad User

Añadimos los atributos que necesitamos para nuestra entidad del Usuario.

				
					import { Entity } from 'typeorm';

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

  name: string;

  email: string;

  password: string;

  active: boolean;

  createdOn: Date;
}
				
			

Y después añadimos la definición de las columnas de la base de datos mediante los decorators.

				
					import {
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
} from 'typeorm';

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

  @Column({ length: 20 })
  name: string;

  @Column({ length: 100 })
  email: string;

  @Column({ length: 100 })
  password: string;

  @Column({ type: 'boolean', default: false })
  active: boolean;

  @CreateDateColumn()
  createdOn: Date;
				
			

A parte del decorator Column usamos PrimaryGeneratedColumn para que typeorm nos generate un uuid automaticamente y CreatedDateColumn para que cuando se crea un usuario typeorm automáticamente ponga la fecha de creación en el campo createdOn.

Una vez que guardemos este fichero podemos ver en la base de datos que la tabla esta creada con todas las columnas definidas.

Si quitamos cualquier atributo de nuestro usuario y guardamos el fichero vemos que automáticamente nestjs borra la columna correspondiente en la base de datos. Este comportamiento no nos interesa en un entorno de producción ya que el mejor proceso para borrar una columna es primero renombrarla columna, esperar una semana y borrarla cuando estemos seguro que todo esta bien.

Por eso es importante no dejar la opción de synchronize a true en un entorno que no sea el de desarrollo.

Registro de usuarios

Ahora vamos a empezar con el primer caso de uso que es el registro.

Lo primero que vamos a hacer en el registro es lo que se denomina happy path en el ingles y es cuando todo va bien.

Lo que vamos a necesitar son 3 valores validos para registrar un usuario: Email, nombre y contraseña.

Primero creamos un DTO para poder mover los datos entre las diferentes capas.

				
					// src/auth/dto/register-user.dto.ts
export class RegisterUserDto {
  name: string;

  email: string;

  password: string;
}
				
			

Y empezamos a añadir el método a la capa más baja infraestructura donde tenemos el repositorio.

				
					// src/auth/user.repository.ts
import { EntityRepository, Repository } from 'typeorm';
import { RegisterUserDto } from './dto/register-user.dto';
import { User } from './user.entity';

@EntityRepository(User)
export class UsersRepository extends Repository<User> {
  async createUser(registerUserDto: RegisterUserDto): Promise<void> {}
}
				
			

Y después vamos al servicio que es parte de nuestra capa del dominio y le añadimos el repositorio como dependencia y el método registerUser que va a llamar al repositorio.

				
					// src/auth/user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { RegisterUserDto } from './dto/register-user.dto';
import { UsersRepository } from './users.repository';

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(UsersRepository)
    private usersRepository: UsersRepository,
  ) {}

  async registerUser(registerUserDto: RegisterUserDto): Promise<void> {
    return this.usersRepository.createUser(registerUserDto);
  }
}
				
			

El decorator InjectRepository se asegura de que se pase una instancia de UserRepository a nuestro servicio.

Y por lo ultimo nos vamos al controlador que es parte de la capa de aplicación

				
					// src/auth/user.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { RegisterUserDto } from './dto/register-user.dto';

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

  @Post('/register')
  register(@Body() registerUserDto: RegisterUserDto): Promise<void> {
    return this.authService.registerUser(registerUserDto);
  }
}
				
			

En esta clase hemos usado dos decorators (Post Y Body) que nos facilitan recibir una llamada post y convertir el body del post a nuestro DTO.

Añadir la lógica al repositorio

Ahora vamos a añadir la lógica de persistencia a nuestro repositorio.

				
					// src/auth/user.repository.ts
import { EntityRepository, Repository } from 'typeorm';
import { RegisterUserDto } from './dto/register-user.dto';
import { User } from './user.entity';

@EntityRepository(User)
export class UsersRepository extends Repository<User> {
  async createUser(registerUserDto: RegisterUserDto): Promise<void> {
    const { name, email, password } = registerUserDto;
    const user = this.create({ name, email, password });
    await this.save(user);
  }
}
				
			

Nuestra clase de User tiene 5 campos pero el id y createdOn se auto-generan por typeorm y el campo active es falso por defecto por lo tanto no hace falta hacer nada con ellos a la hora de crear el usuario.

Ya tenemos nuestro happy path hecho.

Ahora si hacemos una llamada post a https://localhost:3000/auth/register  con el siguiente body vemos que se crea la fila en la base de datos. Con el código actual aunque pasamos un correo invalido o duplicado no se falla la llamada.

				
					{
  "name": "Juan",
  "email": "juan@api.com",
  "password": "pass"
}
				
			

Prevenir email duplicado

Ahora vamos a prevenir que alguien registre con un email que ya esta registrado en nuestra base de datos.

Para ello añadimos el argumento unique: true al decorator Column del atributo email dentro de la clase User.

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

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

  @Column({ length: 20 })
  name: string;

  @Column({ length: 100, unique: true })
  email: string;

  @Column({ length: 100 })
  password: string;

  @Column({ type: 'boolean', default: false })
  active: boolean;

  @CreateDateColumn()
  createdOn: Date;
}
				
			

Y cuando guardamos el fichero vemos que el comando yarn start:dev empieza a aplicar el cambio en la base de datos. Ahora si intentamos a hacer otra llamada post con el mismo correo recibimos un 500.

Así ya tenemos protegido nuestro api contra emails duplicados pero no nos interesa devolver un 500 ya que eso no da ninguna información útil al usuario.

Capturar y convertir error

Para devolver un error más útil al usuario capturamos el error en el repositorio y devolvemos un ConflictException  con un mensaje descriptivo.

				
					// src/auth/user.repository.ts
import {
  ConflictException,
  InternalServerErrorException,
} from '@nestjs/common';
import { EntityRepository, Repository } from 'typeorm';
import { RegisterUserDto } from './dto/register-user.dto';
import { User } from './user.entity';

@EntityRepository(User)
export class UsersRepository extends Repository<User> {
  async createUser(registerUserDto: RegisterUserDto): Promise<void> {
    const { name, email, password } = registerUserDto;
    const user = this.create({ name, email, password });

    try {
      await this.save(user);
    } catch (e) {
      if (e.code === 'ER_DUP_ENTRY') {
        throw new ConflictException('This email is already registered');
      }
      throw new InternalServerErrorException();
    }
  }
}
				
			

Ahora si volvemos a hacer la llamada post vemos que el api nos devuelve un 409 con el mensaje This email is already registered.

Y ahora vamos a añadir validación sobre nuestro DTO para que protegemos nuestro api contra datos invalidos.

				
					// src/auth/dto/register-user.dto.ts

import { IsEmail, IsNotEmpty, IsString, Length } from 'class-validator';

export class RegisterUserDto {
  @IsNotEmpty()
  @IsString()
  @Length(2, 20)
  name: string;

  @IsNotEmpty()
  @IsEmail()
  email: string;

  @IsNotEmpty()
  @Length(6, 20)
  password: string;
}
				
			

Ahora para que se vea bien los errores en la respuesta vamos a añadir un pipe de nestjs a nuestro app.

				
					// src/main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();
				
			

Y ahora si volvemos a hacer una llamada de post con el siguiente json vemos que recibimos un error de 400 con el mensaje correspondiente.

				
					{
  "name": "1",
  "email" "juan@api.com",
  "password": "pass"
}
				
			

Esto es todo para esta semana.

Recuerda que tenemos esta misma serie en nuestro canal de youtube donde Juan explica todo con mucho más detalles.

Subscribete a nuestro newsletter

Contenidos semanales y descuentos exclusivos
Exclusivo

Social share

Share on facebook
Facebook
Share on twitter
Twitter
Share on linkedin
LinkedIn
Share on telegram
Telegram