├── Procfile ├── uploads ├── 1643350160914-baju.jpg ├── 1643354576661-baju.jpg ├── lesly-juarez-RukI4qZGlQs-unsplash.jpg ├── 1627670180486-revolt-164_6wVEHfI-unsplash.jpg ├── 1627657235024-hostaphoto-XFhny3yLA0c-unsplash.jpg ├── info.txt └── 1627657298953-luis-quintero-8TSqJoI-NVs-unsplash.jpg ├── utils └── cloudinary.js ├── .gitignore ├── src ├── database │ └── connection.js ├── controllers │ ├── profile.js │ ├── category.js │ ├── user.js │ ├── auth.js │ ├── product.js │ └── transaction.js ├── middlewares │ ├── auth.js │ └── uploadFile.js ├── routes │ └── index.js └── socket │ └── index.js ├── config └── config.json ├── models ├── productcategory.js ├── category.js ├── profile.js ├── chat.js ├── index.js ├── transaction.js ├── product.js └── user.js ├── migrations ├── 20210712034523-create-category.js ├── 20210706041655-create-user.js ├── 20210712034946-create-product-category.js ├── 20210712032234-create-profile.js ├── 20210728051846-create-chat.js ├── 20210712032356-create-product.js └── 20210712033433-create-transaction.js ├── seeders └── 20210724133318-user.js ├── package.json └── index.js /Procfile: -------------------------------------------------------------------------------- 1 | release: node_modules/.bin/sequelize db:migrate; node_modules/.bin/sequelize db:seed:all; 2 | 3 | web: node index.js -------------------------------------------------------------------------------- /uploads/1643350160914-baju.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinputra01/BackEnd-Dumbmerch/HEAD/uploads/1643350160914-baju.jpg -------------------------------------------------------------------------------- /uploads/1643354576661-baju.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinputra01/BackEnd-Dumbmerch/HEAD/uploads/1643354576661-baju.jpg -------------------------------------------------------------------------------- /uploads/lesly-juarez-RukI4qZGlQs-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinputra01/BackEnd-Dumbmerch/HEAD/uploads/lesly-juarez-RukI4qZGlQs-unsplash.jpg -------------------------------------------------------------------------------- /uploads/1627670180486-revolt-164_6wVEHfI-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinputra01/BackEnd-Dumbmerch/HEAD/uploads/1627670180486-revolt-164_6wVEHfI-unsplash.jpg -------------------------------------------------------------------------------- /uploads/1627657235024-hostaphoto-XFhny3yLA0c-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinputra01/BackEnd-Dumbmerch/HEAD/uploads/1627657235024-hostaphoto-XFhny3yLA0c-unsplash.jpg -------------------------------------------------------------------------------- /uploads/info.txt: -------------------------------------------------------------------------------- 1 | This directory must created to store uploaded file, 2 | 3 | the directory name must match with destination name 4 | defined in multer.diskStorage options callback. -------------------------------------------------------------------------------- /uploads/1627657298953-luis-quintero-8TSqJoI-NVs-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinputra01/BackEnd-Dumbmerch/HEAD/uploads/1627657298953-luis-quintero-8TSqJoI-NVs-unsplash.jpg -------------------------------------------------------------------------------- /utils/cloudinary.js: -------------------------------------------------------------------------------- 1 | const cloudinary = require('cloudinary').v2; 2 | cloudinary.config({ 3 | cloud_name: process.env.CLOUDINARY_NAME, 4 | api_key: process.env.CLOUDINARY_API_KEY, 5 | api_secret: process.env.CLOUDINARY_API_SECRET, 6 | }); 7 | 8 | module.exports = cloudinary; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .env 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* -------------------------------------------------------------------------------- /src/database/connection.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize') 2 | const db = {} 3 | const sequelize = new Sequelize('course-express', 4 | 'root', 5 | 'root', { 6 | host: 'localhost', 7 | port: '8889', 8 | dialect: 'mysql', 9 | logging: console.log, 10 | freezeTableName: true, 11 | 12 | pool: { 13 | max: 5, 14 | min: 0, 15 | acquire: 30000, 16 | idle: 10000 17 | } 18 | }) 19 | 20 | db.sequelize = sequelize 21 | 22 | module.exports = db -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "username": "root", 4 | "password": null, 5 | "database": "dumbmerch", 6 | "host": "localhost", 7 | "dialect": "mysql" 8 | }, 9 | "test": { 10 | "username": "root", 11 | "password": null, 12 | "database": "database_test", 13 | "host": "127.0.0.1", 14 | "dialect": "mysql" 15 | }, 16 | "production": { 17 | "use_env_variable": "DATABASE_URL", 18 | "dialect": "postgres", 19 | "protocol": "postgres", 20 | "dialectOptions": { 21 | "ssl": { 22 | "require": true, 23 | "rejectUnauthorized": false 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /models/productcategory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | Model 4 | } = require('sequelize'); 5 | module.exports = (sequelize, DataTypes) => { 6 | class productCategory extends Model { 7 | /** 8 | * Helper method for defining associations. 9 | * This method is not a part of Sequelize lifecycle. 10 | * The `models/index` file will call this method automatically. 11 | */ 12 | static associate(models) { 13 | // define association here 14 | } 15 | }; 16 | productCategory.init({ 17 | idProduct: DataTypes.INTEGER, 18 | idCategory: DataTypes.INTEGER 19 | }, { 20 | sequelize, 21 | modelName: 'productCategory', 22 | }); 23 | return productCategory; 24 | }; -------------------------------------------------------------------------------- /migrations/20210712034523-create-category.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('categories', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | name: { 12 | type: Sequelize.STRING 13 | }, 14 | createdAt: { 15 | allowNull: false, 16 | type: Sequelize.DATE 17 | }, 18 | updatedAt: { 19 | allowNull: false, 20 | type: Sequelize.DATE 21 | } 22 | }); 23 | }, 24 | down: async (queryInterface, Sequelize) => { 25 | await queryInterface.dropTable('categories'); 26 | } 27 | }; -------------------------------------------------------------------------------- /src/controllers/profile.js: -------------------------------------------------------------------------------- 1 | const { profile } = require("../../models"); 2 | 3 | exports.getProfile = async (req, res) => { 4 | try { 5 | const idUser = req.user.id; 6 | 7 | let data = await profile.findOne({ 8 | where: { 9 | idUser, 10 | }, 11 | attributes: { 12 | exclude: ["createdAt", "updatedAt", "idUser"], 13 | }, 14 | }); 15 | 16 | data = JSON.parse(JSON.stringify(data)); 17 | 18 | data = { 19 | ...data, 20 | image: data.image ? process.env.PATH_FILE + data.image : null, 21 | }; 22 | 23 | res.send({ 24 | status: "success...", 25 | data, 26 | }); 27 | } catch (error) { 28 | console.log(error); 29 | res.send({ 30 | status: "failed", 31 | message: "Server Error", 32 | }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/middlewares/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | 3 | exports.auth = (req, res, next) => { 4 | const authHeader = req.header("Authorization"); 5 | const token = authHeader && authHeader.split(" ")[1]; 6 | // check if user send token via Authorization header or not 7 | if (!token) { 8 | // rejected request and send response access denied 9 | return res.status(401).send({ 10 | status: "failed", 11 | message: "Access denied!", 12 | }); 13 | } 14 | 15 | try { 16 | const verified = jwt.verify(token, process.env.TOKEN_KEY); //verified token 17 | req.user = verified; 18 | next(); // if token valid go to the next request 19 | } catch (error) { 20 | // if token not valid send response invalid token 21 | res.status(400).send({ 22 | status: "failed", 23 | message: "Invalid token", 24 | }); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /models/category.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { Model } = require("sequelize"); 3 | module.exports = (sequelize, DataTypes) => { 4 | class category extends Model { 5 | /** 6 | * Helper method for defining associations. 7 | * This method is not a part of Sequelize lifecycle. 8 | * The `models/index` file will call this method automatically. 9 | */ 10 | static associate(models) { 11 | // define association here 12 | // belongs to many products 13 | category.belongsToMany(models.product, { 14 | as: "products", 15 | through: { 16 | model: "productCategory", 17 | as: "bridge", 18 | }, 19 | foreignKey: "idCategory", 20 | }); 21 | } 22 | } 23 | category.init( 24 | { 25 | name: DataTypes.STRING, 26 | }, 27 | { 28 | sequelize, 29 | modelName: "category", 30 | } 31 | ); 32 | return category; 33 | }; 34 | -------------------------------------------------------------------------------- /models/profile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { Model } = require("sequelize"); 3 | module.exports = (sequelize, DataTypes) => { 4 | class profile extends Model { 5 | /** 6 | * Helper method for defining associations. 7 | * This method is not a part of Sequelize lifecycle. 8 | * The `models/index` file will call this method automatically. 9 | */ 10 | static associate(models) { 11 | // define association here 12 | profile.belongsTo(models.user, { 13 | as: "user", 14 | foreignKey: { 15 | name: "idUser", 16 | }, 17 | }); 18 | } 19 | } 20 | profile.init( 21 | { 22 | phone: DataTypes.STRING, 23 | gender: DataTypes.STRING, 24 | address: DataTypes.STRING, 25 | image: DataTypes.STRING, 26 | idUser: DataTypes.INTEGER, 27 | }, 28 | { 29 | sequelize, 30 | modelName: "profile", 31 | } 32 | ); 33 | return profile; 34 | }; 35 | -------------------------------------------------------------------------------- /seeders/20210724133318-user.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | /** 6 | * Add seed commands here. 7 | * 8 | * Example: 9 | * await queryInterface.bulkInsert('People', [{ 10 | * name: 'John Doe', 11 | * isBetaMember: false 12 | * }], {}); 13 | */ 14 | 15 | await queryInterface.bulkInsert( 16 | "users", 17 | [ 18 | { 19 | email: "admin@mail.com", 20 | password: 21 | "$2b$10$7ovHDrtaMe.FmutXxEhnWOo7rDOdTloUMgqms5RXYmL5/4dfM.OTm", //123456 22 | name: "admin", 23 | status: "admin", 24 | }, 25 | ], 26 | {} 27 | ); 28 | }, 29 | 30 | down: async (queryInterface, Sequelize) => { 31 | /** 32 | * Add commands to revert seed here. 33 | * 34 | * Example: 35 | * await queryInterface.bulkDelete('People', null, {}); 36 | */ 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon server.js", 8 | "client": "npm start --prefix ../client", 9 | "dev": "concurrently \"npm start\" \"npm run client\"" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcrypt": "^5.0.1", 15 | "cloudinary": "^1.28.1", 16 | "concurrently": "^6.2.0", 17 | "cors": "^2.8.5", 18 | "dotenv": "^10.0.0", 19 | "express": "^4.17.1", 20 | "joi": "^17.4.1", 21 | "jsonwebtoken": "^8.5.1", 22 | "midtrans-client": "^1.2.4", 23 | "multer": "^1.4.2", 24 | "mysql2": "^2.2.5", 25 | "nodemailer": "^6.6.3", 26 | "pg": "^8.7.1", 27 | "rupiah-format": "^1.0.0", 28 | "sequelize": "^6.6.4", 29 | "sequelize-cli": "^6.2.0", 30 | "socket.io": "^4.1.3" 31 | }, 32 | "devDependencies": { 33 | "nodemon": "^2.0.9" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /models/chat.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | Model 4 | } = require('sequelize'); 5 | module.exports = (sequelize, DataTypes) => { 6 | class chat extends Model { 7 | /** 8 | * Helper method for defining associations. 9 | * This method is not a part of Sequelize lifecycle. 10 | * The `models/index` file will call this method automatically. 11 | */ 12 | static associate(models) { 13 | // define association here 14 | chat.belongsTo(models.user, { 15 | as: "sender", 16 | foreignKey: { 17 | name: "idSender", 18 | }, 19 | }); 20 | chat.belongsTo(models.user, { 21 | as: "recipient", 22 | foreignKey: { 23 | name: "idRecipient", 24 | }, 25 | }); 26 | } 27 | }; 28 | chat.init({ 29 | message: DataTypes.TEXT, 30 | idSender: DataTypes.INTEGER, 31 | idRecipient: DataTypes.INTEGER 32 | }, { 33 | sequelize, 34 | modelName: 'chat', 35 | }); 36 | return chat; 37 | }; -------------------------------------------------------------------------------- /migrations/20210706041655-create-user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('users', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | email: { 12 | type: Sequelize.STRING 13 | }, 14 | password: { 15 | type: Sequelize.STRING 16 | }, 17 | name: { 18 | type: Sequelize.STRING 19 | }, 20 | status: { 21 | type: Sequelize.STRING 22 | }, 23 | createdAt: { 24 | allowNull: false, 25 | defaultValue: Sequelize.fn('now'), 26 | type: Sequelize.DATE 27 | }, 28 | updatedAt: { 29 | allowNull: false, 30 | defaultValue: Sequelize.fn('now'), 31 | type: Sequelize.DATE 32 | } 33 | }); 34 | }, 35 | down: async (queryInterface, Sequelize) => { 36 | await queryInterface.dropTable('users'); 37 | } 38 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // import dotenv and call config function to load environment 2 | require("dotenv").config(); 3 | const express = require("express"); 4 | 5 | const cors = require("cors"); 6 | 7 | // import this 8 | const http = require("http"); 9 | const { Server } = require("socket.io"); 10 | 11 | // Get routes to the variabel 12 | const router = require("./src/routes"); 13 | 14 | const app = express(); 15 | 16 | const server = http.createServer(app); 17 | const io = new Server(server, { 18 | cors: { 19 | origin: "http://localhost:3000", // we must define cors because our client and server have diffe 20 | }, 21 | }); 22 | 23 | // import socket function and call with parameter io 24 | require("./src/socket")(io); 25 | 26 | const port = process.env.PORT || 5000; 27 | 28 | app.use(express.json()); 29 | app.use(cors()); 30 | 31 | // Add endpoint grouping and router 32 | app.use("/api/v1/", router); 33 | app.use("/uploads", express.static("uploads")); 34 | 35 | // change app to server 36 | server.listen(port, () => console.log(`Listening on port ${port}!`)); 37 | -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const Sequelize = require('sequelize'); 6 | const basename = path.basename(__filename); 7 | const env = process.env.NODE_ENV || 'development'; 8 | const config = require(__dirname + '/../config/config.json')[env]; 9 | const db = {}; 10 | 11 | let sequelize; 12 | if (config.use_env_variable) { 13 | sequelize = new Sequelize(process.env[config.use_env_variable], config); 14 | } else { 15 | sequelize = new Sequelize(config.database, config.username, config.password, config); 16 | } 17 | 18 | fs 19 | .readdirSync(__dirname) 20 | .filter(file => { 21 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); 22 | }) 23 | .forEach(file => { 24 | const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes); 25 | db[model.name] = model; 26 | }); 27 | 28 | Object.keys(db).forEach(modelName => { 29 | if (db[modelName].associate) { 30 | db[modelName].associate(db); 31 | } 32 | }); 33 | 34 | db.sequelize = sequelize; 35 | db.Sequelize = Sequelize; 36 | 37 | module.exports = db; 38 | -------------------------------------------------------------------------------- /migrations/20210712034946-create-product-category.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('productCategories', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | idProduct: { 12 | type: Sequelize.INTEGER, 13 | references: { 14 | model: "products", 15 | key: "id", 16 | }, 17 | onUpdate: "CASCADE", 18 | onDelete: "CASCADE", 19 | }, 20 | idCategory: { 21 | type: Sequelize.INTEGER, 22 | references: { 23 | model: "categories", 24 | key: "id", 25 | }, 26 | onUpdate: "CASCADE", 27 | onDelete: "CASCADE", 28 | }, 29 | createdAt: { 30 | allowNull: false, 31 | type: Sequelize.DATE 32 | }, 33 | updatedAt: { 34 | allowNull: false, 35 | type: Sequelize.DATE 36 | } 37 | }); 38 | }, 39 | down: async (queryInterface, Sequelize) => { 40 | await queryInterface.dropTable('productCategories'); 41 | } 42 | }; -------------------------------------------------------------------------------- /migrations/20210712032234-create-profile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable("profiles", { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER, 10 | }, 11 | phone: { 12 | type: Sequelize.STRING, 13 | }, 14 | gender: { 15 | type: Sequelize.STRING, 16 | }, 17 | address: { 18 | type: Sequelize.STRING, 19 | }, 20 | image: { 21 | type: Sequelize.STRING, 22 | }, 23 | idUser: { 24 | type: Sequelize.INTEGER, 25 | references: { 26 | model: "users", 27 | key: "id", 28 | }, 29 | onUpdate: "CASCADE", 30 | onDelete: "CASCADE", 31 | }, 32 | createdAt: { 33 | allowNull: false, 34 | type: Sequelize.DATE, 35 | }, 36 | updatedAt: { 37 | allowNull: false, 38 | type: Sequelize.DATE, 39 | }, 40 | }); 41 | }, 42 | down: async (queryInterface, Sequelize) => { 43 | await queryInterface.dropTable("profiles"); 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /migrations/20210728051846-create-chat.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('chats', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | message: { 12 | type: Sequelize.TEXT 13 | }, 14 | idSender: { 15 | type: Sequelize.INTEGER, 16 | references: { 17 | model: "users", 18 | key: "id", 19 | }, 20 | onUpdate: "CASCADE", 21 | onDelete: "CASCADE", 22 | }, 23 | idRecipient: { 24 | type: Sequelize.INTEGER, 25 | references: { 26 | model: "users", 27 | key: "id", 28 | }, 29 | onUpdate: "CASCADE", 30 | onDelete: "CASCADE", 31 | }, 32 | createdAt: { 33 | allowNull: false, 34 | type: Sequelize.DATE 35 | }, 36 | updatedAt: { 37 | allowNull: false, 38 | type: Sequelize.DATE 39 | } 40 | }); 41 | }, 42 | down: async (queryInterface, Sequelize) => { 43 | await queryInterface.dropTable('chats'); 44 | } 45 | }; -------------------------------------------------------------------------------- /migrations/20210712032356-create-product.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('products', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | name: { 12 | type: Sequelize.STRING 13 | }, 14 | desc: { 15 | type: Sequelize.TEXT 16 | }, 17 | price: { 18 | type: Sequelize.INTEGER 19 | }, 20 | image: { 21 | type: Sequelize.STRING 22 | }, 23 | qty: { 24 | type: Sequelize.INTEGER 25 | }, 26 | idUser: { 27 | type: Sequelize.INTEGER, 28 | references: { 29 | model: "users", 30 | key: "id", 31 | }, 32 | onUpdate: "CASCADE", 33 | onDelete: "CASCADE", 34 | }, 35 | createdAt: { 36 | allowNull: false, 37 | type: Sequelize.DATE 38 | }, 39 | updatedAt: { 40 | allowNull: false, 41 | type: Sequelize.DATE 42 | } 43 | }); 44 | }, 45 | down: async (queryInterface, Sequelize) => { 46 | await queryInterface.dropTable('products'); 47 | } 48 | }; -------------------------------------------------------------------------------- /models/transaction.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { Model } = require("sequelize"); 3 | module.exports = (sequelize, DataTypes) => { 4 | class transaction extends Model { 5 | /** 6 | * Helper method for defining associations. 7 | * This method is not a part of Sequelize lifecycle. 8 | * The `models/index` file will call this method automatically. 9 | */ 10 | static associate(models) { 11 | // define association here 12 | transaction.belongsTo(models.product, { 13 | as: "product", 14 | foreignKey: { 15 | name: "idProduct", 16 | }, 17 | }); 18 | transaction.belongsTo(models.user, { 19 | as: "buyer", 20 | foreignKey: { 21 | name: "idBuyer", 22 | }, 23 | }); 24 | transaction.belongsTo(models.user, { 25 | as: "seller", 26 | foreignKey: { 27 | name: "idSeller", 28 | }, 29 | }); 30 | } 31 | } 32 | transaction.init( 33 | { 34 | idProduct: DataTypes.INTEGER, 35 | idBuyer: DataTypes.INTEGER, 36 | idSeller: DataTypes.INTEGER, 37 | price: DataTypes.INTEGER, 38 | status: DataTypes.STRING, 39 | }, 40 | { 41 | sequelize, 42 | modelName: "transaction", 43 | } 44 | ); 45 | return transaction; 46 | }; 47 | -------------------------------------------------------------------------------- /models/product.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { Model } = require("sequelize"); 3 | module.exports = (sequelize, DataTypes) => { 4 | class product extends Model { 5 | /** 6 | * Helper method for defining associations. 7 | * This method is not a part of Sequelize lifecycle. 8 | * The `models/index` file will call this method automatically. 9 | */ 10 | static associate(models) { 11 | // define association here 12 | product.belongsTo(models.user, { 13 | as: "user", 14 | foreignKey: { 15 | name: "idUser", 16 | }, 17 | }); 18 | 19 | product.hasMany(models.transaction, { 20 | as: "transactions", 21 | foreignKey: { 22 | name: "idProduct", 23 | }, 24 | }); 25 | 26 | // belongs to many category 27 | product.belongsToMany(models.category, { 28 | as: "categories", 29 | through: { 30 | model: "productCategory", 31 | as: "bridge", 32 | }, 33 | foreignKey: "idProduct", 34 | }); 35 | } 36 | } 37 | product.init( 38 | { 39 | name: DataTypes.STRING, 40 | desc: DataTypes.TEXT, 41 | price: DataTypes.INTEGER, 42 | image: DataTypes.STRING, 43 | qty: DataTypes.INTEGER, 44 | idUser: DataTypes.INTEGER, 45 | }, 46 | { 47 | sequelize, 48 | modelName: "product", 49 | } 50 | ); 51 | return product; 52 | }; 53 | -------------------------------------------------------------------------------- /migrations/20210712033433-create-transaction.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable("transactions", { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER, 10 | }, 11 | idProduct: { 12 | type: Sequelize.INTEGER, 13 | references: { 14 | model: "products", 15 | key: "id", 16 | }, 17 | onUpdate: "CASCADE", 18 | onDelete: "CASCADE", 19 | }, 20 | idBuyer: { 21 | type: Sequelize.INTEGER, 22 | references: { 23 | model: "users", 24 | key: "id", 25 | }, 26 | onUpdate: "CASCADE", 27 | onDelete: "CASCADE", 28 | }, 29 | idSeller: { 30 | type: Sequelize.INTEGER, 31 | references: { 32 | model: "users", 33 | key: "id", 34 | }, 35 | onUpdate: "CASCADE", 36 | onDelete: "CASCADE", 37 | }, 38 | price: { 39 | type: Sequelize.INTEGER, 40 | }, 41 | status: { 42 | type: Sequelize.STRING, 43 | }, 44 | createdAt: { 45 | allowNull: false, 46 | type: Sequelize.DATE, 47 | }, 48 | updatedAt: { 49 | allowNull: false, 50 | type: Sequelize.DATE, 51 | }, 52 | }); 53 | }, 54 | down: async (queryInterface, Sequelize) => { 55 | await queryInterface.dropTable("transactions"); 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { Model } = require("sequelize"); 3 | module.exports = (sequelize, DataTypes) => { 4 | class user extends Model { 5 | /** 6 | * Helper method for defining associations. 7 | * This method is not a part of Sequelize lifecycle. 8 | * The `models/index` file will call this method automatically. 9 | */ 10 | static associate(models) { 11 | // define association here 12 | user.hasOne(models.profile, { 13 | as: "profile", 14 | foreignKey: { 15 | name: "idUser", 16 | }, 17 | }); 18 | 19 | //hasMany to product model 20 | user.hasMany(models.product, { 21 | as: "products", 22 | foreignKey: { 23 | name: "idUser", 24 | }, 25 | }); 26 | 27 | //hasMany association to transaction model 28 | user.hasMany(models.transaction, { 29 | as: "buyerTransactions", 30 | foreignKey: { 31 | name: "idBuyer", 32 | }, 33 | }); 34 | user.hasMany(models.transaction, { 35 | as: "sellerTransactions", 36 | foreignKey: { 37 | name: "idSeller", 38 | }, 39 | }); 40 | 41 | //hasMany association to chat model 42 | user.hasMany(models.chat, { 43 | as: "senderMessage", 44 | foreignKey: { 45 | name: "idSender", 46 | }, 47 | }); 48 | user.hasMany(models.chat, { 49 | as: "recipientMessage", 50 | foreignKey: { 51 | name: "idRecipient", 52 | }, 53 | }); 54 | } 55 | } 56 | user.init( 57 | { 58 | email: DataTypes.STRING, 59 | password: DataTypes.STRING, 60 | name: DataTypes.STRING, 61 | status: DataTypes.STRING, 62 | }, 63 | { 64 | sequelize, 65 | modelName: "user", 66 | } 67 | ); 68 | return user; 69 | }; 70 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const router = express.Router(); 4 | 5 | // Controller 6 | const { 7 | addUsers, 8 | getUsers, 9 | getUser, 10 | updateUser, 11 | deleteUser, 12 | } = require("../controllers/user"); 13 | const { 14 | getProducts, 15 | getProduct, 16 | addProduct, 17 | updateProduct, 18 | deleteProduct, 19 | } = require("../controllers/product"); 20 | const { 21 | getTransactions, 22 | addTransaction, 23 | notification, 24 | } = require("../controllers/transaction"); 25 | const { 26 | getCategories, 27 | addCategory, 28 | updateCategory, 29 | getCategory, 30 | deleteCategory, 31 | } = require("../controllers/category"); 32 | const { getProfile } = require("../controllers/profile"); 33 | const { register, login, checkAuth } = require("../controllers/auth"); 34 | 35 | // Middleware 36 | const { auth } = require("../middlewares/auth"); 37 | const { uploadFile } = require("../middlewares/uploadFile"); 38 | 39 | // Route 40 | router.post("/user", addUsers); 41 | router.get("/users", getUsers); 42 | router.get("/user/:id", getUser); 43 | router.delete("/user/:id", updateUser); 44 | router.delete("/user/:id", deleteUser); 45 | 46 | router.get("/profile", auth, getProfile); 47 | 48 | router.get("/products", auth, getProducts); 49 | router.get("/product/:id", auth, getProduct); 50 | router.post("/product", auth, uploadFile("image"), addProduct); 51 | router.patch("/product/:id", auth, uploadFile("image"), updateProduct); 52 | router.delete("/product/:id", auth, deleteProduct); 53 | 54 | router.get("/transactions", auth, getTransactions); 55 | router.post("/transaction", auth, addTransaction); 56 | 57 | router.post("/notification", notification); 58 | 59 | router.get("/categories", getCategories); 60 | router.get("/category/:id", getCategory); 61 | router.post("/category", addCategory); 62 | router.patch("/category/:id", updateCategory); 63 | router.delete("/category/:id", deleteCategory); 64 | 65 | router.post("/register", register); 66 | router.post("/login", login); 67 | router.get("/check-auth", auth, checkAuth); 68 | 69 | module.exports = router; 70 | -------------------------------------------------------------------------------- /src/middlewares/uploadFile.js: -------------------------------------------------------------------------------- 1 | const multer = require("multer"); 2 | 3 | exports.uploadFile = (imageFile) => { 4 | // initialization multer diskstorage 5 | // make destination file for upload 6 | const storage = multer.diskStorage({ 7 | destination: function (req, file, cb) { 8 | cb(null, "uploads"); //file storage location 9 | }, 10 | filename: function (req, file, cb) { 11 | cb(null, Date.now() + "-" + file.originalname.replace(/\s/g, "")); // rename filename by date now + original filename 12 | }, 13 | }); 14 | 15 | // function for file filter based on extension 16 | const fileFilter = function (req, file, cb) { 17 | if (file.fieldname === imageFile) { 18 | if (!file.originalname.match(/\.(jpg|JPG|jpeg|JPEG|png|PNG|gif|GIF)$/)) { 19 | req.fileValidationError = { 20 | message: "Only image files are allowed!", 21 | }; 22 | return cb(new Error("Only image files are allowed!"), false); 23 | } 24 | } 25 | cb(null, true); 26 | }; 27 | 28 | const sizeInMB = 10; 29 | const maxSize = sizeInMB * 1000 * 1000; // Maximum file size in MB 30 | 31 | // generate multer instance for upload include storage, validation and max file size 32 | const upload = multer({ 33 | storage, 34 | fileFilter, 35 | limits: { 36 | fileSize: maxSize, 37 | }, 38 | }).single(imageFile); 39 | 40 | // middleware handler 41 | return (req, res, next) => { 42 | upload(req, res, function (err) { 43 | // show an error if validation failed 44 | if (req.fileValidationError) 45 | return res.status(400).send(req.fileValidationError); 46 | 47 | // show an error if file doesn't provided in req 48 | // if (!req.file && !err) 49 | // return res.status(400).send({ 50 | // message: "Please select files to upload", 51 | // }); 52 | 53 | // show an error if it exceeds the max size 54 | if (err) { 55 | if (err.code === "LIMIT_FILE_SIZE") { 56 | return res.status(400).send({ 57 | message: "Max file sized 10MB", 58 | }); 59 | } 60 | return res.status(400).send(err); 61 | } 62 | 63 | // if okay next to controller 64 | // in the controller we can access using req.file 65 | return next(); 66 | }); 67 | }; 68 | }; 69 | -------------------------------------------------------------------------------- /src/controllers/category.js: -------------------------------------------------------------------------------- 1 | const { category, productCategory } = require("../../models"); 2 | 3 | exports.getCategories = async (req, res) => { 4 | try { 5 | const data = await category.findAll({ 6 | attributes: { 7 | exclude: ["createdAt", "updatedAt"], 8 | }, 9 | }); 10 | 11 | res.send({ 12 | status: "success...", 13 | data, 14 | }); 15 | } catch (error) { 16 | console.log(error); 17 | res.status(500).send({ 18 | status: "failed", 19 | message: "Server Error", 20 | }); 21 | } 22 | }; 23 | 24 | exports.getCategory = async (req, res) => { 25 | try { 26 | const { id } = req.params; 27 | const data = await category.findOne({ 28 | where: { 29 | id, 30 | }, 31 | attributes: { 32 | exclude: ["createdAt", "updatedAt"], 33 | }, 34 | }); 35 | 36 | res.send({ 37 | status: "success...", 38 | data, 39 | }); 40 | } catch (error) { 41 | console.log(error); 42 | res.status(500).send({ 43 | status: "failed", 44 | message: "Server Error", 45 | }); 46 | } 47 | }; 48 | 49 | exports.addCategory = async (req, res) => { 50 | try { 51 | const newCategory = await category.create(req.body); 52 | 53 | res.send({ 54 | status: "success...", 55 | data: { 56 | id: newCategory.id, 57 | name: newCategory.name, 58 | }, 59 | }); 60 | } catch (error) { 61 | console.log(error); 62 | res.status(500).send({ 63 | status: "failed", 64 | message: "Server Error", 65 | }); 66 | } 67 | }; 68 | 69 | exports.updateCategory = async (req, res) => { 70 | try { 71 | const { id } = req.params; 72 | const newCategory = await category.update(req.body, { 73 | where: { 74 | id, 75 | }, 76 | }); 77 | 78 | res.send({ 79 | status: "success...", 80 | data: { 81 | id: newCategory.id, 82 | name: newCategory.name, 83 | }, 84 | }); 85 | } catch (error) { 86 | console.log(error); 87 | res.status(500).send({ 88 | status: "failed", 89 | message: "Server Error", 90 | }); 91 | } 92 | }; 93 | 94 | exports.deleteCategory = async (req, res) => { 95 | try { 96 | const { id } = req.params; 97 | 98 | await category.destroy({ 99 | where: { 100 | id, 101 | }, 102 | }); 103 | 104 | await productCategory.destroy({ 105 | where: { 106 | idCategory: id, 107 | }, 108 | }); 109 | 110 | res.send({ 111 | status: "success", 112 | message: `Delete category id: ${id} finished`, 113 | }); 114 | } catch (error) { 115 | console.log(error); 116 | res.status(500).send({ 117 | status: "failed", 118 | message: "Server Error", 119 | }); 120 | } 121 | }; 122 | -------------------------------------------------------------------------------- /src/controllers/user.js: -------------------------------------------------------------------------------- 1 | const { user, profile } = require("../../models"); 2 | 3 | exports.addUsers = async (req, res) => { 4 | try { 5 | await user.create(req.body); 6 | 7 | res.send({ 8 | status: "success", 9 | message: "Add user finished", 10 | }); 11 | } catch (error) { 12 | console.log(error); 13 | res.send({ 14 | status: "failed", 15 | message: "Server Error", 16 | }); 17 | } 18 | }; 19 | 20 | exports.getUsers = async (req, res) => { 21 | try { 22 | const users = await user.findAll({ 23 | include: { 24 | model: profile, 25 | as: "profile", 26 | attributes: { 27 | exclude: ["createdAt", "updatedAt", "idUser"], 28 | }, 29 | }, 30 | attributes: { 31 | exclude: ["password", "createdAt", "updatedAt"], 32 | }, 33 | }); 34 | 35 | res.send({ 36 | status: "success", 37 | data: { 38 | users, 39 | }, 40 | }); 41 | } catch (error) { 42 | console.log(error); 43 | res.send({ 44 | status: "failed", 45 | message: "Server Error", 46 | }); 47 | } 48 | }; 49 | 50 | exports.getUser = async (req, res) => { 51 | try { 52 | const { id } = req.params; 53 | 54 | const data = await user.findOne({ 55 | where: { 56 | id, 57 | }, 58 | include: { 59 | model: profile, 60 | as: "profile", 61 | attributes: { 62 | exclude: ["createdAt", "updatedAt", "idUser"], 63 | }, 64 | }, 65 | attributes: { 66 | exclude: ["password", "createdAt", "updatedAt"], 67 | }, 68 | }); 69 | 70 | res.send({ 71 | status: "success", 72 | data: { 73 | user: data, 74 | }, 75 | }); 76 | } catch (error) { 77 | console.log(error); 78 | res.send({ 79 | status: "failed", 80 | message: "Server Error", 81 | }); 82 | } 83 | }; 84 | 85 | exports.updateUser = async (req, res) => { 86 | try { 87 | const { id } = req.params; 88 | 89 | await user.update(req.body, { 90 | where: { 91 | id, 92 | }, 93 | }); 94 | 95 | res.send({ 96 | status: "success", 97 | message: `Update user id: ${id} finished`, 98 | data: req.body, 99 | }); 100 | } catch (error) { 101 | console.log(error); 102 | res.send({ 103 | status: "failed", 104 | message: "Server Error", 105 | }); 106 | } 107 | }; 108 | 109 | exports.deleteUser = async (req, res) => { 110 | try { 111 | const { id } = req.params; 112 | 113 | await user.destroy({ 114 | where: { 115 | id, 116 | }, 117 | }); 118 | 119 | res.send({ 120 | status: "success", 121 | message: `Delete user id: ${id} finished`, 122 | }); 123 | } catch (error) { 124 | console.log(error); 125 | res.send({ 126 | status: "failed", 127 | message: "Server Error", 128 | }); 129 | } 130 | }; 131 | -------------------------------------------------------------------------------- /src/controllers/auth.js: -------------------------------------------------------------------------------- 1 | // import model 2 | const { user } = require("../../models"); 3 | 4 | // import joi validation 5 | const Joi = require("joi"); 6 | // import bcrypt 7 | const bcrypt = require("bcrypt"); 8 | //import jsonwebtoken 9 | const jwt = require("jsonwebtoken"); 10 | 11 | exports.register = async (req, res) => { 12 | // our validation schema here 13 | const schema = Joi.object({ 14 | name: Joi.string().min(3).required(), 15 | email: Joi.string().email().min(6).required(), 16 | password: Joi.string().min(6).required(), 17 | }); 18 | 19 | // do validation and get error object from schema.validate 20 | const { error } = schema.validate(req.body); 21 | 22 | // if error exist send validation error message 23 | if (error) 24 | return res.status(400).send({ 25 | error: { 26 | message: error.details[0].message, 27 | }, 28 | }); 29 | 30 | try { 31 | // we generate salt (random value) with 10 rounds 32 | const salt = await bcrypt.genSalt(10); 33 | // we hash password from request with salt 34 | const hashedPassword = await bcrypt.hash(req.body.password, salt); 35 | 36 | const newUser = await user.create({ 37 | name: req.body.name, 38 | email: req.body.email, 39 | password: hashedPassword, 40 | status: "customer", 41 | }); 42 | 43 | // generate token 44 | const token = jwt.sign({ id: user.id }, process.env.TOKEN_KEY); 45 | 46 | res.status(200).send({ 47 | status: "success", 48 | data: { 49 | name: newUser.name, 50 | email: newUser.email, 51 | token, 52 | }, 53 | }); 54 | } catch (error) { 55 | console.log(error); 56 | res.status(500).send({ 57 | status: "failed", 58 | message: "Server Error", 59 | }); 60 | } 61 | }; 62 | 63 | exports.login = async (req, res) => { 64 | // our validation schema here 65 | const schema = Joi.object({ 66 | email: Joi.string().email().min(6).required(), 67 | password: Joi.string().min(6).required(), 68 | }); 69 | 70 | // do validation and get error object from schema.validate 71 | const { error } = schema.validate(req.body); 72 | 73 | // if error exist send validation error message 74 | if (error) 75 | return res.status(400).send({ 76 | error: { 77 | message: error.details[0].message, 78 | }, 79 | }); 80 | 81 | try { 82 | const userExist = await user.findOne({ 83 | where: { 84 | email: req.body.email, 85 | }, 86 | attributes: { 87 | exclude: ["createdAt", "updatedAt"], 88 | }, 89 | }); 90 | // compare password between entered from client and from database 91 | const isValid = await bcrypt.compare(req.body.password, userExist.password); 92 | 93 | // check if not valid then return response with status 400 (bad request) 94 | if (!isValid) { 95 | return res.status(400).send({ 96 | status: "failed", 97 | message: "credential is invalid", 98 | }); 99 | } 100 | 101 | // generate token 102 | const token = jwt.sign({ id: userExist.id }, process.env.TOKEN_KEY); 103 | 104 | res.status(200).send({ 105 | status: "success", 106 | data: { 107 | id: userExist.id, 108 | name: userExist.name, 109 | email: userExist.email, 110 | status: userExist.status, 111 | token, 112 | }, 113 | }); 114 | } catch (error) { 115 | console.log(error); 116 | res.status(500).send({ 117 | status: "failed", 118 | message: "Server Error", 119 | }); 120 | } 121 | }; 122 | 123 | exports.checkAuth = async (req, res) => { 124 | try { 125 | const id = req.user.id; 126 | 127 | const dataUser = await user.findOne({ 128 | where: { 129 | id, 130 | }, 131 | attributes: { 132 | exclude: ["createdAt", "updatedAt", "password"], 133 | }, 134 | }); 135 | 136 | if (!dataUser) { 137 | return res.status(404).send({ 138 | status: "failed", 139 | }); 140 | } 141 | 142 | res.send({ 143 | status: "success", 144 | data: { 145 | user: { 146 | id: dataUser.id, 147 | name: dataUser.name, 148 | email: dataUser.email, 149 | status: dataUser.status, 150 | }, 151 | }, 152 | }); 153 | } catch (error) { 154 | console.log(error); 155 | res.status({ 156 | status: "failed", 157 | message: "Server Error", 158 | }); 159 | } 160 | }; 161 | -------------------------------------------------------------------------------- /src/socket/index.js: -------------------------------------------------------------------------------- 1 | // import models 2 | const { chat, user, profile } = require("../../models"); 3 | // import jsonwebtoken 4 | const jwt = require("jsonwebtoken"); 5 | 6 | // import sequelize operator 7 | // https://sequelize.org/master/manual/model-querying-basics.html#operators 8 | const { Op } = require("sequelize"); 9 | 10 | const connectedUser = {}; 11 | const socketIo = (io) => { 12 | // create middlewares to prevent client without token to access socket server 13 | io.use((socket, next) => { 14 | if (socket.handshake.auth && socket.handshake.auth.token) { 15 | next(); 16 | } else { 17 | next(new Error("Not Authorized")); 18 | } 19 | }); 20 | 21 | io.on("connection", async (socket) => { 22 | console.log("client connect: ", socket.id); 23 | 24 | // get user connected id 25 | const userId = socket.handshake.query.id; 26 | 27 | // save to connectedUser 28 | connectedUser[userId] = socket.id; 29 | 30 | // define listener on event load admin contact 31 | socket.on("load admin contact", async () => { 32 | try { 33 | const adminContact = await user.findOne({ 34 | include: [ 35 | { 36 | model: profile, 37 | as: "profile", 38 | attributes: { 39 | exclude: ["createdAt", "updatedAt"], 40 | }, 41 | }, 42 | ], 43 | where: { 44 | status: "admin", 45 | }, 46 | attributes: { 47 | exclude: ["createdAt", "updatedAt", "password"], 48 | }, 49 | }); 50 | 51 | socket.emit("admin contact", adminContact); 52 | } catch (err) { 53 | console.log(err); 54 | } 55 | }); 56 | 57 | // define listener on event load customer contact 58 | socket.on("load customer contacts", async () => { 59 | try { 60 | let customerContacts = await user.findAll({ 61 | include: [ 62 | { 63 | model: profile, 64 | as: "profile", 65 | attributes: { 66 | exclude: ["createdAt", "updatedAt"], 67 | }, 68 | }, 69 | { 70 | model: chat, 71 | as: "recipientMessage", 72 | attributes: { 73 | exclude: ["createdAt", "updatedAt", "idRecipient", "idSender"], 74 | }, 75 | }, 76 | { 77 | model: chat, 78 | as: "senderMessage", 79 | attributes: { 80 | exclude: ["createdAt", "updatedAt", "idRecipient", "idSender"], 81 | }, 82 | }, 83 | ], 84 | attributes: { 85 | exclude: ["createdAt", "updatedAt", "password"], 86 | }, 87 | }); 88 | 89 | customerContacts = JSON.parse(JSON.stringify(customerContacts)); 90 | customerContacts = customerContacts.map((item) => ({ 91 | ...item, 92 | profile: { 93 | ...item.profile, 94 | image: item.profile?.image 95 | ? process.env.PATH_FILE + item.profile?.image 96 | : null, 97 | }, 98 | })); 99 | 100 | socket.emit("customer contacts", customerContacts); 101 | } catch (err) { 102 | console.log(err); 103 | } 104 | }); 105 | 106 | // define listener on event load messages 107 | socket.on("load messages", async (payload) => { 108 | console.log("load messages", payload); 109 | try { 110 | const token = socket.handshake.auth.token; 111 | 112 | const tokenKey = process.env.TOKEN_KEY; 113 | const verified = jwt.verify(token, tokenKey); 114 | 115 | const idRecipient = payload; // catch recipient id sent from client 116 | const idSender = verified.id; //id user 117 | 118 | const data = await chat.findAll({ 119 | where: { 120 | idSender: { 121 | [Op.or]: [idRecipient, idSender], 122 | }, 123 | idRecipient: { 124 | [Op.or]: [idRecipient, idSender], 125 | }, 126 | }, 127 | include: [ 128 | { 129 | model: user, 130 | as: "recipient", 131 | attributes: { 132 | exclude: ["createdAt", "updatedAt", "password"], 133 | }, 134 | }, 135 | { 136 | model: user, 137 | as: "sender", 138 | attributes: { 139 | exclude: ["createdAt", "updatedAt", "password"], 140 | }, 141 | }, 142 | ], 143 | order: [["createdAt", "ASC"]], 144 | attributes: { 145 | exclude: ["createdAt", "updatedAt", "idRecipient", "idSender"], 146 | }, 147 | }); 148 | 149 | socket.emit("messages", data); 150 | } catch (error) { 151 | console.log(error); 152 | } 153 | }); 154 | 155 | // define listener on event send message 156 | socket.on("send message", async (payload) => { 157 | try { 158 | const token = socket.handshake.auth.token; 159 | 160 | const tokenKey = process.env.TOKEN_KEY; 161 | const verified = jwt.verify(token, tokenKey); 162 | 163 | const idSender = verified.id; //id user 164 | const { message, idRecipient } = payload; // catch recipient id and message sent from client 165 | 166 | await chat.create({ 167 | message, 168 | idRecipient, 169 | idSender, 170 | }); 171 | 172 | // emit to just sender and recipient default rooms by their socket id 173 | io.to(socket.id) 174 | .to(connectedUser[idRecipient]) 175 | .emit("new message", idRecipient); 176 | } catch (error) { 177 | console.log(error); 178 | } 179 | }); 180 | 181 | socket.on("disconnect", () => { 182 | console.log("client disconnected", socket.id); 183 | delete connectedUser[userId]; 184 | }); 185 | }); 186 | }; 187 | 188 | module.exports = socketIo; 189 | -------------------------------------------------------------------------------- /src/controllers/product.js: -------------------------------------------------------------------------------- 1 | const { product, user, category, productCategory } = require("../../models"); 2 | const cloudinary = require('../../utils/cloudinary'); 3 | 4 | exports.getProducts = async (req, res) => { 5 | try { 6 | let data = await product.findAll({ 7 | include: [ 8 | { 9 | model: user, 10 | as: "user", 11 | attributes: { 12 | exclude: ["createdAt", "updatedAt", "password"], 13 | }, 14 | }, 15 | { 16 | model: category, 17 | as: "categories", 18 | through: { 19 | model: productCategory, 20 | as: "bridge", 21 | attributes: [], 22 | }, 23 | attributes: { 24 | exclude: ["createdAt", "updatedAt"], 25 | }, 26 | }, 27 | ], 28 | attributes: { 29 | exclude: ["createdAt", "updatedAt", "idUser"], 30 | }, 31 | }); 32 | 33 | data = JSON.parse(JSON.stringify(data)); 34 | 35 | data = data.map((item) => { 36 | return { ...item, image: process.env.PATH_FILE + item.image }; 37 | }); 38 | 39 | res.send({ 40 | status: "success...", 41 | data, 42 | }); 43 | } catch (error) { 44 | console.log(error); 45 | res.send({ 46 | status: "failed", 47 | message: "Server Error", 48 | }); 49 | } 50 | }; 51 | 52 | exports.getProduct = async (req, res) => { 53 | try { 54 | const { id } = req.params; 55 | let data = await product.findOne({ 56 | where: { 57 | id, 58 | }, 59 | include: [ 60 | { 61 | model: user, 62 | as: "user", 63 | attributes: { 64 | exclude: ["createdAt", "updatedAt", "password"], 65 | }, 66 | }, 67 | { 68 | model: category, 69 | as: "categories", 70 | through: { 71 | model: productCategory, 72 | as: "bridge", 73 | attributes: [], 74 | }, 75 | attributes: { 76 | exclude: ["createdAt", "updatedAt"], 77 | }, 78 | }, 79 | ], 80 | attributes: { 81 | exclude: ["createdAt", "updatedAt", "idUser"], 82 | }, 83 | }); 84 | 85 | data = JSON.parse(JSON.stringify(data)); 86 | 87 | data = { 88 | ...data, 89 | image: process.env.PATH_FILE + data.image, 90 | }; 91 | 92 | res.send({ 93 | status: "success...", 94 | data, 95 | }); 96 | } catch (error) { 97 | console.log(error); 98 | res.send({ 99 | status: "failed", 100 | message: "Server Error", 101 | }); 102 | } 103 | }; 104 | 105 | exports.addProduct = async (req, res) => { 106 | try { 107 | let { categoryId } = req.body; 108 | categoryId = categoryId.split(","); 109 | 110 | const result = await cloudinary.uploader.upload(req.file.path, { 111 | folder: 'dumbmerch_file', 112 | use_filename: true, 113 | unique_filename: false, 114 | }); 115 | 116 | const data = { 117 | name: req.body.name, 118 | desc: req.body.desc, 119 | price: req.body.price, 120 | image: result.public_id, 121 | qty: req.body.qty, 122 | idUser: req.user.id, 123 | }; 124 | 125 | let newProduct = await product.create(data); 126 | 127 | const productCategoryData = categoryId.map((item) => { 128 | return { idProduct: newProduct.id, idCategory: parseInt(item) }; 129 | }); 130 | 131 | await productCategory.bulkCreate(productCategoryData); 132 | 133 | let productData = await product.findOne({ 134 | where: { 135 | id: newProduct.id, 136 | }, 137 | include: [ 138 | { 139 | model: user, 140 | as: "user", 141 | attributes: { 142 | exclude: ["createdAt", "updatedAt", "password"], 143 | }, 144 | }, 145 | { 146 | model: category, 147 | as: "categories", 148 | through: { 149 | model: productCategory, 150 | as: "bridge", 151 | attributes: [], 152 | }, 153 | attributes: { 154 | exclude: ["createdAt", "updatedAt"], 155 | }, 156 | }, 157 | ], 158 | attributes: { 159 | exclude: ["createdAt", "updatedAt", "idUser"], 160 | }, 161 | }); 162 | productData = JSON.parse(JSON.stringify(productData)); 163 | 164 | res.send({ 165 | status: "success...", 166 | data: { 167 | ...productData, 168 | image: process.env.PATH_FILE + productData.image, 169 | }, 170 | }); 171 | } catch (error) { 172 | console.log(error); 173 | res.status(500).send({ 174 | status: "failed", 175 | message: "Server Error", 176 | }); 177 | } 178 | }; 179 | 180 | exports.updateProduct = async (req, res) => { 181 | try { 182 | const { id } = req.params; 183 | let { categoryId } = req.body; 184 | categoryId = await categoryId.split(","); 185 | 186 | const data = { 187 | name: req?.body?.name, 188 | desc: req?.body.desc, 189 | price: req?.body?.price, 190 | image: req?.file?.filename, 191 | qty: req?.body?.qty, 192 | idUser: req?.user?.id, 193 | }; 194 | 195 | await productCategory.destroy({ 196 | where: { 197 | idProduct: id, 198 | }, 199 | }); 200 | 201 | let productCategoryData = []; 202 | if (categoryId != 0 && categoryId[0] != "") { 203 | productCategoryData = categoryId.map((item) => { 204 | return { idProduct: parseInt(id), idCategory: parseInt(item) }; 205 | }); 206 | } 207 | 208 | if (productCategoryData.length != 0) { 209 | await productCategory.bulkCreate(productCategoryData); 210 | } 211 | 212 | await product.update(data, { 213 | where: { 214 | id, 215 | }, 216 | }); 217 | 218 | res.send({ 219 | status: "success", 220 | data: { 221 | id, 222 | data, 223 | productCategoryData, 224 | image: req?.file?.filename, 225 | }, 226 | }); 227 | } catch (error) { 228 | console.log(error); 229 | res.send({ 230 | status: "failed", 231 | message: "Server Error", 232 | }); 233 | } 234 | }; 235 | 236 | exports.deleteProduct = async (req, res) => { 237 | try { 238 | const { id } = req.params; 239 | 240 | await product.destroy({ 241 | where: { 242 | id, 243 | }, 244 | }); 245 | 246 | await productCategory.destroy({ 247 | where: { 248 | idProduct: id, 249 | }, 250 | }); 251 | 252 | res.send({ 253 | status: "success", 254 | message: `Delete product id: ${id} finished`, 255 | }); 256 | } catch (error) { 257 | console.log(error); 258 | res.send({ 259 | status: "failed", 260 | message: "Server Error", 261 | }); 262 | } 263 | }; 264 | -------------------------------------------------------------------------------- /src/controllers/transaction.js: -------------------------------------------------------------------------------- 1 | const { user, transaction, product, profile } = require("../../models"); 2 | const midtransClient = require("midtrans-client"); 3 | 4 | const convertRupiah = require("rupiah-format"); 5 | 6 | // Import nodemailer here ... 7 | const nodemailer = require("nodemailer") 8 | 9 | exports.getTransactions = async (req, res) => { 10 | try { 11 | const idBuyer = req.user.id; 12 | let data = await transaction.findAll({ 13 | where: { 14 | idBuyer, 15 | }, 16 | order: [["createdAt", "DESC"]], 17 | attributes: { 18 | exclude: ["updatedAt", "idBuyer", "idSeller", "idProduct"], 19 | }, 20 | include: [ 21 | { 22 | model: product, 23 | as: "product", 24 | attributes: { 25 | exclude: [ 26 | "createdAt", 27 | "updatedAt", 28 | "idUser", 29 | "qty", 30 | "price", 31 | "desc", 32 | ], 33 | }, 34 | }, 35 | { 36 | model: user, 37 | as: "buyer", 38 | attributes: { 39 | exclude: ["createdAt", "updatedAt", "password", "status"], 40 | }, 41 | }, 42 | { 43 | model: user, 44 | as: "seller", 45 | attributes: { 46 | exclude: ["createdAt", "updatedAt", "password", "status"], 47 | }, 48 | }, 49 | ], 50 | }); 51 | 52 | data = JSON.parse(JSON.stringify(data)); 53 | 54 | data = data.map((item) => { 55 | return { 56 | ...item, 57 | product: { 58 | ...item.product, 59 | image: process.env.PATH_FILE + item.product.image, 60 | }, 61 | }; 62 | }); 63 | 64 | res.send({ 65 | status: "success", 66 | data, 67 | }); 68 | } catch (error) { 69 | console.log(error); 70 | res.send({ 71 | status: "failed", 72 | message: "Server Error", 73 | }); 74 | } 75 | }; 76 | 77 | exports.addTransaction = async (req, res) => { 78 | try { 79 | let data = req.body; 80 | 81 | data = { 82 | id: parseInt(data.idProduct + Math.random().toString().slice(3, 8)), 83 | ...data, 84 | idBuyer: req.user.id, 85 | status: "pending", 86 | }; 87 | 88 | // Insert data to transaction table 89 | const newData = await transaction.create(data); 90 | 91 | const buyerData = await user.findOne({ 92 | include: { 93 | model: profile, 94 | as: "profile", 95 | attributes: { 96 | exclude: ["createdAt", "updatedAt", "idUser"], 97 | }, 98 | }, 99 | where: { 100 | id: newData.idBuyer, 101 | }, 102 | attributes: { 103 | exclude: ["createdAt", "updatedAt", "password"], 104 | }, 105 | }); 106 | 107 | // Create Snap API instance 108 | let snap = new midtransClient.Snap({ 109 | // Set to true if you want Production Environment (accept real transaction). 110 | isProduction: false, 111 | serverKey: process.env.MIDTRANS_SERVER_KEY, 112 | }); 113 | 114 | let parameter = { 115 | transaction_details: { 116 | order_id: newData.id, 117 | gross_amount: newData.price, 118 | }, 119 | credit_card: { 120 | secure: true, 121 | }, 122 | customer_details: { 123 | full_name: buyerData?.name, 124 | email: buyerData?.email, 125 | phone: buyerData?.profile?.phone, 126 | }, 127 | }; 128 | 129 | const payment = await snap.createTransaction(parameter); 130 | 131 | res.send({ 132 | status: "pending", 133 | message: "Pending transaction payment gateway", 134 | payment, 135 | product: { 136 | id: data.idProduct, 137 | }, 138 | }); 139 | } catch (error) { 140 | console.log(error); 141 | res.send({ 142 | status: "failed", 143 | message: "Server Error", 144 | }); 145 | } 146 | }; 147 | 148 | const MIDTRANS_CLIENT_KEY = process.env.MIDTRANS_CLIENT_KEY; 149 | const MIDTRANS_SERVER_KEY = process.env.MIDTRANS_SERVER_KEY; 150 | 151 | const core = new midtransClient.CoreApi(); 152 | 153 | core.apiConfig.set({ 154 | isProduction: false, 155 | serverKey: MIDTRANS_SERVER_KEY, 156 | clientKey: MIDTRANS_CLIENT_KEY, 157 | }); 158 | 159 | /** 160 | * Handle update transaction status after notification 161 | * from midtrans webhook 162 | * @param {string} status 163 | * @param {transactionId} transactionId 164 | */ 165 | 166 | exports.notification = async (req, res) => { 167 | try { 168 | const statusResponse = await core.transaction.notification(req.body); 169 | const orderId = statusResponse.order_id; 170 | const transactionStatus = statusResponse.transaction_status; 171 | const fraudStatus = statusResponse.fraud_status; 172 | 173 | if (transactionStatus == "capture") { 174 | if (fraudStatus == "challenge") { 175 | // TODO set transaction status on your database to 'challenge' 176 | // and response with 200 OK 177 | sendEmail("pending", orderId); //sendEmail with status pending and order id 178 | handleTransaction("pending", orderId); 179 | res.status(200); 180 | } else if (fraudStatus == "accept") { 181 | // TODO set transaction status on your database to 'success' 182 | // and response with 200 OK 183 | sendEmail("success", orderId); //sendEmail with status success and order id 184 | updateProduct(orderId); 185 | handleTransaction("success", orderId); 186 | res.status(200); 187 | } 188 | } else if (transactionStatus == "settlement") { 189 | // TODO set transaction status on your database to 'success' 190 | // and response with 200 OK 191 | sendEmail("success", orderId); //sendEmail with status success and order id 192 | updateProduct(orderId); 193 | handleTransaction("success", orderId); 194 | res.status(200); 195 | } else if ( 196 | transactionStatus == "cancel" || 197 | transactionStatus == "deny" || 198 | transactionStatus == "expire" 199 | ) { 200 | // TODO set transaction status on your database to 'failure' 201 | // and response with 200 OK 202 | sendEmail("failed", orderId); //sendEmail with status failed and order id 203 | handleTransaction("failed", orderId); 204 | res.status(200); 205 | } else if (transactionStatus == "pending") { 206 | // TODO set transaction status on your database to 'pending' / waiting payment 207 | // and response with 200 OK 208 | sendEmail("pending", orderId); //sendEmail with status pending and order id 209 | handleTransaction("pending", orderId); 210 | res.status(200); 211 | } 212 | } catch (error) { 213 | console.log(error); 214 | res.status(500); 215 | } 216 | }; 217 | 218 | const handleTransaction = async (status, transactionId) => { 219 | await transaction.update( 220 | { 221 | status, 222 | }, 223 | { 224 | where: { 225 | id: transactionId, 226 | }, 227 | } 228 | ); 229 | }; 230 | 231 | const updateProduct = async (orderId) => { 232 | const transactionData = await transaction.findOne({ 233 | where: { 234 | id: orderId, 235 | }, 236 | }); 237 | const productData = await product.findOne({ 238 | where: { 239 | id: transactionData.idProduct, 240 | }, 241 | }); 242 | const qty = productData.qty - 1; 243 | await product.update({ qty }, { where: { id: productData.id } }); 244 | }; 245 | 246 | //Create function recive two params (status, orderId) for handle send email .... 247 | const sendEmail = async (status, transactionId) => { 248 | // Config service and email account 249 | const transporter = nodemailer.createTransport({ 250 | service: "gmail", 251 | auth: { 252 | user: process.env.SYSTEM_EMAIL, 253 | pass: process.env.SYSTEM_PASSWORD, 254 | }, 255 | }); 256 | 257 | // Get transaction data 258 | let data = await transaction.findOne({ 259 | where: { 260 | id: transactionId, 261 | }, 262 | attributes: { 263 | exclude: ["createdAt", "updatedAt", "password"], 264 | }, 265 | include: [ 266 | { 267 | model: user, 268 | as: "buyer", 269 | attributes: { 270 | exclude: ["createdAt", "updatedAt", "password", "status"], 271 | }, 272 | }, 273 | { 274 | model: product, 275 | as: "product", 276 | attributes: { 277 | exclude: [ 278 | "createdAt", 279 | "updatedAt", 280 | "idUser", 281 | "qty", 282 | "price", 283 | "desc", 284 | ], 285 | }, 286 | }, 287 | ], 288 | }); 289 | 290 | data = JSON.parse(JSON.stringify(data)); 291 | 292 | // Email options content 293 | const mailOptions = { 294 | from: process.env.SYSTEM_EMAIL, 295 | to: data.buyer.email, 296 | subject: "Payment status", 297 | text: "Your payment is
" + status, 298 | html: ` 299 | 300 | 301 | 302 | 303 | 304 | Document 305 | 310 | 311 | 312 |

Product payment :

313 | 318 | 319 | `, 320 | }; 321 | 322 | // Send an email if there is a change in the transaction status 323 | if (data.status != status) { 324 | transporter.sendMail(mailOptions, (err, info) => { 325 | if (err) throw err; 326 | console.log("Email sent: " + info.response); 327 | 328 | return res.send({ 329 | status: "Success", 330 | message: info.response, 331 | }); 332 | }); 333 | } 334 | }; 335 | --------------------------------------------------------------------------------