Autorización basa en Roles con Guard en Nest.js
Autenticación: Es el proceso de verificar la identidad del usuario. Se asegura de que el usuario sea quien dice ser, generalmente a través de credenciales como nombre de usuario y contraseña. La autenticación se utiliza para conceder acceso a usuarios legítimos y evitar el acceso no autorizado.
Autorización: Es el proceso de determinar qué acciones o recursos un usuario autenticado tiene permitido acceder o realizar. Una vez que un usuario ha sido autenticado, la autorización se encarga de verificar si ese usuario tiene los permisos adecuados para realizar ciertas acciones o acceder a ciertas partes de la aplicación.
Código Github
Ayúdame a seguir creando contenido 😍
Tienes varias jugosas alternativas:
- Suscríbete al canal de Youtube (es gratis) click aquí
- Si estás viendo un video no olvides regalar un 👍 like y comentario 🙏🏼
- También puedes ser miembro del canal de Youtube click aquí
- Puedes adquirir cursos premium en Udemy 👇🏼👇🏼👇🏼
Muchas gracias por su tremendo apoyo 😊
Guard
En Nest.js, los Guards (guardias) son un mecanismo que te permite controlar el acceso a rutas o puntos finales (endpoints) de tu aplicación. Los Guards se ejecutan antes de que se procese una solicitud a una ruta determinada y pueden permitir o denegar el acceso en función de ciertas condiciones o lógica de negocio.
Agregar role en JWT
WARNING
En la entity del user tiene que existir:
@Column({ default: 'user' })
role: string;
Tambien se podría trabajar como un array de roles.
src\auth\auth.service.ts
const payload = { email: user.email, role: user.role };
RequestWithUser
interface RequestWithUser extends Request {
user: { email: string; role: string };
}
...
@Get('profile')
@UseGuards(AuthGuard)
profile(
@Request()
req: RequestWithUser,
) {
return req.user;
}
@SetMetadata
El decorador @SetMetadata
en Nest.js se utiliza para asignar metadatos personalizados a los controladores, controladores de métodos o parámetros de un método.
Los metadatos son información adicional que puedes adjuntar a estos elementos para usarlos en diversos propósitos, como la configuración de autorización, validación, serialización, etc.
import { SetMetadata } from '@nestjs/common';
// Uso del decorador
@SetMetadata(key, value)
- key: Es el nombre de la clave bajo la cual se almacenarán los metadatos. Puedes elegir cualquier string como clave para identificar los metadatos.
- value: Es el valor que deseas asignar a la clave como metadato. Puede ser cualquier tipo de dato válido en TypeScript (números, strings, objetos, etc.).
Decorador personalizado
src\auth\decorators\roles.decorator.ts
import { SetMetadata } from "@nestjs/common";
export const Roles = (role) => SetMetadata("roles", role);
@Get('profile')
@Roles('user')
@UseGuards(AuthGuard)
profile(
@Request()
req: RequestWithUser,
) {
return req.user;
}
Reflector y Guard
Para acceder a los metadatos establecidos con el decorador @SetMetadata en el controlador o sus métodos, puedes hacer uso del módulo Reflector proporcionado por Nest.js.
El Reflector te permitirá leer los metadatos adjuntados a los controladores o controladores de métodos en tiempo de ejecución.
nest g guard auth/guard/roles --flat ---no-spec
src\auth\guard\roles.guard.ts
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride("roles", [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return user.role === requiredRoles;
}
}
La función getAllAndOverride
del Reflector se utiliza para obtener los metadatos de un controlador o controlador de método, pero con la capacidad de sobrescribir esos metadatos si existen en varios niveles de la jerarquía.
El método getAllAndOverride
toma dos argumentos:
- metadataKey: Este es el nombre de la clave bajo la cual se almacenan los metadatos en el controlador o controlador de método. En este caso, metadataKey es 'roles'.
- metatype: Este es un arreglo que contiene los elementos de los cuales se obtendrán los metadatos. En este caso, se pasa un arreglo con dos elementos:
- context.getHandler(): da como resultado la extracción de los metadatos para el controlador de ruta procesado actualmente.
- context.getClass(): metadatos del controlador de clase.
La función getAllAndOverride
buscará los metadatos en el primer elemento del arreglo (context.getHandler(), que representa los metadatos del controlador de método) y si no los encuentra, buscará en el segundo elemento (context.getClass(), que representa los metadatos del controlador de clase). Si encuentra metadatos en cualquiera de estos elementos, los devolverá y los almacenará en la variable requiredRoles.
@Get('profile')
@Roles('user')
@UseGuards(AuthGuard, RolesGuard)
profile(
@Request()
req: RequestWithUser,
) {
return req.user;
}
Optimizando
src\auth\enums\role.enum.ts
export enum Role {
ADMIN = "admin",
USER = "user",
}
@Get('profile')
@Roles(Role.USER)
@UseGuards(AuthGuard, RolesGuard)
profile(
@Request()
req: RequestWithUser,
) {
return req.user;
}
src\auth\decorators\roles.decorator.ts
import { SetMetadata } from "@nestjs/common";
import { Role } from "../enums/role.enum";
export const ROLES_KEY = "roles";
export const Roles = (role: Role) => SetMetadata(ROLES_KEY, role);
src\auth\guard\roles.guard.ts
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { ROLES_KEY } from "../decorators/roles.decorator";
import { Role } from "../enums/role.enum";
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
console.log(requiredRoles);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return user.role === requiredRoles;
}
}
Decorator composition
src\auth\decorators\auth.decorators.ts
import { applyDecorators, UseGuards } from "@nestjs/common";
import { Role } from "../enums/role.enum";
import { AuthGuard } from "../guard/auth.guard";
import { RolesGuard } from "../guard/roles.guard";
import { Roles } from "./roles.decorator";
export function Auth(role: Role) {
return applyDecorators(Roles(role), UseGuards(AuthGuard, RolesGuard));
}
@Get('profile2')
@Auth(Role.ADMIN)
profile2(
@Request()
req: RequestWithUser,
) {
return req.user;
}
Parte 2 (continuación)
En esta segunda parte realizaremos refactorizaciones y mejoras en el código de la aplicación. Además relacionaremos la entidad Cat con la entidad User para que cada usuario pueda tener sus propios gatos.
Global Hidden password
src\users\entities\user.entity.ts
@Column({ nullable: false, select: false })
password: string;
src\users\users.service.ts
findOneByEmailWithPassword(email: string) {
return this.userRepository.findOne({
where: { email },
select: ['id', 'name', 'email', 'password', 'role'],
});
}
src\auth\auth.service.ts
async login({ email, password }: LoginDto) {
const user = await this.usersService.findOneByEmailWithPassword(email);
...
}
Admin role guard
src\auth\guard\roles.guard.ts
// si es administrador lo dejamos hacer lo que sea :D
if (user.role === Role.ADMIN) return true;
Role enum (common)
src\common\enums\role.enum.ts
export enum Role {
USER = "user",
ADMIN = "admin",
}
User entity (users)
src\users\entities\user.entity.ts
// import { Role } from '../../common/enums/role.enum';
@Column({ type: 'enum', enum: Role, default: Role.USER })
role: Role;
users controllers
src\users\users.controller.ts
@Controller('users')
@Auth(Role.ADMIN)
export class UsersController {
constructor(private readonly usersService: UsersService) {}
}
Active user decorator and interfaces
Nuestro objetivo es llegar a un decorador personalizado que nos permita obtener el usuario activo en el controlador.
@Get()
findAll(
@ActiveUser()
user: ActiveUserInterface,
) {
return this.catsService.findAll(user);
}
- custom-decorators
- createParamDecorator: sirve para inyectar datos adicionales en los parámetros de los controladores.
- ExecutionContext: En el caso de las aplicaciones basadas en Express, el contexto de ejecución es una instancia de la clase
http.Request
.
src\common\decorators\active-user.decorator.ts
import { ExecutionContext, createParamDecorator } from "@nestjs/common";
export const ActiveUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
}
);
src\common\interfaces\active-user.interface.ts
export interface ActiveUserInterface {
email: string;
role: string;
}
src\cats\cats.controller.ts
@Auth(Role.USER)
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get()
findAll(
@ActiveUser()
user: ActiveUserInterface,
) {
console.log(user);
return this.catsService.findAll();
}
}
users OneToMany cats
JoinColumn
- JoinColumn: no solo define qué lado de la relación contiene la columna de combinación con una clave externa, sino que también le permite personalizar el nombre de la columna de combinación y el nombre de la columna de referencia.
- Cuando configuramos @JoinColumn, crea automáticamente una columna en la base de datos llamada propertyName + referencedColumnName.
- Para no instanciar al usuario en el cat, se puede usar @Column userEmail, además esto solo no carga todo el usuario en el cat, solo el email. Más info aquí
TIP
@OneToMany no puede existir sin @ManyToOne: @ManyToOne es el lado propietario de la relación, por lo que es responsable de actualizar la clave externa en la base de datos.
Sin embargo, no se requiere lo contrario: si solo le importa la relación @ManyToOne, puede definirla sin tener @OneToMany en la entidad relacionada. Donde lo establezca @ManyToOne, su entidad relacionada tendrá "id de relación" y clave externa.
src\cats\entities\cat.entity.ts
@ManyToOne(() => User)
@JoinColumn({ name: 'userEmail', referencedColumnName: 'email', })
user: User;
@Column()
userEmail: string;
Ayúdame a seguir creando contenido 😍
Tienes varias jugosas alternativas:
- Suscríbete al canal de Youtube (es gratis) click aquí
- Si estás viendo un video no olvides regalar un 👍 like y comentario 🙏🏼
- También puedes ser miembro del canal de Youtube click aquí
- Puedes adquirir cursos premium en Udemy 👇🏼👇🏼👇🏼
Muchas gracias por su tremendo apoyo 😊
Create cat
src\cats\cats.controller.ts
@Post()
create(
@Body() createCatDto: CreateCatDto,
@ActiveUser()
user: ActiveUserInterface,
) {
return this.catsService.create(createCatDto, user);
}
src\cats\cats.service.ts
async create(createCatDto: CreateCatDto, user: ActiveUserInterface) {
const breed = await this.validateBreed(createCatDto.breed);
return await this.catRepository.save({
...createCatDto,
breed,
userEmail: user.email,
});
}
private async validateBreed(breed: string) {
const breedEntity = await this.breedRepository.findOneBy({ name: breed });
if (!breedEntity) {
throw new BadRequestException('Breed not found');
}
return breedEntity;
}
Find cats by user
src\cats\cats.controller.ts
@Get()
findAll(
@ActiveUser()
user: ActiveUserInterface,
) {
return this.catsService.findAll(user);
}
src\cats\cats.service.ts
async findAll(user: ActiveUserInterface) {
if (user.role === Role.ADMIN) {
return await this.catRepository.find();
}
return await this.catRepository.find({
where: { userEmail: user.email },
});
}
Find cat by id
src\cats\cats.controller.ts
@Get(':id')
findOne(
@Param('id') id: number,
@ActiveUser()
user: ActiveUserInterface,
) {
return this.catsService.findOne(id, user);
}
src\cats\cats.service.ts
async findOne(id: number, user: ActiveUserInterface) {
const cat = await this.catRepository.findOneBy({ id });
if (!cat) {
throw new BadRequestException('Cat not found');
}
this.validateOwnership(cat, user);
return cat;
}
private validateOwnership(cat: Cat, user: ActiveUserInterface) {
if (user.role !== Role.ADMIN && cat.userEmail !== user.email) {
throw new UnauthorizedException();
}
}
Update cat
src\cats\cats.controller.ts
@Patch(':id')
update(
@Param('id') id: number,
@Body() updateCatDto: UpdateCatDto,
@ActiveUser()
user: ActiveUserInterface,
) {
return this.catsService.update(id, updateCatDto, user);
}
src\cats\cats.service.ts
async update(id: number, updateCatDto: UpdateCatDto, user: ActiveUserInterface) {
await this.findOne(id, user );
return await this.catRepository.update(id, {
...updateCatDto,
breed: updateCatDto.breed ? await this.validateBreed(updateCatDto.breed) : undefined,
userEmail: user.email,
})
}
Delete cat
src\cats\cats.controller.ts
@Delete(':id')
remove(
@Param('id') id: number,
@ActiveUser()
user: ActiveUserInterface,
) {
return this.catsService.remove(id, user);
}
src\cats\cats.service.ts
async remove(id: number, user: ActiveUserInterface) {
await this.findOne(id, user);
return await this.catRepository.softDelete({ id });
}
Conclusiones
Si estos videos y tutoriales te gustan no olvides apoyar suscribiendote al canal aquí y dando like al video. Si tienes alguna duda o sugerencia puedes dejarla en los comentarios. Nos vemos en el próximo video.