├── .env.example ├── src ├── index.js ├── utils │ └── auth │ │ ├── index.js │ │ └── strategies │ │ ├── jwt.strategy.js │ │ └── local.strategy.js ├── db │ ├── config.js │ ├── sequelize.js │ ├── seeders │ │ ├── 5-orders.js │ │ ├── 2-customers.js │ │ ├── 6-order-product.js │ │ ├── 3-categories.js │ │ ├── 1-users.js │ │ └── 4-products.js │ ├── models │ │ ├── index.js │ │ ├── category.model.js │ │ ├── user.model.js │ │ ├── product.model.js │ │ ├── customer.model.js │ │ ├── order-product.model.js │ │ └── order.model.js │ └── migrations │ │ └── 20210830181610-init.js ├── middlewares │ ├── validator.handler.js │ ├── auth.handler.js │ └── error.handler.js ├── config │ └── config.js ├── dtos │ ├── category.dto.js │ ├── user.dto.js │ ├── order.dto.js │ ├── customer.dto.js │ └── product.dto.js ├── routes │ ├── index.js │ ├── profile.router.js │ ├── auth.router.js │ ├── orders.router.js │ ├── customers.router.js │ ├── users.router.js │ ├── products.router.js │ └── categories.router.js ├── app.js └── services │ ├── category.service.js │ ├── user.service.js │ ├── customers.service.js │ ├── product.service.js │ ├── order.service.js │ └── auth.service.js ├── .sequelizerc ├── .eslintrc.json ├── .editorconfig ├── README.md ├── docker-compose.yml ├── package.json ├── .gitignore └── insomnia.json /.env.example: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | DATABASE_URL=postgres://:@localhost:5432/my_store 3 | API_KEY=79823 4 | JWT_SECRET= 5 | SMTP_EMAIL=your@email.com 6 | SMTP_PASSWORD=password-email 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const createApp = require('./app'); 2 | 3 | const port = process.env.PORT || 3000; 4 | const app = createApp(); 5 | app.listen(port, () => { 6 | console.log(`Mi port ${port}`); 7 | }); 8 | -------------------------------------------------------------------------------- /.sequelizerc: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'config': './src/db/config.js', 3 | 'models-path': './src/db/models/', 4 | 'migrations-path': './src/db/migrations/', 5 | 'seeders-path': './src/db/seeders/', 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/auth/index.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | 3 | const LocalStrategy = require('./strategies/local.strategy'); 4 | const JwtStrategy = require('./strategies/jwt.strategy'); 5 | 6 | passport.use(LocalStrategy); 7 | passport.use(JwtStrategy); 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2018 4 | }, 5 | "extends": ["eslint:recommended", "prettier"], 6 | "env": { 7 | "es6": true, 8 | "node": true, 9 | "jest": true 10 | }, 11 | "rules": { 12 | "no-console": "warn" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.js] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /src/db/config.js: -------------------------------------------------------------------------------- 1 | const { config } = require('./../config/config'); 2 | 3 | module.exports = { 4 | development: { 5 | url: config.dbUrl, 6 | dialect: 'postgres', 7 | }, 8 | production: { 9 | url: config.dbUrl, 10 | dialect: 'postgres', 11 | dialectOptions: { 12 | ssl: { 13 | rejectUnauthorized: false 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/middlewares/validator.handler.js: -------------------------------------------------------------------------------- 1 | const boom = require('@hapi/boom'); 2 | 3 | function validatorHandler(schema, property) { 4 | return (req, res, next) => { 5 | const data = req[property]; 6 | const { error } = schema.validate(data, { abortEarly: false }); 7 | if (error) { 8 | next(boom.badRequest(error)); 9 | } 10 | next(); 11 | } 12 | } 13 | 14 | module.exports = validatorHandler; 15 | -------------------------------------------------------------------------------- /src/utils/auth/strategies/jwt.strategy.js: -------------------------------------------------------------------------------- 1 | const { Strategy, ExtractJwt } = require('passport-jwt'); 2 | 3 | const { config } = require('../../../config/config'); 4 | 5 | const options = { 6 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 7 | secretOrKey: config.jwtSecret 8 | } 9 | 10 | const JwtStrategy = new Strategy(options, (payload, done) => { 11 | return done(null, payload); 12 | }); 13 | 14 | module.exports = JwtStrategy; 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Install 2 | 3 | ```sh 4 | npm install 5 | ``` 6 | 7 | 8 | # Migrations 9 | 10 | ```sh 11 | npm run migrations:run 12 | ``` 13 | 14 | # Run in dev mode 15 | 16 | ```sh 17 | npm run dev 18 | ``` 19 | 20 | # Run in prod mode 21 | 22 | ```sh 23 | npm run start 24 | ``` 25 | 26 | # Connect to DB from Docker 27 | 28 | ```sh 29 | docker-compose exec postgres bash 30 | psql -h localhost -d my_store -U nico 31 | \d+ 32 | SELECT * FROM users; 33 | DELETE FROM users WHERE id=; 34 | ``` 35 | -------------------------------------------------------------------------------- /src/config/config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const config = { 4 | env: process.env.NODE_ENV || 'development', 5 | isProd: process.env.NODE_ENV === 'production', 6 | isDev: process.env.NODE_ENV === 'development', 7 | port: process.env.PORT || 3000, 8 | dbUrl: process.env.DATABASE_URL, 9 | apiKey: process.env.API_KEY, 10 | jwtSecret: process.env.JWT_SECRET, 11 | smtpEmail: process.env.SMTP_EMAIL, 12 | smtpPassword: process.env.SMTP_PASSWORD, 13 | } 14 | 15 | module.exports = { config }; 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | postgres: 5 | image: postgres:13 6 | environment: 7 | - POSTGRES_DB=my_store 8 | - POSTGRES_USER=nico 9 | - POSTGRES_PASSWORD=admin123 10 | ports: 11 | - 5432:5432 12 | volumes: 13 | - ./postgres_data:/var/lib/postgresql/data 14 | 15 | pgadmin: 16 | image: dpage/pgadmin4 17 | environment: 18 | - PGADMIN_DEFAULT_EMAIL=admin@mail.com 19 | - PGADMIN_DEFAULT_PASSWORD=root 20 | ports: 21 | - 5050:80 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/db/sequelize.js: -------------------------------------------------------------------------------- 1 | const { Sequelize } = require('sequelize'); 2 | 3 | const { config } = require('../config/config'); 4 | const setupModels = require('./models'); 5 | 6 | const options = { 7 | dialect: 'postgres', 8 | logging: config.isDev ? console.log : false, 9 | } 10 | 11 | if (config.isProd) { 12 | options.dialectOptions = { 13 | ssl: { 14 | rejectUnauthorized: false 15 | } 16 | } 17 | } 18 | 19 | const sequelize = new Sequelize(config.dbUrl, options); 20 | 21 | setupModels(sequelize); 22 | 23 | module.exports = sequelize; 24 | -------------------------------------------------------------------------------- /src/dtos/category.dto.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const id = Joi.number().integer(); 4 | const name = Joi.string().min(3).max(15); 5 | const image = Joi.string().uri(); 6 | 7 | const createCategoryDto = Joi.object({ 8 | name: name.required(), 9 | image: image.required() 10 | }); 11 | 12 | const updateCategoryDto = Joi.object({ 13 | name: name, 14 | image: image 15 | }); 16 | 17 | const getCategoryDto = Joi.object({ 18 | id: id.required(), 19 | }); 20 | 21 | module.exports = { createCategoryDto, updateCategoryDto, getCategoryDto } 22 | -------------------------------------------------------------------------------- /src/dtos/user.dto.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const id = Joi.number().integer(); 4 | const email = Joi.string().email(); 5 | const password = Joi.string().min(8); 6 | const role = Joi.string().min(5); 7 | 8 | const createUserDto = Joi.object({ 9 | email: email.required(), 10 | password: password.required() 11 | }); 12 | 13 | const updateUserDto = Joi.object({ 14 | email: email, 15 | role: role, 16 | }); 17 | 18 | const getUserDto = Joi.object({ 19 | id: id.required(), 20 | }); 21 | 22 | module.exports = { createUserDto, updateUserDto, getUserDto } 23 | -------------------------------------------------------------------------------- /src/utils/auth/strategies/local.strategy.js: -------------------------------------------------------------------------------- 1 | const { Strategy } = require('passport-local'); 2 | 3 | const AuthService = require('./../../../services/auth.service'); 4 | const service = new AuthService(); 5 | 6 | const LocalStrategy = new Strategy({ 7 | usernameField: 'email', 8 | passwordField: 'password' 9 | }, 10 | async (email, password, done) => { 11 | try { 12 | const user = await service.getUser(email, password); 13 | done(null, user); 14 | } catch (error) { 15 | done(error, false); 16 | } 17 | } 18 | ); 19 | 20 | module.exports = LocalStrategy; 21 | -------------------------------------------------------------------------------- /src/db/seeders/5-orders.js: -------------------------------------------------------------------------------- 1 | const { ORDER_TABLE } = require('./../models/order.model'); 2 | 3 | module.exports = { 4 | up: async (queryInterface) => { 5 | if (queryInterface.context) { 6 | queryInterface = queryInterface.context; 7 | } 8 | return queryInterface.bulkInsert(ORDER_TABLE, [ 9 | { 10 | customer_id: 1, 11 | created_at: new Date() 12 | }, 13 | ]); 14 | }, 15 | down: (queryInterface) => { 16 | if (queryInterface.context) { 17 | queryInterface = queryInterface.context; 18 | } 19 | return queryInterface.bulkDelete(ORDER_TABLE, null, {}); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/dtos/order.dto.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const id = Joi.number().integer(); 4 | const customerId = Joi.number().integer(); 5 | const orderId = Joi.number().integer(); 6 | const productId = Joi.number().integer(); 7 | const amount = Joi.number().integer().min(1); 8 | 9 | const getOrderDto = Joi.object({ 10 | id: id.required(), 11 | }); 12 | 13 | const createOrderDto = Joi.object({ 14 | customerId: customerId.required(), 15 | }); 16 | 17 | const addItemDto = Joi.object({ 18 | orderId: orderId.required(), 19 | productId: productId.required(), 20 | amount: amount.required(), 21 | }); 22 | 23 | module.exports = { getOrderDto, createOrderDto, addItemDto }; 24 | -------------------------------------------------------------------------------- /src/db/seeders/2-customers.js: -------------------------------------------------------------------------------- 1 | const { CUSTOMER_TABLE } = require('./../models/customer.model'); 2 | 3 | module.exports = { 4 | up: async (queryInterface) => { 5 | if (queryInterface.context) { 6 | queryInterface = queryInterface.context; 7 | } 8 | return queryInterface.bulkInsert(CUSTOMER_TABLE, [ 9 | { 10 | name: 'Juanita', 11 | last_name: 'Perez', 12 | phone: '7830601', 13 | user_id: 2, 14 | created_at: new Date() 15 | }, 16 | ]); 17 | }, 18 | down: (queryInterface) => { 19 | if (queryInterface.context) { 20 | queryInterface = queryInterface.context; 21 | } 22 | return queryInterface.bulkDelete(CUSTOMER_TABLE, null, {}); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/db/seeders/6-order-product.js: -------------------------------------------------------------------------------- 1 | const { ORDER_PRODUCT_TABLE } = require('../models/order-product.model'); 2 | 3 | module.exports = { 4 | up: async (queryInterface) => { 5 | if (queryInterface.context) { 6 | queryInterface = queryInterface.context; 7 | } 8 | return queryInterface.bulkInsert(ORDER_PRODUCT_TABLE, [ 9 | { 10 | amount: 2, 11 | order_id: 1, 12 | product_id: 1, 13 | created_at: new Date() 14 | }, 15 | { 16 | amount: 2, 17 | order_id: 1, 18 | product_id: 2, 19 | created_at: new Date() 20 | }, 21 | ]); 22 | }, 23 | down: (queryInterface) => { 24 | if (queryInterface.context) { 25 | queryInterface = queryInterface.context; 26 | } 27 | return queryInterface.bulkDelete(ORDER_PRODUCT_TABLE, null, {}); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/dtos/customer.dto.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const id = Joi.number().integer(); 4 | const name = Joi.string().min(3).max(30); 5 | const lastName = Joi.string(); 6 | const phone = Joi.string(); 7 | const userId = Joi.number().integer(); 8 | const email = Joi.string().email(); 9 | const password = Joi.string(); 10 | 11 | const getCustomerDto = Joi.object({ 12 | id: id.required(), 13 | }); 14 | 15 | const createCustomerDto = Joi.object({ 16 | name: name.required(), 17 | lastName: lastName.required(), 18 | phone: phone.required(), 19 | user: Joi.object({ 20 | email: email.required(), 21 | password: password.required() 22 | }) 23 | }); 24 | 25 | const updateCustomerDto = Joi.object({ 26 | name, 27 | lastName, 28 | phone, 29 | userId 30 | }); 31 | 32 | module.exports = { getCustomerDto, createCustomerDto, updateCustomerDto }; 33 | -------------------------------------------------------------------------------- /src/db/seeders/3-categories.js: -------------------------------------------------------------------------------- 1 | const { CATEGORY_TABLE } = require('../models/category.model'); 2 | 3 | module.exports = { 4 | up: async (queryInterface) => { 5 | if (queryInterface.context) { 6 | queryInterface = queryInterface.context; 7 | } 8 | return queryInterface.bulkInsert(CATEGORY_TABLE, [ 9 | { 10 | name: 'Category 1', 11 | image: 'https://api.lorem.space/image/game?w=150&h=220', 12 | created_at: new Date() 13 | }, 14 | { 15 | name: 'Category 2', 16 | image: 'https://api.lorem.space/image/game?w=150&h=220', 17 | created_at: new Date() 18 | } 19 | ]); 20 | }, 21 | down: (queryInterface) => { 22 | if (queryInterface.context) { 23 | queryInterface = queryInterface.context; 24 | } 25 | return queryInterface.bulkDelete(CATEGORY_TABLE, null, {}); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const productsRouter = require('./products.router'); 4 | const categoriesRouter = require('./categories.router'); 5 | const usersRouter = require('./users.router'); 6 | const orderRouter = require('./orders.router'); 7 | const customersRouter = require('./customers.router'); 8 | const authRouter = require('./auth.router'); 9 | const profileRouter = require('./profile.router'); 10 | 11 | function routerApi(app) { 12 | const router = express.Router(); 13 | app.use('/api/v1', router); 14 | router.use('/products', productsRouter); 15 | router.use('/categories', categoriesRouter); 16 | router.use('/users', usersRouter); 17 | router.use('/orders', orderRouter); 18 | router.use('/customers', customersRouter); 19 | router.use('/auth', authRouter); 20 | router.use('/profile', profileRouter); 21 | } 22 | 23 | module.exports = routerApi; 24 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | 2 | const express = require('express'); 3 | const cors = require('cors'); 4 | 5 | const routerApi = require('./routes'); 6 | const { checkApiKey } = require('./middlewares/auth.handler'); 7 | 8 | const { logErrors, errorHandler, boomErrorHandler, ormErrorHandler } = require('./middlewares/error.handler'); 9 | 10 | const createApp = () => { 11 | const app = express(); 12 | 13 | app.use(express.json()); 14 | app.use(cors()); 15 | 16 | require('./utils/auth'); 17 | 18 | app.get('/', (req, res) => { 19 | res.send('Hola mi server en express'); 20 | }); 21 | 22 | app.get('/nueva-ruta', checkApiKey, (req, res) => { 23 | res.send('Hola, soy una nueva ruta'); 24 | }); 25 | 26 | routerApi(app); 27 | 28 | app.use(logErrors); 29 | app.use(ormErrorHandler); 30 | app.use(boomErrorHandler); 31 | app.use(errorHandler); 32 | return app; 33 | } 34 | 35 | module.exports = createApp; 36 | -------------------------------------------------------------------------------- /src/middlewares/auth.handler.js: -------------------------------------------------------------------------------- 1 | const boom = require('@hapi/boom'); 2 | 3 | const { config } = require('./../config/config'); 4 | 5 | function checkApiKey(req, res, next) { 6 | const apiKey = req.headers['api']; 7 | if (apiKey === config.apiKey) { 8 | next(); 9 | } else { 10 | next(boom.unauthorized()); 11 | } 12 | } 13 | 14 | function checkAdminRole(req, res, next) { 15 | const user = req.user; 16 | if (user.role === 'admin') { 17 | next(); 18 | } else { 19 | next(boom.unauthorized('your role is not admin')); 20 | } 21 | } 22 | 23 | 24 | function checkRoles(...roles) { 25 | return (req, res, next) => { 26 | const user = req.user; 27 | if (roles.includes(user.role)) { 28 | next(); 29 | } else { 30 | next(boom.unauthorized('your role is not allow')); 31 | } 32 | } 33 | } 34 | 35 | 36 | 37 | module.exports = { checkApiKey, checkAdminRole, checkRoles } 38 | -------------------------------------------------------------------------------- /src/db/seeders/1-users.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | const { USER_TABLE } = require('./../models/user.model'); 3 | 4 | module.exports = { 5 | up: async (queryInterface) => { 6 | if (queryInterface.context) { 7 | queryInterface = queryInterface.context; 8 | } 9 | return queryInterface.bulkInsert(USER_TABLE, [ 10 | { 11 | email: 'admin@mail.com', 12 | password: await bcrypt.hash('admin123', 10), 13 | role: 'admin', 14 | created_at: new Date() 15 | }, 16 | { 17 | email: 'customer@mail.com', 18 | password: await bcrypt.hash('customer123', 10), 19 | role: 'customer', 20 | created_at: new Date() 21 | } 22 | ]); 23 | }, 24 | down: (queryInterface) => { 25 | if (queryInterface.context) { 26 | queryInterface = queryInterface.context; 27 | } 28 | return queryInterface.bulkDelete(USER_TABLE, null, {}); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/middlewares/error.handler.js: -------------------------------------------------------------------------------- 1 | const { ValidationError } = require('sequelize'); 2 | const { config } = require('./../config/config'); 3 | 4 | function logErrors (err, req, res, next) { 5 | if (config.isDev) { 6 | console.error(err); 7 | } 8 | next(err); 9 | } 10 | 11 | function errorHandler(err, req, res, next) { 12 | res.status(500).json({ 13 | message: err.message, 14 | stack: err.stack, 15 | }); 16 | } 17 | 18 | function boomErrorHandler(err, req, res, next) { 19 | if (err.isBoom) { 20 | const { output } = err; 21 | res.status(output.statusCode).json(output.payload); 22 | } else { 23 | next(err); 24 | } 25 | } 26 | 27 | function ormErrorHandler(err, req, res, next) { 28 | if (err instanceof ValidationError) { 29 | res.status(409).json({ 30 | statusCode: 409, 31 | message: err.name, 32 | errors: err.errors 33 | }); 34 | } 35 | next(err); 36 | } 37 | 38 | 39 | module.exports = { logErrors, errorHandler, boomErrorHandler, ormErrorHandler } 40 | -------------------------------------------------------------------------------- /src/services/category.service.js: -------------------------------------------------------------------------------- 1 | const boom = require('@hapi/boom'); 2 | const { models }= require('../db/sequelize'); 3 | 4 | class CategoryService { 5 | 6 | constructor(){ 7 | } 8 | async create(data) { 9 | const newCategory = await models.Category.create(data); 10 | return newCategory; 11 | } 12 | 13 | async find() { 14 | const categories = await models.Category.findAll(); 15 | return categories; 16 | } 17 | 18 | async findOne(id) { 19 | const category = await models.Category.findByPk(id, { 20 | include: ['products'] 21 | }); 22 | if (!category) { 23 | throw boom.notFound('category not found'); 24 | } 25 | return category; 26 | } 27 | 28 | async update(id, changes) { 29 | const category = await this.findOne(id); 30 | const rta = await category.update(changes); 31 | return rta; 32 | } 33 | 34 | async delete(id) { 35 | const category = await this.findOne(id); 36 | await category.destroy(); 37 | return { id }; 38 | } 39 | 40 | } 41 | 42 | module.exports = CategoryService; 43 | -------------------------------------------------------------------------------- /src/routes/profile.router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const passport = require('passport'); 3 | 4 | const OrderService = require('../services/order.service'); 5 | const UserService = require('../services/user.service'); 6 | 7 | const router = express.Router(); 8 | const orderService = new OrderService(); 9 | const userService = new UserService(); 10 | 11 | router.get('/my-orders', 12 | passport.authenticate('jwt', {session: false}), 13 | async (req, res, next) => { 14 | try { 15 | const user = req.user; 16 | const orders = await orderService.findByUser(user.sub); 17 | res.json(orders); 18 | } catch (error) { 19 | next(error); 20 | } 21 | } 22 | ); 23 | 24 | router.get('/my-user', 25 | passport.authenticate('jwt', {session: false}), 26 | async (req, res, next) => { 27 | try { 28 | const userPayload = req.user; 29 | const user = await userService.findOne(userPayload.sub); 30 | res.json(user); 31 | } catch (error) { 32 | next(error); 33 | } 34 | } 35 | ); 36 | 37 | module.exports = router; 38 | -------------------------------------------------------------------------------- /src/db/models/index.js: -------------------------------------------------------------------------------- 1 | const { User, UserSchema } = require('./user.model'); 2 | const { Customer, CustomerSchema } = require('./customer.model'); 3 | const { Category, CategorySchema } = require('./category.model'); 4 | const { Product, ProductSchema } = require('./product.model'); 5 | const { Order, OrderSchema } = require('./order.model'); 6 | const { OrderProduct, OrderProductSchema } = require('./order-product.model'); 7 | 8 | function setupModels(sequelize) { 9 | User.init(UserSchema, User.config(sequelize)); 10 | Customer.init(CustomerSchema, Customer.config(sequelize)); 11 | Category.init(CategorySchema, Category.config(sequelize)); 12 | Product.init(ProductSchema, Product.config(sequelize)); 13 | Order.init(OrderSchema, Order.config(sequelize)); 14 | OrderProduct.init(OrderProductSchema, OrderProduct.config(sequelize)); 15 | 16 | User.associate(sequelize.models); 17 | Customer.associate(sequelize.models); 18 | Category.associate(sequelize.models); 19 | Product.associate(sequelize.models); 20 | Order.associate(sequelize.models); 21 | } 22 | 23 | module.exports = setupModels; 24 | -------------------------------------------------------------------------------- /src/db/models/category.model.js: -------------------------------------------------------------------------------- 1 | const { Model, DataTypes, Sequelize } = require('sequelize'); 2 | 3 | const CATEGORY_TABLE = 'categories'; 4 | 5 | const CategorySchema = { 6 | id: { 7 | allowNull: false, 8 | autoIncrement: true, 9 | primaryKey: true, 10 | type: DataTypes.INTEGER 11 | }, 12 | name: { 13 | type: DataTypes.STRING, 14 | unique: true, 15 | allowNull: false, 16 | }, 17 | image: { 18 | type: DataTypes.STRING, 19 | allowNull: false, 20 | }, 21 | createdAt: { 22 | allowNull: false, 23 | type: DataTypes.DATE, 24 | field: 'created_at', 25 | defaultValue: Sequelize.NOW, 26 | }, 27 | } 28 | 29 | 30 | class Category extends Model { 31 | 32 | static associate(models) { 33 | this.hasMany(models.Product, { 34 | as: 'products', 35 | foreignKey: 'categoryId' 36 | }); 37 | } 38 | 39 | static config(sequelize) { 40 | return { 41 | sequelize, 42 | tableName: CATEGORY_TABLE, 43 | modelName: 'Category', 44 | timestamps: false 45 | } 46 | } 47 | } 48 | 49 | module.exports = { Category, CategorySchema, CATEGORY_TABLE }; 50 | -------------------------------------------------------------------------------- /src/routes/auth.router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const passport = require('passport'); 3 | 4 | const AuthService = require('./../services/auth.service'); 5 | 6 | const router = express.Router(); 7 | const service = new AuthService(); 8 | 9 | router.post('/login', 10 | passport.authenticate('local', {session: false}), 11 | async (req, res, next) => { 12 | try { 13 | const user = req.user; 14 | res.json(service.signToken(user)); 15 | } catch (error) { 16 | next(error); 17 | } 18 | } 19 | ); 20 | 21 | router.post('/recovery', 22 | async (req, res, next) => { 23 | try { 24 | const { email } = req.body; 25 | const rta = await service.sendRecovery(email); 26 | res.json(rta); 27 | } catch (error) { 28 | next(error); 29 | } 30 | } 31 | ); 32 | 33 | router.post('/change-password', 34 | async (req, res, next) => { 35 | try { 36 | const { token, newPassword } = req.body; 37 | const rta = await service.changePassword(token, newPassword); 38 | res.json(rta); 39 | } catch (error) { 40 | next(error); 41 | } 42 | } 43 | ); 44 | 45 | module.exports = router; 46 | -------------------------------------------------------------------------------- /src/dtos/product.dto.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const id = Joi.number().integer(); 4 | const name = Joi.string().min(3).max(15); 5 | const price = Joi.number().integer().min(10); 6 | const description = Joi.string().min(10); 7 | const image = Joi.string().uri(); 8 | const categoryId = Joi.number().integer(); 9 | 10 | const price_min = Joi.number().integer(); 11 | const price_max = Joi.number().integer(); 12 | 13 | const limit = Joi.number().integer(); 14 | const offset = Joi.number().integer(); 15 | 16 | const createProductDto = Joi.object({ 17 | name: name.required(), 18 | price: price.required(), 19 | description: description.required(), 20 | image: image.required(), 21 | categoryId: categoryId.required(), 22 | }); 23 | 24 | const updateProductDto = Joi.object({ 25 | name: name, 26 | price: price, 27 | image: image, 28 | description: description, 29 | categoryId 30 | }); 31 | 32 | const getProductDto = Joi.object({ 33 | id: id.required(), 34 | }); 35 | 36 | const queryProductDto = Joi.object({ 37 | limit, 38 | offset, 39 | price, 40 | price_min, 41 | price_max: Joi.when('price_min', { 42 | is: Joi.exist(), 43 | then: price_max.required(), 44 | }) 45 | }); 46 | 47 | module.exports = { createProductDto, updateProductDto, getProductDto, queryProductDto } 48 | -------------------------------------------------------------------------------- /src/db/models/user.model.js: -------------------------------------------------------------------------------- 1 | const { Model, DataTypes, Sequelize } = require('sequelize'); 2 | 3 | const USER_TABLE = 'users'; 4 | 5 | const UserSchema = { 6 | id: { 7 | allowNull: false, 8 | autoIncrement: true, 9 | primaryKey: true, 10 | type: DataTypes.INTEGER 11 | }, 12 | email: { 13 | allowNull: false, 14 | type: DataTypes.STRING, 15 | unique: true, 16 | }, 17 | password: { 18 | allowNull: false, 19 | type: DataTypes.STRING 20 | }, 21 | recoveryToken: { 22 | field: 'recovery_token', 23 | allowNull: true, 24 | type: DataTypes.STRING 25 | }, 26 | role: { 27 | allowNull: false, 28 | type: DataTypes.STRING, 29 | defaultValue: 'customer' 30 | }, 31 | createdAt: { 32 | allowNull: false, 33 | type: DataTypes.DATE, 34 | field: 'created_at', 35 | defaultValue: Sequelize.NOW 36 | } 37 | } 38 | 39 | class User extends Model { 40 | static associate(models) { 41 | this.hasOne(models.Customer, { 42 | as: 'customer', 43 | foreignKey: 'userId' 44 | }); 45 | } 46 | 47 | static config(sequelize) { 48 | return { 49 | sequelize, 50 | tableName: USER_TABLE, 51 | modelName: 'User', 52 | timestamps: false 53 | } 54 | } 55 | } 56 | 57 | 58 | module.exports = { USER_TABLE, UserSchema, User } 59 | -------------------------------------------------------------------------------- /src/services/user.service.js: -------------------------------------------------------------------------------- 1 | const boom = require('@hapi/boom'); 2 | const bcrypt = require('bcrypt'); 3 | 4 | const { models } = require('../db/sequelize'); 5 | 6 | class UserService { 7 | constructor() {} 8 | 9 | async create(data) { 10 | const hash = await bcrypt.hash(data.password, 10); 11 | const newUser = await models.User.create({ 12 | ...data, 13 | password: hash, 14 | role: "admin", 15 | }); 16 | delete newUser.dataValues.password; 17 | return newUser; 18 | } 19 | 20 | async find() { 21 | const rta = await models.User.findAll({ 22 | include: ['customer'] 23 | }); 24 | return rta; 25 | } 26 | 27 | async findByEmail(email) { 28 | const rta = await models.User.findOne({ 29 | where: { email } 30 | }); 31 | return rta; 32 | } 33 | 34 | async findOne(id) { 35 | const user = await models.User.findByPk(id); 36 | if (!user) { 37 | throw boom.notFound('user not found'); 38 | } 39 | return user; 40 | } 41 | 42 | async update(id, changes) { 43 | const user = await this.findOne(id); 44 | const rta = await user.update(changes); 45 | return rta; 46 | } 47 | 48 | async delete(id) { 49 | const user = await this.findOne(id); 50 | await user.destroy(); 51 | return { id }; 52 | } 53 | } 54 | 55 | module.exports = UserService; 56 | -------------------------------------------------------------------------------- /src/services/customers.service.js: -------------------------------------------------------------------------------- 1 | const boom = require('@hapi/boom'); 2 | const bcrypt = require('bcrypt'); 3 | const { models } = require('../db/sequelize'); 4 | 5 | class CustomerService { 6 | 7 | constructor() {} 8 | 9 | async find() { 10 | const rta = await models.Customer.findAll({ 11 | include: ['user'] 12 | }); 13 | return rta; 14 | } 15 | 16 | async findOne(id) { 17 | const user = await models.Customer.findByPk(id); 18 | if (!user) { 19 | throw boom.notFound('customer not found'); 20 | } 21 | return user; 22 | } 23 | 24 | async create(data) { 25 | const hash = await bcrypt.hash(data.user.password, 10); 26 | const newData = { 27 | ...data, 28 | user: { 29 | ...data.user, 30 | password: hash 31 | } 32 | } 33 | const newCustomer = await models.Customer.create(newData, { 34 | include: ['user'] 35 | }); 36 | delete newCustomer.dataValues.user.dataValues.password; 37 | return newCustomer; 38 | } 39 | 40 | async update(id, changes) { 41 | const model = await this.findOne(id); 42 | const rta = await model.update(changes); 43 | return rta; 44 | } 45 | 46 | async delete(id) { 47 | const model = await this.findOne(id); 48 | await model.destroy(); 49 | return { rta: true }; 50 | } 51 | 52 | } 53 | 54 | module.exports = CustomerService; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-store", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "NODE_ENV=development nodemon src/index.js", 8 | "start": "node src/index.js", 9 | "lint": "eslint", 10 | "seed:all": "sequelize-cli db:seed:all", 11 | "seed:undo": "sequelize-cli db:seed:undo:all", 12 | "migrations:generate": "sequelize-cli migration:generate --name", 13 | "migrations:run": "sequelize-cli db:migrate", 14 | "migrations:revert": "sequelize-cli db:migrate:undo", 15 | "migrations:delete": "sequelize-cli db:migrate:undo:all" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "devDependencies": { 21 | "eslint": "8.21.0", 22 | "eslint-config-prettier": "8.5.0", 23 | "eslint-plugin-prettier": "4.2.1", 24 | "nodemon": "2.0.19", 25 | "prettier": "2.7.1" 26 | }, 27 | "dependencies": { 28 | "@hapi/boom": "10.0.0", 29 | "bcrypt": "5.0.1", 30 | "cors": "2.8.5", 31 | "dotenv": "16.0.1", 32 | "express": "4.18.1", 33 | "@faker-js/faker": "7.4.0", 34 | "joi": "17.6.0", 35 | "jsonwebtoken": "8.5.1", 36 | "nodemailer": "6.7.7", 37 | "passport": "0.6.0", 38 | "passport-jwt": "4.0.0", 39 | "passport-local": "1.0.0", 40 | "pg": "8.7.3", 41 | "sequelize": "6.21.3", 42 | "sequelize-cli": "6.4.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/routes/orders.router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const OrderService = require('../services/order.service'); 4 | const validatorHandler = require('../middlewares/validator.handler'); 5 | const { 6 | getOrderDto, 7 | createOrderDto, 8 | addItemDto, 9 | } = require('../dtos/order.dto'); 10 | 11 | const router = express.Router(); 12 | const service = new OrderService(); 13 | 14 | router.get( 15 | '/:id', 16 | validatorHandler(getOrderDto, 'params'), 17 | async (req, res, next) => { 18 | try { 19 | const { id } = req.params; 20 | const order = await service.findOne(id); 21 | res.json(order); 22 | } catch (error) { 23 | next(error); 24 | } 25 | } 26 | ); 27 | 28 | router.post( 29 | '/', 30 | validatorHandler(createOrderDto, 'body'), 31 | async (req, res, next) => { 32 | try { 33 | const body = req.body; 34 | const newOrder = await service.create(body); 35 | res.status(201).json(newOrder); 36 | } catch (error) { 37 | next(error); 38 | } 39 | } 40 | ); 41 | 42 | router.post( 43 | '/add-item', 44 | validatorHandler(addItemDto, 'body'), 45 | async (req, res, next) => { 46 | try { 47 | const body = req.body; 48 | const newItem = await service.addItem(body); 49 | res.status(201).json(newItem); 50 | } catch (error) { 51 | next(error); 52 | } 53 | } 54 | ); 55 | 56 | module.exports = router; 57 | -------------------------------------------------------------------------------- /src/db/models/product.model.js: -------------------------------------------------------------------------------- 1 | const { Model, DataTypes, Sequelize } = require('sequelize'); 2 | 3 | const { CATEGORY_TABLE } = require('./category.model'); 4 | 5 | const PRODUCT_TABLE = 'products'; 6 | 7 | const ProductSchema = { 8 | id: { 9 | allowNull: false, 10 | autoIncrement: true, 11 | primaryKey: true, 12 | type: DataTypes.INTEGER 13 | }, 14 | name: { 15 | type: DataTypes.STRING, 16 | allowNull: false, 17 | }, 18 | image: { 19 | type: DataTypes.STRING, 20 | allowNull: false, 21 | }, 22 | description: { 23 | type: DataTypes.TEXT, 24 | allowNull: false, 25 | }, 26 | price: { 27 | type: DataTypes.INTEGER, 28 | allowNull: false, 29 | }, 30 | createdAt: { 31 | allowNull: false, 32 | type: DataTypes.DATE, 33 | field: 'created_at', 34 | defaultValue: Sequelize.NOW, 35 | }, 36 | categoryId: { 37 | field: 'category_id', 38 | allowNull: false, 39 | type: DataTypes.INTEGER, 40 | references: { 41 | model: CATEGORY_TABLE, 42 | key: 'id' 43 | }, 44 | onUpdate: 'CASCADE', 45 | onDelete: 'SET NULL' 46 | } 47 | } 48 | 49 | 50 | class Product extends Model { 51 | 52 | static associate(models) { 53 | this.belongsTo(models.Category, { as: 'category' }); 54 | } 55 | 56 | static config(sequelize) { 57 | return { 58 | sequelize, 59 | tableName: PRODUCT_TABLE, 60 | modelName: 'Product', 61 | timestamps: false 62 | } 63 | } 64 | } 65 | 66 | module.exports = { Product, ProductSchema, PRODUCT_TABLE }; 67 | -------------------------------------------------------------------------------- /src/db/seeders/4-products.js: -------------------------------------------------------------------------------- 1 | const { PRODUCT_TABLE } = require('../models/product.model'); 2 | 3 | module.exports = { 4 | up: async (queryInterface) => { 5 | if (queryInterface.context) { 6 | queryInterface = queryInterface.context; 7 | } 8 | return queryInterface.bulkInsert(PRODUCT_TABLE, [ 9 | { 10 | name: 'Product 1', 11 | image: 'https://api.lorem.space/image/game?w=150&h=220', 12 | description: 'bla bla bla', 13 | price: 100, 14 | category_id: 1, 15 | created_at: new Date() 16 | }, 17 | { 18 | name: 'Product 2', 19 | image: 'https://api.lorem.space/image/game?w=150&h=220', 20 | description: 'bla bla bla', 21 | price: 200, 22 | category_id: 1, 23 | created_at: new Date() 24 | }, 25 | { 26 | name: 'Product 3', 27 | image: 'https://api.lorem.space/image/game?w=150&h=220', 28 | description: 'bla bla bla', 29 | price: 300, 30 | category_id: 2, 31 | created_at: new Date() 32 | }, 33 | { 34 | name: 'Product 4', 35 | image: 'https://api.lorem.space/image/game?w=150&h=220', 36 | description: 'bla bla bla', 37 | price: 400, 38 | category_id: 2, 39 | created_at: new Date() 40 | } 41 | ]); 42 | }, 43 | down: (queryInterface) => { 44 | if (queryInterface.context) { 45 | queryInterface = queryInterface.context; 46 | } 47 | return queryInterface.bulkDelete(PRODUCT_TABLE, null, {}); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /src/db/models/customer.model.js: -------------------------------------------------------------------------------- 1 | const { Model, DataTypes, Sequelize } = require('sequelize'); 2 | 3 | const { USER_TABLE } = require('./user.model') 4 | 5 | const CUSTOMER_TABLE = 'customers'; 6 | 7 | const CustomerSchema = { 8 | id: { 9 | allowNull: false, 10 | autoIncrement: true, 11 | primaryKey: true, 12 | type: DataTypes.INTEGER 13 | }, 14 | name: { 15 | allowNull: false, 16 | type: DataTypes.STRING, 17 | }, 18 | lastName: { 19 | allowNull: false, 20 | type: DataTypes.STRING, 21 | field: 'last_name', 22 | }, 23 | phone: { 24 | allowNull: true, 25 | type: DataTypes.STRING, 26 | }, 27 | createdAt: { 28 | allowNull: false, 29 | type: DataTypes.DATE, 30 | field: 'created_at', 31 | defaultValue: Sequelize.NOW, 32 | }, 33 | userId: { 34 | field: 'user_id', 35 | allowNull: false, 36 | type: DataTypes.INTEGER, 37 | unique: true, 38 | references: { 39 | model: USER_TABLE, 40 | key: 'id' 41 | }, 42 | onUpdate: 'CASCADE', 43 | onDelete: 'SET NULL' 44 | } 45 | } 46 | 47 | class Customer extends Model { 48 | 49 | static associate(models) { 50 | this.belongsTo(models.User, {as: 'user'}); 51 | this.hasMany(models.Order, { 52 | as: 'orders', 53 | foreignKey: 'customerId' 54 | }); 55 | } 56 | 57 | static config(sequelize) { 58 | return { 59 | sequelize, 60 | tableName: CUSTOMER_TABLE, 61 | modelName: 'Customer', 62 | timestamps: false 63 | } 64 | } 65 | } 66 | 67 | module.exports = { Customer, CustomerSchema, CUSTOMER_TABLE }; 68 | -------------------------------------------------------------------------------- /src/db/models/order-product.model.js: -------------------------------------------------------------------------------- 1 | const { Model, DataTypes, Sequelize } = require('sequelize'); 2 | 3 | const { ORDER_TABLE } = require('./order.model'); 4 | const { PRODUCT_TABLE } = require('./product.model'); 5 | 6 | const ORDER_PRODUCT_TABLE = 'orders_products'; 7 | 8 | const OrderProductSchema = { 9 | id: { 10 | allowNull: false, 11 | autoIncrement: true, 12 | primaryKey: true, 13 | type: DataTypes.INTEGER 14 | }, 15 | createdAt: { 16 | allowNull: false, 17 | type: DataTypes.DATE, 18 | field: 'created_at', 19 | defaultValue: Sequelize.NOW, 20 | }, 21 | amount: { 22 | allowNull: false, 23 | type: DataTypes.INTEGER 24 | }, 25 | orderId: { 26 | field: 'order_id', 27 | allowNull: false, 28 | type: DataTypes.INTEGER, 29 | references: { 30 | model: ORDER_TABLE, 31 | key: 'id' 32 | }, 33 | onUpdate: 'CASCADE', 34 | onDelete: 'SET NULL' 35 | }, 36 | productId: { 37 | field: 'product_id', 38 | allowNull: false, 39 | type: DataTypes.INTEGER, 40 | references: { 41 | model: PRODUCT_TABLE, 42 | key: 'id' 43 | }, 44 | onUpdate: 'CASCADE', 45 | onDelete: 'SET NULL' 46 | } 47 | } 48 | 49 | class OrderProduct extends Model { 50 | 51 | static associate(models) { 52 | // 53 | } 54 | 55 | static config(sequelize) { 56 | return { 57 | sequelize, 58 | tableName: ORDER_PRODUCT_TABLE, 59 | modelName: 'OrderProduct', 60 | timestamps: false 61 | } 62 | } 63 | } 64 | 65 | module.exports = { OrderProduct, OrderProductSchema, ORDER_PRODUCT_TABLE }; 66 | -------------------------------------------------------------------------------- /src/routes/customers.router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const CustomerService = require('../services/customers.service'); 4 | const validationHandler = require('../middlewares/validator.handler'); 5 | const { 6 | createCustomerDto, 7 | getCustomerDto, 8 | updateCustomerDto, 9 | } = require('../dtos/customer.dto'); 10 | 11 | const router = express.Router(); 12 | const service = new CustomerService(); 13 | 14 | router.get('/', async (req, res, next) => { 15 | try { 16 | res.json(await service.find()); 17 | } catch (error) { 18 | next(error); 19 | } 20 | }); 21 | 22 | router.post('/', 23 | validationHandler(createCustomerDto, 'body'), 24 | async (req, res, next) => { 25 | try { 26 | const body = req.body; 27 | res.status(201).json(await service.create(body)); 28 | } catch (error) { 29 | next(error); 30 | } 31 | } 32 | ); 33 | 34 | router.patch('/:id', 35 | validationHandler(getCustomerDto, 'params'), 36 | validationHandler(updateCustomerDto, 'body'), 37 | async (req, res, next) => { 38 | try { 39 | const { id } = req.params; 40 | const body = req.body; 41 | res.status(201).json(await service.update(id, body)); 42 | } catch (error) { 43 | next(error); 44 | } 45 | } 46 | ); 47 | 48 | router.delete('/:id', 49 | validationHandler(getCustomerDto, 'params'), 50 | async (req, res, next) => { 51 | try { 52 | const { id } = req.params; 53 | res.status(200).json(await service.delete(id)); 54 | } catch (error) { 55 | next(error); 56 | } 57 | } 58 | ); 59 | 60 | module.exports = router; 61 | -------------------------------------------------------------------------------- /src/db/models/order.model.js: -------------------------------------------------------------------------------- 1 | const { Model, DataTypes, Sequelize } = require('sequelize'); 2 | const { CUSTOMER_TABLE } = require('./customer.model'); 3 | 4 | const ORDER_TABLE = 'orders'; 5 | 6 | const OrderSchema = { 7 | id: { 8 | allowNull: false, 9 | autoIncrement: true, 10 | primaryKey: true, 11 | type: DataTypes.INTEGER 12 | }, 13 | customerId: { 14 | field: 'customer_id', 15 | allowNull: false, 16 | type: DataTypes.INTEGER, 17 | references: { 18 | model: CUSTOMER_TABLE, 19 | key: 'id' 20 | }, 21 | onUpdate: 'CASCADE', 22 | onDelete: 'SET NULL' 23 | }, 24 | createdAt: { 25 | allowNull: false, 26 | type: DataTypes.DATE, 27 | field: 'created_at', 28 | defaultValue: Sequelize.NOW, 29 | }, 30 | total: { 31 | type: DataTypes.VIRTUAL, 32 | get() { 33 | if (this.items && this.items.length > 0) { 34 | return this.items.reduce((total, item) => { 35 | return total + (item.price * item.OrderProduct.amount); 36 | }, 0); 37 | } 38 | return 0; 39 | } 40 | } 41 | } 42 | 43 | 44 | class Order extends Model { 45 | 46 | static associate(models) { 47 | this.belongsTo(models.Customer, { 48 | as: 'customer', 49 | }); 50 | this.belongsToMany(models.Product, { 51 | as: 'items', 52 | through: models.OrderProduct, 53 | foreignKey: 'orderId', 54 | otherKey: 'productId' 55 | }); 56 | } 57 | 58 | static config(sequelize) { 59 | return { 60 | sequelize, 61 | tableName: ORDER_TABLE, 62 | modelName: 'Order', 63 | timestamps: false 64 | } 65 | } 66 | } 67 | 68 | module.exports = { Order, OrderSchema, ORDER_TABLE }; 69 | -------------------------------------------------------------------------------- /src/services/product.service.js: -------------------------------------------------------------------------------- 1 | const { Op } = require('sequelize'); 2 | const boom = require('@hapi/boom'); 3 | 4 | const { models } = require('../db/sequelize'); 5 | 6 | class ProductsService { 7 | 8 | async create(data) { 9 | const category = await models.Category.findByPk(data.categoryId); 10 | if (!category) { 11 | throw boom.notFound('category not found'); 12 | } 13 | const newProduct = await models.Product.create(data); 14 | return newProduct; 15 | } 16 | 17 | async find(query) { 18 | const options = { 19 | include: ['category'], 20 | where: {} 21 | } 22 | const { limit, offset } = query; 23 | if (limit && offset) { 24 | options.limit = limit; 25 | options.offset = offset; 26 | } 27 | 28 | const { price } = query; 29 | if (price) { 30 | options.where.price = price; 31 | } 32 | 33 | const { price_min, price_max } = query; 34 | if (price_min && price_max) { 35 | options.where.price = { 36 | [Op.gte]: price_min, 37 | [Op.lte]: price_max, 38 | }; 39 | } 40 | const products = await models.Product.findAll(options); 41 | return products; 42 | } 43 | 44 | async findOne(id) { 45 | const product = await models.Product.findByPk(id, { 46 | include: ['category'] 47 | }); 48 | if (!product) { 49 | throw boom.notFound('product not found'); 50 | } 51 | return product; 52 | } 53 | 54 | async update(id, changes) { 55 | const product = await this.findOne(id); 56 | const rta = await product.update(changes); 57 | return rta; 58 | } 59 | 60 | async delete(id) { 61 | const product = await this.findOne(id); 62 | await product.destroy(); 63 | return { id }; 64 | } 65 | 66 | } 67 | 68 | module.exports = ProductsService; 69 | -------------------------------------------------------------------------------- /src/routes/users.router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const UserService = require('./../services/user.service'); 4 | const validatorHandler = require('./../middlewares/validator.handler'); 5 | const { updateUserDto, createUserDto, getUserDto } = require('../dtos/user.dto'); 6 | 7 | const router = express.Router(); 8 | const service = new UserService(); 9 | 10 | router.get('/', async (req, res, next) => { 11 | try { 12 | const users = await service.find(); 13 | res.json(users); 14 | } catch (error) { 15 | next(error); 16 | } 17 | }); 18 | 19 | router.get('/:id', 20 | validatorHandler(getUserDto, 'params'), 21 | async (req, res, next) => { 22 | try { 23 | const { id } = req.params; 24 | const user = await service.findOne(id); 25 | res.json(user); 26 | } catch (error) { 27 | next(error); 28 | } 29 | } 30 | ); 31 | 32 | router.post('/', 33 | validatorHandler(createUserDto, 'body'), 34 | async (req, res, next) => { 35 | try { 36 | const body = req.body; 37 | const newUser = await service.create(body); 38 | res.status(201).json(newUser); 39 | } catch (error) { 40 | next(error); 41 | } 42 | } 43 | ); 44 | 45 | router.patch('/:id', 46 | validatorHandler(getUserDto, 'params'), 47 | validatorHandler(updateUserDto, 'body'), 48 | async (req, res, next) => { 49 | try { 50 | const { id } = req.params; 51 | const body = req.body; 52 | const user = await service.update(id, body); 53 | res.json(user); 54 | } catch (error) { 55 | next(error); 56 | } 57 | } 58 | ); 59 | 60 | router.delete('/:id', 61 | validatorHandler(getUserDto, 'params'), 62 | async (req, res, next) => { 63 | try { 64 | const { id } = req.params; 65 | await service.delete(id); 66 | res.status(201).json({id}); 67 | } catch (error) { 68 | next(error); 69 | } 70 | } 71 | ); 72 | 73 | module.exports = router; 74 | 75 | -------------------------------------------------------------------------------- /src/services/order.service.js: -------------------------------------------------------------------------------- 1 | const boom = require('@hapi/boom'); 2 | const { models } = require('../db/sequelize'); 3 | 4 | class OrderService { 5 | 6 | constructor(){ 7 | } 8 | 9 | async create(data) { 10 | const customer = await models.Customer.findByPk(data.customerId); 11 | if (!customer) { 12 | throw boom.notFound('customer not found'); 13 | } 14 | const newOrder = await models.Order.create(data); 15 | return newOrder; 16 | } 17 | 18 | async addItem(data) { 19 | const product = await models.Product.findByPk(data.productId); 20 | if (!product) { 21 | throw boom.notFound('product not found'); 22 | } 23 | const order = await models.Order.findByPk(data.orderId); 24 | if (!order) { 25 | throw boom.notFound('order not found'); 26 | } 27 | const newItem = await models.OrderProduct.create(data); 28 | return newItem; 29 | } 30 | 31 | async findByUser(userId) { 32 | const orders = await models.Order.findAll({ 33 | where: { 34 | '$customer.user.id$': userId 35 | }, 36 | include: [ 37 | { 38 | association: 'customer', 39 | include: ['user'] 40 | } 41 | ] 42 | }); 43 | return orders; 44 | } 45 | 46 | async findOne(id) { 47 | const order = await models.Order.findByPk(id, { 48 | include: [ 49 | { 50 | association: 'customer', 51 | include: ['user'] 52 | }, 53 | 'items' 54 | ] 55 | }); 56 | if (!order) { 57 | throw boom.notFound('order not found'); 58 | } 59 | return order; 60 | } 61 | 62 | async update(id, changes) { 63 | const order = await this.findOne(id); 64 | const rta = await order.update(changes); 65 | return rta; 66 | } 67 | 68 | async delete(id) { 69 | const order = await this.findOne(id); 70 | await order.destroy(); 71 | return { id }; 72 | } 73 | 74 | } 75 | 76 | module.exports = OrderService; 77 | -------------------------------------------------------------------------------- /src/routes/products.router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const ProductsService = require('./../services/product.service'); 4 | const validatorHandler = require('./../middlewares/validator.handler'); 5 | const { createProductDto, updateProductDto, getProductDto, queryProductDto } = require('../dtos/product.dto'); 6 | 7 | const router = express.Router(); 8 | const service = new ProductsService(); 9 | 10 | router.get('/', 11 | validatorHandler(queryProductDto, 'query'), 12 | async (req, res, next) => { 13 | try { 14 | const products = await service.find(req.query); 15 | res.json(products); 16 | } catch (error) { 17 | next(error); 18 | } 19 | } 20 | ); 21 | 22 | router.get('/:id', 23 | validatorHandler(getProductDto, 'params'), 24 | async (req, res, next) => { 25 | try { 26 | const { id } = req.params; 27 | const product = await service.findOne(id); 28 | res.json(product); 29 | } catch (error) { 30 | next(error); 31 | } 32 | } 33 | ); 34 | 35 | router.post('/', 36 | validatorHandler(createProductDto, 'body'), 37 | async (req, res, next) => { 38 | try { 39 | const body = req.body; 40 | const newProduct = await service.create(body); 41 | res.status(201).json(newProduct); 42 | } catch (error) { 43 | next(error); 44 | } 45 | } 46 | ); 47 | 48 | router.patch('/:id', 49 | validatorHandler(getProductDto, 'params'), 50 | validatorHandler(updateProductDto, 'body'), 51 | async (req, res, next) => { 52 | try { 53 | const { id } = req.params; 54 | const body = req.body; 55 | const product = await service.update(id, body); 56 | res.json(product); 57 | } catch (error) { 58 | next(error); 59 | } 60 | } 61 | ); 62 | 63 | router.delete('/:id', 64 | validatorHandler(getProductDto, 'params'), 65 | async (req, res, next) => { 66 | try { 67 | const { id } = req.params; 68 | await service.delete(id); 69 | res.status(201).json({id}); 70 | } catch (error) { 71 | next(error); 72 | } 73 | } 74 | ); 75 | 76 | module.exports = router; 77 | -------------------------------------------------------------------------------- /src/routes/categories.router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const passport = require('passport'); 3 | 4 | const CategoryService = require('./../services/category.service'); 5 | const validatorHandler = require('./../middlewares/validator.handler'); 6 | const { checkRoles } = require('./../middlewares/auth.handler'); 7 | const { createCategoryDto, updateCategoryDto, getCategoryDto } = require('../dtos/category.dto'); 8 | 9 | const router = express.Router(); 10 | const service = new CategoryService(); 11 | 12 | router.get('/', 13 | passport.authenticate('jwt', {session: false}), 14 | checkRoles('admin', 'seller', 'customer'), 15 | async (req, res, next) => { 16 | try { 17 | const categories = await service.find(); 18 | res.json(categories); 19 | } catch (error) { 20 | next(error); 21 | } 22 | }); 23 | 24 | router.get('/:id', 25 | passport.authenticate('jwt', {session: false}), 26 | checkRoles('admin', 'seller', 'customer'), 27 | validatorHandler(getCategoryDto, 'params'), 28 | async (req, res, next) => { 29 | try { 30 | const { id } = req.params; 31 | const category = await service.findOne(id); 32 | res.json(category); 33 | } catch (error) { 34 | next(error); 35 | } 36 | } 37 | ); 38 | 39 | router.post('/', 40 | passport.authenticate('jwt', {session: false}), 41 | checkRoles('admin'), 42 | validatorHandler(createCategoryDto, 'body'), 43 | async (req, res, next) => { 44 | try { 45 | const body = req.body; 46 | const newCategory = await service.create(body); 47 | res.status(201).json(newCategory); 48 | } catch (error) { 49 | next(error); 50 | } 51 | } 52 | ); 53 | 54 | router.patch('/:id', 55 | passport.authenticate('jwt', {session: false}), 56 | checkRoles('admin', 'seller'), 57 | validatorHandler(getCategoryDto, 'params'), 58 | validatorHandler(updateCategoryDto, 'body'), 59 | async (req, res, next) => { 60 | try { 61 | const { id } = req.params; 62 | const body = req.body; 63 | const category = await service.update(id, body); 64 | res.json(category); 65 | } catch (error) { 66 | next(error); 67 | } 68 | } 69 | ); 70 | 71 | router.delete('/:id', 72 | passport.authenticate('jwt', {session: false}), 73 | checkRoles('admin', 'seller'), 74 | validatorHandler(getCategoryDto, 'params'), 75 | async (req, res, next) => { 76 | try { 77 | const { id } = req.params; 78 | await service.delete(id); 79 | res.status(201).json({id}); 80 | } catch (error) { 81 | next(error); 82 | } 83 | } 84 | ); 85 | 86 | module.exports = router; 87 | -------------------------------------------------------------------------------- /src/services/auth.service.js: -------------------------------------------------------------------------------- 1 | const boom = require('@hapi/boom'); 2 | const bcrypt = require('bcrypt'); 3 | const jwt = require('jsonwebtoken'); 4 | const nodemailer = require('nodemailer'); 5 | 6 | const { config } = require('./../config/config'); 7 | const UserService = require('./user.service'); 8 | const service = new UserService(); 9 | 10 | class AuthService { 11 | 12 | async getUser(email, password) { 13 | const user = await service.findByEmail(email); 14 | if (!user) { 15 | throw boom.unauthorized(); 16 | } 17 | const isMatch = await bcrypt.compare(password, user.password); 18 | if (!isMatch) { 19 | throw boom.unauthorized();; 20 | } 21 | delete user.dataValues.password; 22 | return user; 23 | } 24 | 25 | signToken(user) { 26 | const payload = { 27 | sub: user.id, 28 | role: user.role 29 | } 30 | const access_token = jwt.sign(payload, config.jwtSecret); 31 | return { 32 | user, 33 | access_token 34 | }; 35 | } 36 | 37 | async sendRecovery(email) { 38 | const user = await service.findByEmail(email); 39 | if (!user) { 40 | throw boom.unauthorized(); 41 | } 42 | const payload = { sub: user.id }; 43 | const token = jwt.sign(payload, config.jwtSecret, {expiresIn: '15min'}); 44 | const link = `http://myfrontend.com/recovery?token=${token}`; 45 | await service.update(user.id, {recoveryToken: token}); 46 | const mail = { 47 | from: config.smtpEmail, 48 | to: `${user.email}`, 49 | subject: "Email para recuperar contraseña", 50 | html: `Ingresa a este link => ${link}`, 51 | } 52 | const rta = await this.sendMail(mail); 53 | return rta; 54 | } 55 | 56 | async changePassword(token, newPassword) { 57 | try { 58 | const payload = jwt.verify(token, config.jwtSecret); 59 | const user = await service.findOne(payload.sub); 60 | if (user.recoveryToken !== token) { 61 | throw boom.unauthorized(); 62 | } 63 | const hash = await bcrypt.hash(newPassword, 10); 64 | await service.update(user.id, {recoveryToken: null, password: hash}); 65 | return { message: 'password changed' }; 66 | } catch (error) { 67 | throw boom.unauthorized(); 68 | } 69 | } 70 | 71 | async sendMail(infoMail) { 72 | const transporter = nodemailer.createTransport({ 73 | host: "smtp.gmail.com", 74 | secure: true, 75 | port: 465, 76 | auth: { 77 | user: config.smtpEmail, 78 | pass: config.smtpPassword 79 | } 80 | }); 81 | await transporter.sendMail(infoMail); 82 | return { message: 'mail sent' }; 83 | } 84 | } 85 | 86 | module.exports = AuthService; 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/node,windows,linux,macos 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,windows,linux,macos 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### macOS ### 21 | # General 22 | .DS_Store 23 | .AppleDouble 24 | .LSOverride 25 | 26 | # Icon must end with two \r 27 | Icon 28 | 29 | 30 | # Thumbnails 31 | ._* 32 | 33 | # Files that might appear in the root of a volume 34 | .DocumentRevisions-V100 35 | .fseventsd 36 | .Spotlight-V100 37 | .TemporaryItems 38 | .Trashes 39 | .VolumeIcon.icns 40 | .com.apple.timemachine.donotpresent 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | 49 | ### Node ### 50 | # Logs 51 | logs 52 | *.log 53 | npm-debug.log* 54 | yarn-debug.log* 55 | yarn-error.log* 56 | lerna-debug.log* 57 | .pnpm-debug.log* 58 | 59 | # Diagnostic reports (https://nodejs.org/api/report.html) 60 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 61 | 62 | # Runtime data 63 | pids 64 | *.pid 65 | *.seed 66 | *.pid.lock 67 | 68 | # Directory for instrumented libs generated by jscoverage/JSCover 69 | lib-cov 70 | 71 | # Coverage directory used by tools like istanbul 72 | coverage 73 | *.lcov 74 | 75 | # nyc test coverage 76 | .nyc_output 77 | 78 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 79 | .grunt 80 | 81 | # Bower dependency directory (https://bower.io/) 82 | bower_components 83 | 84 | # node-waf configuration 85 | .lock-wscript 86 | 87 | # Compiled binary addons (https://nodejs.org/api/addons.html) 88 | build/Release 89 | 90 | # Dependency directories 91 | node_modules/ 92 | jspm_packages/ 93 | 94 | # Snowpack dependency directory (https://snowpack.dev/) 95 | web_modules/ 96 | 97 | # TypeScript cache 98 | *.tsbuildinfo 99 | 100 | # Optional npm cache directory 101 | .npm 102 | 103 | # Optional eslint cache 104 | .eslintcache 105 | 106 | # Microbundle cache 107 | .rpt2_cache/ 108 | .rts2_cache_cjs/ 109 | .rts2_cache_es/ 110 | .rts2_cache_umd/ 111 | 112 | # Optional REPL history 113 | .node_repl_history 114 | 115 | # Output of 'npm pack' 116 | *.tgz 117 | 118 | # Yarn Integrity file 119 | .yarn-integrity 120 | 121 | # dotenv environment variables file 122 | .env 123 | .env.test 124 | .env.production 125 | 126 | # parcel-bundler cache (https://parceljs.org/) 127 | .cache 128 | .parcel-cache 129 | 130 | # Next.js build output 131 | .next 132 | out 133 | 134 | # Nuxt.js build / generate output 135 | .nuxt 136 | dist 137 | 138 | # Gatsby files 139 | .cache/ 140 | # Comment in the public line in if your project uses Gatsby and not Next.js 141 | # https://nextjs.org/blog/next-9-1#public-directory-support 142 | # public 143 | 144 | # vuepress build output 145 | .vuepress/dist 146 | 147 | # Serverless directories 148 | .serverless/ 149 | 150 | # FuseBox cache 151 | .fusebox/ 152 | 153 | # DynamoDB Local files 154 | .dynamodb/ 155 | 156 | # TernJS port file 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | .vscode-test 161 | 162 | # yarn v2 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.* 168 | 169 | ### Windows ### 170 | # Windows thumbnail cache files 171 | Thumbs.db 172 | Thumbs.db:encryptable 173 | ehthumbs.db 174 | ehthumbs_vista.db 175 | 176 | # Dump file 177 | *.stackdump 178 | 179 | # Folder config file 180 | [Dd]esktop.ini 181 | 182 | # Recycle Bin used on file shares 183 | $RECYCLE.BIN/ 184 | 185 | # Windows Installer files 186 | *.cab 187 | *.msi 188 | *.msix 189 | *.msm 190 | *.msp 191 | 192 | # Windows shortcuts 193 | *.lnk 194 | 195 | postgres_data 196 | mysql_data 197 | .vscode 198 | 199 | # End of https://www.toptal.com/developers/gitignore/api/node,windows,linux,macos 200 | -------------------------------------------------------------------------------- /src/db/migrations/20210830181610-init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { USER_TABLE } = require('./../models/user.model'); 4 | const { CUSTOMER_TABLE } = require('./../models/customer.model'); 5 | const { CATEGORY_TABLE } = require('./../models/category.model'); 6 | const { PRODUCT_TABLE } = require('./../models/product.model'); 7 | const { ORDER_TABLE } = require('./../models/order.model'); 8 | const { ORDER_PRODUCT_TABLE } = require('./../models/order-product.model'); 9 | 10 | 11 | module.exports = { 12 | up: async (queryInterface, Sequelize) => { 13 | await queryInterface.createTable(USER_TABLE, { 14 | id: { 15 | allowNull: false, 16 | autoIncrement: true, 17 | primaryKey: true, 18 | type: Sequelize.DataTypes.INTEGER 19 | }, 20 | email: { 21 | allowNull: false, 22 | type: Sequelize.DataTypes.STRING, 23 | unique: true, 24 | }, 25 | password: { 26 | allowNull: false, 27 | type: Sequelize.DataTypes.STRING 28 | }, 29 | role: { 30 | allowNull: false, 31 | type: Sequelize.DataTypes.STRING, 32 | defaultValue: 'customer' 33 | }, 34 | createdAt: { 35 | allowNull: false, 36 | type: Sequelize.DataTypes.DATE, 37 | field: 'created_at', 38 | defaultValue: Sequelize.NOW 39 | }, 40 | recovery_token: { 41 | field: 'recovery_token', 42 | allowNull: true, 43 | type: Sequelize.DataTypes.STRING 44 | } 45 | }); 46 | await queryInterface.createTable(CUSTOMER_TABLE, { 47 | id: { 48 | allowNull: false, 49 | autoIncrement: true, 50 | primaryKey: true, 51 | type: Sequelize.DataTypes.INTEGER 52 | }, 53 | name: { 54 | allowNull: false, 55 | type: Sequelize.DataTypes.STRING, 56 | }, 57 | lastName: { 58 | allowNull: false, 59 | type: Sequelize.DataTypes.STRING, 60 | field: 'last_name', 61 | }, 62 | phone: { 63 | allowNull: true, 64 | type: Sequelize.DataTypes.STRING, 65 | }, 66 | createdAt: { 67 | allowNull: false, 68 | type: Sequelize.DataTypes.DATE, 69 | field: 'created_at', 70 | defaultValue: Sequelize.NOW, 71 | }, 72 | userId: { 73 | field: 'user_id', 74 | allowNull: false, 75 | type: Sequelize.DataTypes.INTEGER, 76 | unique: true, 77 | references: { 78 | model: USER_TABLE, 79 | key: 'id' 80 | }, 81 | onUpdate: 'CASCADE', 82 | onDelete: 'SET NULL' 83 | } 84 | }); 85 | await queryInterface.createTable(CATEGORY_TABLE, { 86 | id: { 87 | allowNull: false, 88 | autoIncrement: true, 89 | primaryKey: true, 90 | type: Sequelize.DataTypes.INTEGER 91 | }, 92 | name: { 93 | type: Sequelize.DataTypes.STRING, 94 | unique: true, 95 | allowNull: false, 96 | }, 97 | image: { 98 | type: Sequelize.DataTypes.STRING, 99 | allowNull: false, 100 | }, 101 | createdAt: { 102 | allowNull: false, 103 | type: Sequelize.DataTypes.DATE, 104 | field: 'created_at', 105 | defaultValue: Sequelize.NOW, 106 | }, 107 | }); 108 | await queryInterface.createTable(PRODUCT_TABLE, { 109 | id: { 110 | allowNull: false, 111 | autoIncrement: true, 112 | primaryKey: true, 113 | type: Sequelize.DataTypes.INTEGER 114 | }, 115 | name: { 116 | type: Sequelize.DataTypes.STRING, 117 | allowNull: false, 118 | }, 119 | image: { 120 | type: Sequelize.DataTypes.STRING, 121 | allowNull: false, 122 | }, 123 | description: { 124 | type: Sequelize.DataTypes.TEXT, 125 | allowNull: false, 126 | }, 127 | price: { 128 | type: Sequelize.DataTypes.INTEGER, 129 | allowNull: false, 130 | }, 131 | createdAt: { 132 | allowNull: false, 133 | type: Sequelize.DataTypes.DATE, 134 | field: 'created_at', 135 | defaultValue: Sequelize.NOW, 136 | }, 137 | categoryId: { 138 | field: 'category_id', 139 | allowNull: false, 140 | type: Sequelize.DataTypes.INTEGER, 141 | references: { 142 | model: CATEGORY_TABLE, 143 | key: 'id' 144 | }, 145 | onUpdate: 'CASCADE', 146 | onDelete: 'SET NULL' 147 | } 148 | }); 149 | await queryInterface.createTable(ORDER_TABLE, { 150 | id: { 151 | allowNull: false, 152 | autoIncrement: true, 153 | primaryKey: true, 154 | type: Sequelize.DataTypes.INTEGER 155 | }, 156 | customerId: { 157 | field: 'customer_id', 158 | allowNull: false, 159 | type: Sequelize.DataTypes.INTEGER, 160 | references: { 161 | model: CUSTOMER_TABLE, 162 | key: 'id' 163 | }, 164 | onUpdate: 'CASCADE', 165 | onDelete: 'SET NULL' 166 | }, 167 | createdAt: { 168 | allowNull: false, 169 | type: Sequelize.DataTypes.DATE, 170 | field: 'created_at', 171 | defaultValue: Sequelize.NOW, 172 | }, 173 | }); 174 | await queryInterface.createTable(ORDER_PRODUCT_TABLE, { 175 | id: { 176 | allowNull: false, 177 | autoIncrement: true, 178 | primaryKey: true, 179 | type: Sequelize.DataTypes.INTEGER 180 | }, 181 | createdAt: { 182 | allowNull: false, 183 | type: Sequelize.DataTypes.DATE, 184 | field: 'created_at', 185 | defaultValue: Sequelize.NOW, 186 | }, 187 | amount: { 188 | allowNull: false, 189 | type: Sequelize.DataTypes.INTEGER 190 | }, 191 | orderId: { 192 | field: 'order_id', 193 | allowNull: false, 194 | type: Sequelize.DataTypes.INTEGER, 195 | references: { 196 | model: ORDER_TABLE, 197 | key: 'id' 198 | }, 199 | onUpdate: 'CASCADE', 200 | onDelete: 'SET NULL' 201 | }, 202 | productId: { 203 | field: 'product_id', 204 | allowNull: false, 205 | type: Sequelize.DataTypes.INTEGER, 206 | references: { 207 | model: PRODUCT_TABLE, 208 | key: 'id' 209 | }, 210 | onUpdate: 'CASCADE', 211 | onDelete: 'SET NULL' 212 | } 213 | }); 214 | 215 | }, 216 | 217 | down: async (queryInterface) => { 218 | await queryInterface.dropTable(ORDER_PRODUCT_TABLE); 219 | await queryInterface.dropTable(ORDER_TABLE); 220 | await queryInterface.dropTable(PRODUCT_TABLE); 221 | await queryInterface.dropTable(CATEGORY_TABLE); 222 | await queryInterface.dropTable(CUSTOMER_TABLE); 223 | await queryInterface.dropTable(USER_TABLE); 224 | } 225 | }; 226 | -------------------------------------------------------------------------------- /insomnia.json: -------------------------------------------------------------------------------- 1 | {"_type":"export","__export_format":4,"__export_date":"2022-08-12T11:47:22.586Z","__export_source":"insomnia.desktop.app:v2022.5.0","resources":[{"_id":"req_c153496dc88641cd92e974ced0a402f5","parentId":"fld_4ceb6c7f0f91411d89e6edde805a53b3","modified":1660230360829,"created":1660230321853,"url":"{{ _.API_URL }}","name":"hello","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1660230321853,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_4ceb6c7f0f91411d89e6edde805a53b3","parentId":"wrk_274c0c4782374cbab8dc493e9591b40f","modified":1660304692238,"created":1660230309143,"name":"init","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1660230309243,"_type":"request_group"},{"_id":"wrk_274c0c4782374cbab8dc493e9591b40f","parentId":null,"modified":1660230089172,"created":1660230089172,"name":"Platzi Store API","description":"","scope":"collection","_type":"workspace"},{"_id":"req_e5e907f0c5694bcc97bc0f08dca739a5","parentId":"fld_4ceb6c7f0f91411d89e6edde805a53b3","modified":1660230368749,"created":1660230354723,"url":"{{ _.API_URL }}/nueva-ruta","name":"new-route","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1658373366661.125,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_f98bc9abf425415a87852b79acf2de70","parentId":"fld_3459a23ab8464e6ca79f1458da0c9f9c","modified":1660304486160,"created":1660230089224,"url":"{{ _.API_URL }}/api/v1/auth/login","name":"login","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"email\": \"admin@mail.com\",\n\t\"password\": \"admin123\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_6c4adef091164c07b597fc1c8415ff6a"}],"authentication":{},"metaSortKey":-1641566932833,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_3459a23ab8464e6ca79f1458da0c9f9c","parentId":"wrk_274c0c4782374cbab8dc493e9591b40f","modified":1660304643902,"created":1660230089223,"name":"auth","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1660230309193,"_type":"request_group"},{"_id":"req_c2862df88fcb44cf9f9076eccadf0410","parentId":"fld_3459a23ab8464e6ca79f1458da0c9f9c","modified":1660304548320,"created":1660304521934,"url":"{{ _.API_URL }}/api/v1/auth/recovery","name":"recovery","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"email\": \"admin@mail.com\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_6c4adef091164c07b597fc1c8415ff6a"}],"authentication":{},"metaSortKey":-1641561546066.7656,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_f8d5a75eb23540edb9f1ca9d9f450b66","parentId":"fld_3459a23ab8464e6ca79f1458da0c9f9c","modified":1660304552697,"created":1660304536745,"url":"{{ _.API_URL }}/api/v1/auth/change-password","name":"change-password","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"token\": \"---\",\n\t\"email\": \"admin@mail.com\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_6c4adef091164c07b597fc1c8415ff6a"}],"authentication":{},"metaSortKey":-1641558852683.6484,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_489692a699f041d199c2e92a2e40838a","parentId":"fld_d7310bd77bff4769a8b6ad8dc56fde08","modified":1660304640557,"created":1660230089198,"url":"{{ _.API_URL }}/api/v1/categories/","name":"getAll","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"type":"bearer","token":"{{ _.ACCESS_TOKEN }}"},"metaSortKey":-1634932093112,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_d7310bd77bff4769a8b6ad8dc56fde08","parentId":"wrk_274c0c4782374cbab8dc493e9591b40f","modified":1660304646674,"created":1660230089196,"name":"categories","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1660230309168,"_type":"request_group"},{"_id":"req_09563cff5d13450fb00aa756187655c5","parentId":"fld_d7310bd77bff4769a8b6ad8dc56fde08","modified":1660304654412,"created":1660230089200,"url":"{{ _.API_URL }}/api/v1/categories/1","name":"getOne","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"type":"bearer","token":"{{ _.ACCESS_TOKEN }}"},"metaSortKey":-1634916067803.1133,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_f2d0680190494d1781500b930f1b570c","parentId":"fld_d7310bd77bff4769a8b6ad8dc56fde08","modified":1660232684275,"created":1660230089205,"url":"{{ _.API_URL }}/api/v1/categories/","name":"create","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n \"name\": \"New Category\",\n \"image\": \"https://api.lorem.space/image/game?w=150&h=220\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_7461a0646fad4c75a737345770f9cf0f"}],"authentication":{"type":"bearer","token":"{{ _.ACCESS_TOKEN }}"},"metaSortKey":-1634900042494.2266,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_c7ce7e47dc0043918446a94b0288f067","parentId":"fld_d7310bd77bff4769a8b6ad8dc56fde08","modified":1660304665486,"created":1660230089202,"url":"{{ _.API_URL }}/api/v1/categories/3","name":"update","description":"","method":"PUT","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"nuevo\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_7461a0646fad4c75a737345770f9cf0f"}],"authentication":{"type":"bearer","token":"{{ _.ACCESS_TOKEN }}"},"metaSortKey":-1634419283227.625,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_a28cb7f579aa46c3b9c01659f59b4396","parentId":"fld_d7310bd77bff4769a8b6ad8dc56fde08","modified":1660304676835,"created":1660304669529,"url":"{{ _.API_URL }}/api/v1/categories/3","name":"delete","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{"type":"bearer","token":"{{ _.ACCESS_TOKEN }}"},"metaSortKey":-1634312447835.0469,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_3523528de09e470daef8ec57b04fed58","parentId":"fld_7ee90c40698844769342c3814ee75e47","modified":1660231026679,"created":1660231022149,"url":"{{ _.API_URL }}/api/v1/customers/","name":"getAll","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1645014463273,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_7ee90c40698844769342c3814ee75e47","parentId":"wrk_274c0c4782374cbab8dc493e9591b40f","modified":1660304685091,"created":1660231022140,"name":"customers","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1660230309155.5,"_type":"request_group"},{"_id":"req_2caad79b5c724138a93023edd51427a1","parentId":"fld_7ee90c40698844769342c3814ee75e47","modified":1660304700360,"created":1660231022144,"url":"{{ _.API_URL }}/api/v1/customers/","name":"create","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Valentina\",\n\t\"lastName\": \"Molina\",\n\t\"phone\": \"121212\",\n\t\"user\": {\n\t\t\"email\": \"customer@gmail.com\",\n\t\t\"password\": \"12345678\"\n\t}\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_6c4adef091164c07b597fc1c8415ff6a"}],"authentication":{},"metaSortKey":-1645014463223,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_398d97ac9f9849abb94bba9dee72eb7f","parentId":"fld_7ee90c40698844769342c3814ee75e47","modified":1660304704611,"created":1660231022153,"url":"{{ _.API_URL }}/api/v1/customers/1","name":"update","description":"","method":"PUT","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Nicolas\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_6c4adef091164c07b597fc1c8415ff6a"}],"authentication":{},"metaSortKey":-1643635451067,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_5b25b6ca4c53407fbe980f02d6d16703","parentId":"fld_7ee90c40698844769342c3814ee75e47","modified":1660304710968,"created":1660304708228,"url":"{{ _.API_URL }}/api/v1/customers/1","name":"delete","description":"","method":"DELETE","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Nicolas\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_6c4adef091164c07b597fc1c8415ff6a"}],"authentication":{},"metaSortKey":-1642945944989,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_09b9e4da95794e2f84cf8ed01ce780ef","parentId":"fld_0f6c0f07d86e422a83e0aae5fe884ca7","modified":1660304364740,"created":1660304272573,"url":"{{ _.API_URL }}/api/v1/orders/1","name":"getOne","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1634590219855.75,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_0f6c0f07d86e422a83e0aae5fe884ca7","parentId":"wrk_274c0c4782374cbab8dc493e9591b40f","modified":1660304725910,"created":1660304272567,"name":"orders","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1658685883035,"_type":"request_group"},{"_id":"req_f2fb0c5183ff4a948c1842c7839a05d9","parentId":"fld_0f6c0f07d86e422a83e0aae5fe884ca7","modified":1660304450677,"created":1660304272576,"url":"{{ _.API_URL }}/api/v1/orders/add-item","name":"add item","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n \"orderId\": 1,\n \"productId\": 2,\n\t\"amount\": 2\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_0df83e19008842feb0a4366fe1624462"}],"authentication":{},"metaSortKey":-1634419283227.625,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_d398b4236707449885ac40c44fa0a0f7","parentId":"fld_0f6c0f07d86e422a83e0aae5fe884ca7","modified":1660304434766,"created":1660304272578,"url":"{{ _.API_URL }}/api/v1/orders/","name":"create","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n \"customerId\": 1\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_0df83e19008842feb0a4366fe1624462"}],"authentication":{},"metaSortKey":-1633991941657.3125,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_0b24e1287db2498ab6540634c60375cb","parentId":"fld_6b6f3ec477cb4549bf54b4fcc068a176","modified":1660230171812,"created":1660230089183,"url":"{{ _.API_URL }}/api/v1/products/","name":"getAll","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1634932093112,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_6b6f3ec477cb4549bf54b4fcc068a176","parentId":"wrk_274c0c4782374cbab8dc493e9591b40f","modified":1660304735261,"created":1660230089181,"name":"products","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1657913669974.75,"_type":"request_group"},{"_id":"req_33c9f643bd714c1488ef8e4d10a5179a","parentId":"fld_6b6f3ec477cb4549bf54b4fcc068a176","modified":1660304739582,"created":1660230089185,"url":"{{ _.API_URL }}/api/v1/products/10","name":"getOne","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1634867991876.4531,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_62585c6659854a899c735112044dd74e","parentId":"fld_6b6f3ec477cb4549bf54b4fcc068a176","modified":1660304791454,"created":1660230089193,"url":"{{ _.API_URL }}/api/v1/products/","name":"pagination","description":"","method":"GET","body":{},"parameters":[{"id":"pair_3ba9be553276479d83f2e6330c75f661","name":"limit","value":"2","description":"","disabled":false},{"id":"pair_3d77edb2679c4116b410b920fc75a9c5","name":"offset","value":"0","description":"","disabled":false}],"headers":[],"authentication":{},"metaSortKey":-1634803890640.9062,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_dad89d8df1e74d5690b75cd77d66f153","parentId":"fld_6b6f3ec477cb4549bf54b4fcc068a176","modified":1660304743658,"created":1660230089191,"url":"{{ _.API_URL }}/api/v1/products/","name":"create","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n \"name\": \"New Product\",\n \"price\": 10,\n \"description\": \"A description\",\n \"categoryId\": 1,\n \"image\": \"https://api.lorem.space/image/game?w=150&h=220\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_0df83e19008842feb0a4366fe1624462"}],"authentication":{},"metaSortKey":-1634611586934.2656,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_44b34d3fbe4b4711ae6bbf2d61a07a04","parentId":"fld_6b6f3ec477cb4549bf54b4fcc068a176","modified":1660304748809,"created":1660230089187,"url":"{{ _.API_URL }}/api/v1/products/10","name":"update","description":"","method":"PUT","body":{"mimeType":"application/json","text":"{\n \"title\": \"Change title\",\n \"price\": 100\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_0df83e19008842feb0a4366fe1624462"}],"authentication":{},"metaSortKey":-1634419283227.625,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_72401977ce6a4262ad92bece8712b53e","parentId":"fld_6b6f3ec477cb4549bf54b4fcc068a176","modified":1660304759080,"created":1660304753711,"url":"{{ _.API_URL }}/api/v1/products/10","name":"delete","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1634365865531.336,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_b7a6f2afd3814205975837d4cc3c7dec","parentId":"fld_36b7fcf7c30244fcb6c34fe3bbca81a8","modified":1660304631278,"created":1660230089225,"url":"{{ _.API_URL }}/api/v1/profile/my-orders","name":"profile","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"type":"bearer","token":"{{ _.ACCESS_TOKEN }}","disabled":false},"metaSortKey":-1654438711214.5625,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_36b7fcf7c30244fcb6c34fe3bbca81a8","parentId":"wrk_274c0c4782374cbab8dc493e9591b40f","modified":1660304803782,"created":1660304600670,"name":"profile","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1657527563444.625,"_type":"request_group"},{"_id":"req_2ec8c6e09ce54ebb82cd35bfea46b064","parentId":"fld_4460e82b21404e0384fa66af6c38eeb1","modified":1660230214850,"created":1660230089215,"url":"{{ _.API_URL }}/api/v1/users/","name":"getAll","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1645014463273,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_4460e82b21404e0384fa66af6c38eeb1","parentId":"wrk_274c0c4782374cbab8dc493e9591b40f","modified":1660230923767,"created":1660230089209,"name":"users","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1657141456914.5,"_type":"request_group"},{"_id":"req_01a529af7eb34a14a1d3fab227c9d22d","parentId":"fld_4460e82b21404e0384fa66af6c38eeb1","modified":1660304814793,"created":1660230089218,"url":"{{ _.API_URL }}/api/v1/users/1212","name":"getOne","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1645014463248,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_ea3a0017958948b39b67d2fa889c840b","parentId":"fld_4460e82b21404e0384fa66af6c38eeb1","modified":1660232328767,"created":1660230089211,"url":"{{ _.API_URL }}/api/v1/users/","name":"create","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"email\": \"admin@mail.com\",\n\t\"password\": \"admin123\",\n\t\"role\": \"admin\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_6c4adef091164c07b597fc1c8415ff6a"}],"authentication":{},"metaSortKey":-1645014463223,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_a53057701c4541ada5d38ce5cbf700fd","parentId":"fld_4460e82b21404e0384fa66af6c38eeb1","modified":1660304819880,"created":1660230089219,"url":"{{ _.API_URL }}/api/v1/users/1","name":"update","description":"","method":"PUT","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Nicolas\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_6c4adef091164c07b597fc1c8415ff6a"}],"authentication":{},"metaSortKey":-1643635451067,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_60f6fcbc3a3d41b184afaa6163c6b81e","parentId":"fld_4460e82b21404e0384fa66af6c38eeb1","modified":1660304824844,"created":1660304822821,"url":"{{ _.API_URL }}/api/v1/users/1","name":"delete","description":"","method":"DELETE","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Nicolas\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_6c4adef091164c07b597fc1c8415ff6a"}],"authentication":{},"metaSortKey":-1643290698028,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"env_b384bf9405454baf9af26bbf2de16eac","parentId":"wrk_274c0c4782374cbab8dc493e9591b40f","modified":1660230107847,"created":1660230089174,"name":"Base Environment","data":{},"dataPropertyOrder":{},"color":null,"isPrivate":false,"metaSortKey":1634932084267,"_type":"environment"},{"_id":"jar_358d2f6fa8df4054a796cb228b71851f","parentId":"wrk_274c0c4782374cbab8dc493e9591b40f","modified":1660230089176,"created":1660230089176,"name":"Default Jar","cookies":[{"key":"_mcid","value":"1.0e909ed12c0a970f1d20fb728a2c8d1e.82d987a5c3e3505a224dc90f166c56edb9b6eca7e38592ff4bc074503da74a51","expires":"2023-01-15T00:16:14.000Z","maxAge":31536000,"domain":"nicobytes.us20.list-manage.com","path":"/","hostOnly":true,"creation":"2022-01-14T23:08:37.926Z","lastAccessed":"2022-01-15T00:16:13.975Z","id":"20978535593938807"},{"key":"_abck","value":"B0563B8DEF26B459BF90EC433C93A1A4~-1~YAAQ16NivnP/9KB9AQAA7p3ZWgfu5i0ETaJ1lOfOq4U/G0rh0ICBFjd4Xevexuipa8+9Z3r21fMfC6JKPA0/aHUII+wTRMwd9+q5S51iymoX1UBy1GySJbo4+YurOLTYA11FF3Wa6kSr1+uJxtgkLQDqBtODWegti02W1SuGAuIAA9lU3RR2p/GXSnAtIn1UWvASlD8EoxVPUrjTTd/lu2tgUFMBVnoQmRFOZuS5gArCRIARrHHzQ4DxPFGF5sVmdjFgKTXrsTgUxSly5mh9umlB5FPAFfGWKixtUA7iRvUR2d8KFmrNJp5nlXIlVNPSQhbIuT6gLmytf022q+UI58sWESyRO0cirHBoaMmwUdtHkVsk3k3cBbDlN/WlBg==~-1~-1~-1","expires":"2023-01-14T23:08:38.000Z","maxAge":31536000,"domain":"list-manage.com","path":"/","secure":true,"extensions":["SameSite=None"],"hostOnly":false,"creation":"2022-01-14T23:08:37.926Z","lastAccessed":"2022-01-14T23:08:37.926Z","id":"9567220843806037"},{"key":"ak_bmsc","value":"46BC42567BB834EF71243EF8295206A0~000000000000000000000000000000~YAAQ16NivnT/9KB9AQAA7p3ZWg6lrjHZ+whpQIXYWPa5fEm6BbN8VTTEumPpwKzMdvgRfJrirKuNpSL/owZ8F4nEyRQhvceLv4wFhRbcFL53QYMNUKjyHQqi/C3SL+3n/6ldoBKPyrua9XmOE5Fl4sWA0Ya7vG54Wu8abdn7W7OHLCSJW90bQ3XNuifvdqIezryA+hAPYI+HiUKpY4jozORnxRSV5Q4SvdAWIXNsSApP6B0PUnhrCuSDNLeTMTrRknCpS5hEIcFfiST74EVNpl4oW5deOYNFoXiqPth4liWnaY5fkhv9f5fqsQrjlQdGO0sTgG9eRHjscWxnweGAVEauYDui+TJ9rDtwE9VENifOUlpUkdWjKi/ZUS/PAJlvrYlsFF6CP4l9YpUJQrHU6fJG5A+Eig==","expires":"2022-01-15T01:08:37.000Z","maxAge":7199,"domain":"us20.list-manage.com","path":"/","secure":true,"httpOnly":true,"extensions":["SameSite=None"],"hostOnly":false,"creation":"2022-01-14T23:08:37.927Z","lastAccessed":"2022-01-14T23:08:37.927Z","id":"9202008446698922"},{"key":"bm_sz","value":"4CA2EDCDBBC258ACD6AB5F47B7327AE7~YAAQ16NivnX/9KB9AQAA7p3ZWg5wOtk5Qwlq/K9g0z2BtEUbkubSNrXG91WLeLOLeKIjIeT93Afy0mz8I9zK3ccXYF4rNba9hQVDpGjoRsAh0JhineoatQZgoqHcdDHOPZF+Wdpi6QF4FQmc+5sjb3hjH+qXW7APB7DORJeUHSgSluyLrJ/0kiHaR5bbsAyID8YqXmR4Z1uugZqzaikcmeYftSFFv6qXHXRh29l9GNIGu/faH2g36PRjNi3sTaQjspybPggAP4RXopuWIq1/Hv3PCuV7vO4MTsLvgphfEavApyWQSONn8w==~4534325~3290677","expires":"2022-01-15T03:08:37.000Z","maxAge":14399,"domain":"list-manage.com","path":"/","secure":true,"extensions":["SameSite=None"],"hostOnly":false,"creation":"2022-01-14T23:08:37.927Z","lastAccessed":"2022-01-14T23:08:37.927Z","id":"6638779298120716"},{"key":"bm_sv","value":"8B663F95A9F98C36431D10D1E2E91AA8~BKANYTsmHB3XKPV8rZenHtcybBfJdB11roU9NnvyeXSIZ8XCECzDw3NEEZ11j/daHQ9vNNb9i7z3mTinLBroAE5EKIiIs7gRGckJJjuvDfhbtQvyJa1m9MiYjGmjaEmKsAwDQYw5E+t8p0Bc3J6zJWrm+duArTHeZFxgcDa35X0=","maxAge":3153,"domain":"us20.list-manage.com","path":"/","httpOnly":true,"hostOnly":false,"creation":"2022-01-14T23:08:47.771Z","lastAccessed":"2022-01-15T00:16:13.975Z","id":"26026988064497036"}],"_type":"cookie_jar"},{"_id":"spc_b678adceeef94c809a008c9af041cd10","parentId":"wrk_274c0c4782374cbab8dc493e9591b40f","modified":1660230089245,"created":1660230089179,"fileName":"Platzi Store API","contents":"","contentType":"yaml","_type":"api_spec"},{"_id":"env_4b2f12a3f92949ff84f029fd651c7942","parentId":"env_b384bf9405454baf9af26bbf2de16eac","modified":1660231243629,"created":1660230112159,"name":"Dev","data":{"API_URL":"http://localhost:3000","ACCESS_TOKEN":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjQsInJvbGUiOiJhZG1pbiIsImlhdCI6MTY2MDIzMTIyM30.GFrfKZywiH9NvzistyP1_p1cOL65oS3Q_rjIrU9vbeU"},"dataPropertyOrder":{"&":["API_URL","ACCESS_TOKEN"]},"color":"#7d69cb","isPrivate":false,"metaSortKey":1660230112159,"_type":"environment"}]} --------------------------------------------------------------------------------