Obteniendo 贸rdenes del perfil

16/22

Lectura

En la clase anterior vimos como puedes hacer para identificar un rol y de acuerdo a eso poder darle acceso a un endpoint, ahora te pongo el siguiente escenario:

驴Como crear un endpoint que retorne todas las 贸rdenes relacionados con el usuario que tiene sesi贸n?

Una soluci贸n v谩lida a este problema es que puedes crear un endpoint que envi谩ndole el ID de un usuario te retorne dichas 贸rdenes, algo como esto:

http://localhost:3000/users/1/orders
Este endpoint me retornar铆a todas las 贸rdenes del usuario con ID 1, sin embargo puede ser algo peligroso, es decir, este endpoint solo deber铆a estar disponible para los roles administrativos, pero no para los usuarios porque si conoces el ID de alg煤n cliente podr铆as obtener la informaci贸n de 贸rdenes de compra de un usuario.

La soluci贸n m谩s pr谩ctica para este caso es crear un endpoint de este tipo:

http://localhost:3000/profile/my-orders

Como ves en el endpoint anterior no est谩s colocando de forma expl铆cita el ID de un usuario. 驴Entonces como sabe a qu茅 usuario le pertenecen las 贸rdenes?聽Sencillo, s铆 nuestro usuario ya tiene una sesi贸n, esta informaci贸n ya est谩 en el JWT que se le otorg贸 a ese usuario al autentificarse en el sistema as铆 con eso podemos inferirlo de acuerdo a la sesi贸n

Ok, ok, cerebrito, suena bien pero, 驴y el c贸digo鈥? Vamos a iniciar con ello. Lo primero es que vamos a hacer es crear un nuevo controlador para todos las solicitudes que sean de este tipo. Lo vamos a crear con el nombre ProfileController.

nest g co users/controllers/profile --flat

All铆 vamos a exponer un nuevo endpoint que va a recibir la solicitud y gracias al decorador de聽
@UseGuards(JwtAuthGuard,聽RolesGuard) nos aseguramos que tenga un token v谩lido, as铆 que en ese m茅todo recogemos la informaci贸n de usuario que tiene esa sesi贸n as铆:

  @Roles(Role.CUSTOMER)
  @Get('my-orders')
  getOrders(@Req() req: Request) {
    const user = req.user as PayloadToken;
    return this.orderService.findOne(user.sub);
  }

Simplemente con decirle que dentro del m茅todo que queremos la informaci贸n del Request podemos obtener la informaci贸n del usuario que tiene sesi贸n, luego simplemente obtenemos el ID y ya con eso podemos crear un m茅todo en nuestro servicio de 贸rdenes que las retorne, algo as铆:

  ordersByCustomer(customerId: number) {
    return this.orderRepo.find({
      where: {
        customer: customerId,
      },
    });
  }

Este ser铆a el c贸digo completo del nuevo controlador ProfileController:

import { Controller, Get, UseGuards, Req } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';

import { Request } from 'express';

import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../../auth/guards/roles.guard';
import { Roles } from '../../auth/decorators/roles.decorator';
import { Role } from '../../auth/models/roles.model';
import { PayloadToken } from 'src/auth/models/token.model';
import { OrdersService } from '../services/orders.service';

@UseGuards(JwtAuthGuard, RolesGuard)
@ApiTags('profile')
@Controller('profile')
@Controller('profile')
export class ProfileController {
  constructor(private orderService: OrdersService) {}

  @Roles(Role.CUSTOMER)
  @Get('my-orders')
  getOrders(@Req() req: Request) {
    const user = req.user as PayloadToken;
    return this.orderService.ordersByCustomer(user.sub);
  }
}

Puedes ver toda esta soluci贸n en la rama 14.

Aportes 5

Preguntas 1

Ordenar por:

驴Quieres ver m谩s aportes, preguntas y respuestas de la comunidad? Crea una cuenta o inicia sesi贸n.

A mi no me tra铆a las ordenes del cliente, debido a que el JWT el sub viene firmado con el id del usuario y no del customer.
Mi soluci贸n fue la siguiente:
Inyect茅 el repositorio de usuario en el constructor de OrdersService:

 @InjectRepository(User) private userRepo: Repository<User>,

Luego modifiqu茅 el m茅todo de la lectura de la siguiente manera:

