¿Cómo manejar las órdenes de compra con autenticación?
En la implementación de sistemas que gestionan órdenes de compra, es esencial proteger la información de los usuarios. En la clase, hemos aprendido cómo, mediante el uso de JWT (JSON Web Tokens), podemos hacer que nuestras aplicaciones sean más seguras y a la vez más eficientes. Este proceso permite que los clientes autenticados consulten sus órdenes sin tener que enviar su identificador repetidamente. Vamos a desglosar este procedimiento.
¿Cómo crear un router para manejar las órdenes de compra?
Para empezar, necesitamos un nuevo router que maneje las órdenes de compra por usuario autenticado. Usamos Express y JWT para desarrollar este nuevo endpoint:
Este nuevo router crea un endpoint que devuelve las órdenes de compra para el usuario autenticado, extrayendo su ID del payload del token.
¿Cómo realizar consultas complejas con asociaciones?
Para manejar la relación entre tablas, empleamos Sequelize, que ofrece la capacidad de realizar consultas complejas. En este caso, tenemos relaciones de uno a uno entre usuario y cliente y cliente y orden.
Esta consulta utiliza las asociaciones para encontrar las órdenes correspondientes al usuario autenticado, sin requerir el envío explícito del ID del cliente en cada solicitud.
¿Qué beneficios ofrece el uso de JWT en esta implementación?
El uso de tokens JWT ofrece varias ventajas:
Seguridad: Los tokens están firmados y tienen una fecha de expiración, lo que previene su uso indevido.
Simplicidad: Permite evitar la transmisión constante de credenciales o identificadores del usuario, ya que el token almacena información suficiente para autenticación.
Escalabilidad: Facilita la implementación en aplicaciones distribuidas, donde la autenticación se puede descentralizar de manera segura.
Reto adicional: Automatizando la asociación en nuevas órdenes de compra
Cuando un usuario autenticado desea crear una nueva orden de compra, el sistema debería poder deducir su customerId a partir del userId almacenado en el token:
Con este enfoque, eliminamos el paso manual de tener que enviar el customerId, lo que simplifica el proceso para el usuario y minimiza errores potenciales.
Este tipo de implementaciones no solo ayudan a mantener la integridad y seguridad de los datos del usuario, sino que también proporcionan una experiencia de usuario más fluida y agradable. ¡Sigue explorando y mejorando tus habilidades en el manejo de autenticación y seguridad en aplicaciones web!
Luego en order.service.js Modificamos el create() para que la inserción sea automatizada con solo enviar el sub o userId, hacemos una busqueda findOne al customer Where user .id sea igual a data.userId. Donde este se almacenará en customer el cual tendremos que extraer el ID para enviarlo al create de order. Si no se encuentra se regresa un no encontrado.
Y quitarle el required en el schema de validacion al crear las ordenes.
Buena solucion!
Hola a todos.
Estaba recibiendo este error:
Y esto se remonta al curso pasado de "Node con Postgres" donde yo continue con mi progreso.
Si realizaba hacer un post de una orden o consultar todas las ordenes, me salia el mismo error.
Esto se corrige poniendo una condicional en el modelo de ordenes en el campo virtual total. Ponemos una condicional que diga si exista "items" entonces que proceda con su respectiva validacion de "this.items.length"
total:{type:DataTypes.VIRTUAL,get(){//Reviso si tenemos productosif(this.items){console.log('existen items en la orden')if(this.items.length>0){returnthis.items.reduce((total, item)=>{return total +(item.price* item.OrderProduct.amount)},0)}return0}}}
tu validación la peudes resumir así: this.items && this.items.length > 0
Muchas gracias
Para poder ver las órdenes de compra de un usuario podemos usar el token que tiene, obtener el sub y obtener la información directamente sin necesidad de enviar el ID del usuario.
Se crea un nuevo método en el servicio orders, el método findByUser recibe el userId y realiza una consulta respecto a éste donde se incluye la asociación de user y customer, y se desea obtener el id del customer ya que únicamente se cuenta con el id de usuario, es por ello que se utiliza where: {'$customer.user.id$': userId}, es decir, le estamos diciendo a qué asociaciones estamos haciendo la consulta.
Se crea un nuevo router profile.router.js, aquí se obtiene el id del usuario que está en este momento con sesión (const orders = await service.findByUser(user.sub)), el id está en el sub del payload. Es decir, ya no es necesario enviar el user id porque ya viene en el token y se va a obtener de ahí.
Para resolver el reto de crear la order sin neesidad de enviar el customerId en el body de la petición hariamos lo siguente:
En order.router.js
router.post('/', passport.authenticate('jwt',{session:false}),//validatorHandler(createOrderSchema, 'body'),async(req, res, next)=>{try{const body ={userId: req.user.sub};const order =await service.create(body); res.status(201).json(order);}catch(error){next(error);}});
Hay que eliminar el validator porque ahora no es necesario pasar en el body el customer id, pero se agrega el jwt validator
Luego en order.service.js
asynccreate(data){// Accedemos al modelo Customer y usando where encadenamos hacia userconst customer =await models.Customer.findAll({where:{'$user.id$': data.userId},include:['user']});// Validamos que exista el customerif(!customer){throw boom.notFound('Customer not found');}// Creamos un objeto con el customerId obtenido de la consultaconst dataOrder ={customerId: customer[0].id};const newOrder =await models.Order.create(dataOrder);return newOrder;}
TRas un poco de depuración con este metodo vi que los resultados de la consulta llegan en un Array, si se canbia el FinAll por findOne en ese caso solo retorna un unico objeto y no haria falta usar el customer[0] sino directamente customer
Para crear una orden necesitamos el customerID ¿Cómo obtenerlo del token?
Agregué la autenticación por token en el post de Orders
Modifiqué el schema de Orders para que customerId ya no sea solicitado.
Construí una función en los servicios de Customer que encuentre un Customer que contenga el mismo userId que le paso como argumento.
En el Router de Orders importé los servicios de Customer y usé la función que me devuelve el customer en función al userId que obtengo de res.user.sub
El Body a enviar lo he de-construído y he agregado un customerId: customer. id.
En customer.service.js
asyncfindByUser(userId){const customer =await models.Customer.findOne({where:{userId: userId }});if(!customer){throw boom.notFound("Sorry, user not found");}return customer;}
¿Qué desventaja traeria usar una sola tabla para el usuario y el customer en lugar de dos?
Hola, depende a tu caso si realmente no tienes varios roles o tipos de usuario en el sistema puedes dejar esa información en una sola tabla, pero si luego tienes diferentes tipos de usuarios se puede usar herencia en bases de datos o hacer un modelado que pueda soportar múltiples tipos de roles.
Como dice Nico, además en este caso tenemos dos roles: clientes y administradores. Para poder tratar a los clientes necesitamos los datos que ingresamos en la tabla customer, pero de los administradores no necesitamos ese dato. Ambos son usuarios, pero solo de uno necesitamos mas información
📋 Clase #13: Obteniendo órdenes del perfil 13/20 📋
Se quiere obtener las ordenes del usuario sin necesidad de enviar el id del usuario, para ello se usa el token del usuario cuando hizo login que a su vez tiene un identificador llamado “sub”.
Pasos:
Vamos a ++VSC++, abrimos la carpeta services y abrimos el archivo order.service.js, se implementa la lógica para la función buscar por usuario: findByUser (mas info: aquí), el código queda:
asyncfindByUser(userId){//Se hace la consulta a pesar que se tiene el id de user y no de customerconst orders =await models.Order.findAll({//Cuando hay tipos de estas asociaciones, se usa: wherewhere:{'$customer.user.id$': userId
},include:[{//Customer tiene una relación con user (relación de tablas)association:'customer',//Con user se puede filtrar esas ordenes de compra asociadas a ese usuario y a customerinclude:['user']}]});return orders;}
Guardamos, ahora necesitamos crear una nueva ruta, vamos a la carpeta routes y creamos el archivo profile.routes.js, se implementará la lógica para obtener las ordenes del usuario pasándole el sub del payload y no el id del user, el código queda:
const express=require('express');const passport=require('passport');constOrderService=require('../services/order.service');const router=express.Router();const service=newOrderService();router.get('/my-orders', passport.authenticate('jwt',{session:false}),async(req,res,next)=>{try{//Necesitamos el user para obtener el identificador subconst user=req.user;//Las ordenes de compraconst orders=await service.findByUser(user.sub); res.json(orders);}catch(error){next(error);}});module.exports=router;
Guardamos, necesitamos actualizar el index.js de la carpeta routes, se agrega después de authRouter:
const profileRouter =require('./profile.router');
La ruta dentro de routerApi queda:
router.use('/profile', profileRouter);
Guardamos, si no se ha compilado con: npm run dev en la terminal, esperar a Mi port 3000
Vamos a ++Insomnia++, entramos a la carpeta Auth, duplicamos el Login y lo editamos y colocamos Get orders y la solicitud en GET, la dirección: _.API_URL/api/v1/profile/my-orders, antes de consultar, en la pestaña de Auth, se selcciona “Bearer” y en TOKEN se pega el token del usuario autenticado en Auth. Si le damos a ++Send++, debe salir un código 200 OK y como no se han hecho registros de ordenes, nos sale un array vacío: []
Para llenar el array de las ordenes de compra, debemos añadir las ordenes. Para ello necesitamos saber el id del usuario, pero necesitamos el id del customer porque lo creamos desde esa tabla, para conocer ese Id, vamos al navegador, entramos a ++pgAdmin++.
Cuando entramos a ++pgAdmin++, al lado izquierdo hay un panel donde sale “Servers”, al darle click sale una lista de jerarquía desplegable, ir a MyStore > Databases > my_store > Schemas > public > Tables > customers, le damos con el botón derecho del mouse sobre customers y seleccionamos View/Edit Data > All Rows, aparece la tabla con todos los customers, buscamos el usuario del token e identificamos en la primera columna el id.
Vamos a ++Insomnia++ y dentro de la carpeta Orders, seleccionamos POST Create orders (dirección: _ . API_URL/api/v1/orders) y en el body de JSON se coloca el id que identificamos en ++pgAdmin++, en mi caso:
{"customerId":2}
Para agregar productos a la orden, debemos tener algunos productos creados, en caso de que no se hayan creado, vamos a la carpeta de Products, seleccionamos POST Create products (dirección: _ . API_URL/api/v1/products) y vamos creando productos solo editando el name (se puede editar el resto de items si se desea), por ejemplo:
{"name":"Product 2","price":"30","description":"example and test 2","image":"http: // placeimg. com/640/480","categoryId":2}
Al dar ++Send++, nos va arrojando un código 201 Created cada vez que creamos un producto:
{"createdAt":"2023-03-29T02:29:31.823Z","id":2,"name":"Product 2","price":30,"description":"example and test 2","image":"http: // placeimg. com/640/480","categoryId":2}
Ahora vamos a la carpeta de Orders y seleccionamos POST Create Items (dirección: _ . API_URL/api/v1/orders/add-item), se debe especificar los siguientes items:
Si agregamos varios items y queremos consultarlos todos, vamos a la carpeta de Auth en Get orders y al dar ++Send++, nos arroja un código 200 OK y todos los items con su respectiva información:
Se quiere crear una orden sin necesidad de pasar el ++ID del customer++, es decir, que en el body en ++Insomnia++ cuando se hace la petición POST esté vacío.
Vamos a la carpeta routes y abrimos el archivo order.router.js, en la cabecera agregamos a passport:
//Librería del passport para realizar el POSTconst passport=require('passport');
Luego modificamos la función del POST, donde incorporamos la lógica de passport en lugar de validatorHandler, el código queda:
router.post('/',//Se implementa aquí esta capa del passport passport.authenticate('jwt',{session:false}),async(req, res, next)=>{try{const body ={userId: req.user.sub};const newOrder =await service.create(body); res.status(201).json(newOrder);}catch(error){next(error);}});
Guardamos, vamos a la carpeta de services, abrimos el archivo order.service.js y se edita la función de Create(data) ya que antes de generar una nueva orden agregándola a la tabla de Order, se debe primero buscar la data que coincida con el token, una vez que se encuentre, se extrae el userId que se pasaba manualmente antes por el body de la petición, el código queda:
asynccreate(data){//Se hace la consulta ya que no se tiene el id de customerconst customer =await models.Customer.findOne({//Cuando hay tipos de estas asociaciones, se usa: wherewhere:{'$user.id$': data.userId,},//Con user se puede filtrar esas ordenes de compra asociadas a ese usuario y a customerinclude:['user'],}); data.customerId= customer.id;//delete data.userId;//Se crea la orden en la tabla de valoresconst newOrder =await models.Order.create(data);return newOrder;}
Guardamos, si no se ha compilado con: npm run dev en la terminal, esperar a Mi port 3000
Vamos a ++Insomnia++, entramos a la carpeta de Orders y en Create ordes, con la peticón POST no agregamos nada en el body, pero tenemos que seleccionar en la pestaña de Auth a “Bearer” y colocar en TOKEN el token que corresponde del user, al presionar ++Send++, debe salir un código 201 Created:
Este es el reto finalizado, sin embargo al ver que varios compañeros tenian que comentar el validor del schema vi que no deberia ser correcto ya que se está eliminando capas de seguridad.
✨🦄 Recordé que en la tabla de Customers ya había una referencia al userId
Por lo que la inclusión del customerId a la nueva orden se simplifica bastante.
Modifiqué la ruta de la siguiente manera:
Y el servicio de create solamente así:
Como ven, solamente usé el userId del modelo de Customer para la búsqueda y la obtención del id
tengo un customer de id = 5 el cual su usuario es = 10, en el payload del token viene el sub = 10 pero para crear la orden necesito es pasar el customerId = 5, si creo la orden con los datos del payload, va asignar la orden a otra persona, alguien resolvio esto ?
Reto 😄...
.
order.schema: Quitamos el schema de creación.
Hola!
Tengo un problema, cuando trato de hacer el post, para crear una nueva orden, me da este error.
Y hago un console.log de los req.body y me da un objeto vacío.
A que se debe este error ?
Hola! 👋
En tu EndPoint si creaste productos de ejemplo?
de igual forma revisa este ejercicio de este platzinauta
no comprendo a qué se refiere con 'ir un poco más allá' cuando hace el '$customer.user.id': userId
es decir, en la tabla customer tenemos el atributo user_id, no creo que se refiera a ese atributo, está yendo a la tabla user y al atributo id?
estaria mal si en en los servicios de modelo hago una busqueda asi:
1 busco un user por id en la tabla User.
2 luego busco ese user como propietario en la tabla Order.
es decir esta mal crear un servicio mesclando modelos, osea al fina retorna una orden pero tube que hacer uso de models.User ?
Pero en la tabla customer tenemos el, id del usuario no
Yo solucione el reto de la siguiente manera.
Elimina el esquema para la creación de ordenes
El siguiente paso viene en lo que seria la ruta orders
const user = req.user;const rta =await service.create(user);
Bueno, si esto no te llegara a funcionar, sería porque yo estoy trabajando este proyecto de otra manera pero estoy casi seguro de que funcionaria para todos los casos 😅
Para crear la orden opté por hacer una subconsulta basada en el userId. Me parece que puede ser beneficioso a escala, pues cuando traemos el customer por separado y luego la inserción tendríamos 2 consultas a la db, y nos generan algo de latencia (sobretodo si la db esta en un server remoto).
asynccreate(userId){const order =await models.Order.create({customerId: sequelize.literal(`(SELECT id FROM customers WHERE user_id = ${userId})`),});return order;}
¡Hola compañeros!
Esta fue mi solución:
En orders.router quité el middleware de verificación y, envíe el sub en el create():
Hola Carlos! Gracias por compartir tu solución.
Pero encuentro un pequeño error en la lógica de tu order.service.
Si no utilizamos el where en el método .findOne() este va a buscar en la columna de primary_key. Es decir si el enviamos un userId=5, este te va a retornar el customer con Id = 5 y no es eso lo que queremos. Es posible que el user con id=5 esté ligado en realidad al customer id=8.
.
Deberíamos ser así:
El reto me costo un poquito, por un error que tenia, veo en los aportes que muchos lo hicieron de una forma distinta a mi, utilizando una variable body y asignándole un objeto, etc, les dejo lo que hice yo, de una forma diferente
Únicamente se le pasa como parámetro el user.sub directamente, se elimina (la comente para dejarla como repaso) validatorHandler, ya que no se enviara nada por el body en este caso
Por alguna razon, "user" me regresa los datos como un array, no entiendo bien porque, es por eso que coloco user[0] para que me de los datos sin array, y con eso ya se envia el customerId automaticamente por medio de la sesion