├── 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 |