async ordersByCustomer(userId: number) {
    const customerId = (await this.userRepo.findOne(userId,{relations: ["customer"]})).customer.id;
    return this.orderRepo.find({
      where: {
        customer: customerId,
      },
      relations: ['items', 'items.product'],
    });
  }

Y as铆 ya me trae las ordenes de mi cliente y con sus respectivos productos por orden.

Espero les ayude en algo.
Excelente curso!

Comparto el codigo para MongoDB

PayloadToken

export interface PayloadToken {
  role: string;
  sub: string;
}

AuthService

generateJWT(user: User) {
    const payload: PayloadToken = { role: user.role, sub: user._id };
    return {
      access_token: this.jwtService.sign(payload),
      user,
    };
  }

ProfileController

@Roles(Role.CUSTOMER)
  @Get('my-orders')
  getOrders(@Req() req: Request) {
    const user = req.user as PayloadToken;
    return this.orderService.ordersByUser(user.sub);
  }

OrdersService

async ordersByUser(customer: string) {
    return await this.orderModel.findOne({customer}).exec();
}

Para mongo:

Para realizar este ejercicio hay un error de logica, primero deben agregar el objeto de 鈥淐ustomer鈥 como propiedad referenciada, es una relacion 1:1 asi que no hay problema.

// user.entity.ts

@Schema()
export class User extends Document {
  @Prop({ required: true })
  name: string;

  @Prop({ required: true, unique: true })
  email: string;

  @ExcludeProperty()
  @Prop({ required: true })
  password: string;

  @Prop({ required: true })
  role: string;

  @Prop({ type: Types.ObjectId, ref: Customer.name, required: true })
  customer: Customer | Types.ObjectId; // relacion 1:1 referenciada
}

La estructura del controlador no tiene errores asi que esta todo en orden, pero la consulta la debemos cambiar porque en si establecimos una relacio en el paso anterior hasta ahora:

// order.service.ts

async ordersByCustomer(userId: string) {
    
    /*Nos traemos el usuario debido a que es quien contiene la referencia del customer.*/
    const user = await (await this.userService.getUser(userId)).toJSON();

/*Hacemos la consulta en base al customer ID*/
    return await this.orderModel
// como el id es un objeto debemos transformarlo a un string para poder comparar, sino da error.
      .find({ customer: user.customer._id.toString() }) 
      .populate('products') // populamos la consulta
      .exec();
  }

Puede que tengan un error de tipado, esto es debido a que el sub de nuestro PayloadToken es de tipo number y los 鈥淚d鈥 de mongo son ObjectId o Strings, asi que debemos cambiarlo tambien:

// token.model.ts

export interface PayloadToken {
  role: string;
  sub: string;
}

Espero que les halla funcionado.

Decorador:

// auth/current-user.decorator.ts
import {
  createParamDecorator,
  ExecutionContext,
  PreconditionFailedException,
} from '@nestjs/common';

export const CurrnetUser = createParamDecorator(
  (_: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const { user } = request;

    if (!user) {
      throw new PreconditionFailedException('User not authenticated.');
    }

    return user;
  },
);

Uso:

// user/profile.controller.ts

// ... agrega UseGuards(JwtAuthGuard)...

@UseGuards(JwtGuard, RolesGuard)
@Controller('profile')
export class ProfileController {
	// ... en el m茅todo de tu controlador, hacer: .. 
	@Get()
	myMethod(@CurrentUser() user: PayloadToken) {
		return this.myService.findOne(user.sub);
	}
}

馃槂

El m茅todo del servicio est谩 mal y no retornar谩 nunca nada. La soluci贸n es la siguiente

  ordersByCustomer(customerId: number) {
    return this.orderRepo.find( { customer: customerId } ); //馃憟馃徎 ac谩 est谩 el problema
  } 

Ya que el 鈥榦rderRepo鈥 est谩 tipado con el modelo de Order. Entonces hay que indicarle que filtre por alguno de estos par谩metros. En nuestro caso queremos que traiga todas las 贸rdenes del customer que haga la petici贸n(recuerden que el id viene v铆a JWT)

@Schema()
export class Order extends Document{
  @Prop({type: Date})
  date: Date;

  @Prop({type: Types.ObjectId, ref: Customers.name, required: true})
  customer: Customers | Types.ObjectId;

  @Prop({type: [{type: Types.ObjectId, ref: Product.name}]})
  products: Types.Array<Product>;
}