Practice React Ecommerce - Back End

Express

install

  • express
  • dotenv : env 設定
  • nodemon : node 修改自動重啟
1
2
3
4
# npm init
npm init -y
# install module
npm install express dotenv nodemon

simple example

.env
1
PORT=8000
.gitignore
1
2
node_modules
.env
package.json - script
1
2
3
"scripts": {
"start": "nodemon app.js"
},
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
const express = require("express");
const app = express();
require("dotenv").config();

app.get("/", (req, res) => {
res.send("hello from node !!");
});

const port = process.env.PORT || 8080;

app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
1
npm install mongodb

change to routes middleware

./app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ./app.js
const express = require("express");
const app = express();
require("dotenv").config();
// import routes
// const userRoutes = require("./routes/user");
const userRoutes = require("./routes/user");

// routes middleware
app.use(userRoutes);

const port = process.env.PORT || 8080;

app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
./routes/user.js
1
2
3
4
5
6
7
8
9
// ./routes/user.js
const express = require("express");
const router = express.Router();

router.get("/", (req, res) => {
res.send("hello from routers's user.js");
});

module.exports = router;

connect mongoDB altas

.env
1
2
PORT=8000
DATABASE=mongodb://robert2:{password}@cluster0-shard-00-00.bscvu.mongodb.net:27017,cluster0-shard-00-01.bscvu.mongodb.net:27017,cluster0-shard-00-02.bscvu.mongodb.net:27017/myFirstDatabase?ssl=true&replicaSet=atlas-11cyyj-shard-0&authSource=admin&retryWrites=true&w=majority
./app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ./app.js
const express = require("express");
const app = express();
require("dotenv").config();
// connect mangoDB altas
// using 2.2.12 or later's uri
const mongoose = require("mongoose");

// import routes
// const userRoutes = require("./routes/user");
const userRoutes = require("./routes/user");

// connect mangoDB altas
// using 2.2.12 or later's uri
mongoose
.connect(process.env.DATABASE, {
useNewUrlParser: true,
useUnifiedTopology: true,
// useCreateIndex not support - 課程示範
// useCreateIndex: true,
})
.then(() => {
console.log("MongoDB Connected…");
})
.catch((err) => console.log(err));

// routes middleware
app.use("/api", userRoutes);

const port = process.env.PORT || 8080;

app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});

分離出 controller

./routes/user.js
1
2
3
4
5
6
7
8
9
// ./routes/user.js
const express = require("express");
const router = express.Router();
// add controller
const { sayHi } = require("../controllers/user");

router.get("/", sayHi);

module.exports = router;
./controller/user.js
1
2
3
4
5
// ./controller/user.js
// http://localhost:8000/api --> {"message":"hello there!"}
exports.sayHi = (req, res) => {
res.json({ message: "hello there!" });
};

user API

install
1
2
3
4
5
6
7
8
9
npm install uuid
# body-parser 被標記為棄用, 使用 express 之 app.use(express.json());
# npm install body-parser
npm install morgan
npm i cookie-parser
# version 6 會有 error : TypeError: expressValidator is not a function
# 使用 version 5
npm i express-validator@5.3.1
npm i express-jwt jsonwebtoken
./app.js : program entry
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// ./app.js
const express = require("express");
// connect mangoDB altas
// using 2.2.12 or later's uri
const mongoose = require("mongoose");
// import routes
const userRoutes = require("./routes/user");
// import morgan
const morgan = require("morgan");
// cookie-parser
// const cookieParser = require("cookie-parser");
// express-validator
const expressValidator = require("express-validator");
// env
require("dotenv").config();

// app
const app = express();

// connect mangoDB altas
// using 2.2.12 or later's uri
mongoose
.connect(process.env.DATABASE, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log("MongoDB Connected…");
})
.catch((err) => console.log(err));

// middlewares
app.use(morgan("dev")); // morgan - http request log
app.use(express.json()); // body parser
// app.use(cookieParser()); // cookie-parser
app.use(expressValidator()); // express-validator

// routes middleware
app.use("/api", userRoutes);

const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
./routes/user.js : route
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ./routes/user.js
const express = require("express");
const router = express.Router();
// add controller
const { signup, signin, signout } = require("../controllers/user");
// valid
const { userSignupValidator } = require("../validator");

// 註冊加驗證測試
router.post("/signup", userSignupValidator, signup);
router.post("/signin", signin);
router.get("/signout", signout);

module.exports = router;
./validator/index.js - valid field
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ./validator/index.js
exports.userSignupValidator = (req, res, next) => {
req.check("name", "Name is required").notEmpty();
req
.check("email", "Email must be between 6 to 32 characters")
.matches(/.+\@.+\..+/)
.withMessage("Email must contain @ and .")
.isLength({
min: 6,
max: 32,
});
req.check("password", "Password is required").notEmpty();
req
.check("password")
.isLength({ min: 6 })
.withMessage("password must conatin at least 6 characters")
.matches(/\d/)
.withMessage("Password must contain a number");
const errors = req.validationErrors();
// console.log(errors);
if (errors) {
const firstError = errors.map((error) => error.msg)[0];
return res.status(400).json({ error: firstError });
}
next();
};
./controller/user.js - control every page
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// ./controller/user.js
const User = require("../models/user");
const { errorHandler } = require("../helpers/dbErrorHandler");
// jwt
const jwt = require("jsonwebtoken"); // to generate signed token
// const express = require("express-jwt"); // for authorization check

exports.signup = (req, res) => {
console.log("req.body", req.body);
const user = new User(req.body);
user.save((err, user) => {
if (err) {
return res.status(400).json({
err: errorHandler(err),
});
}

res.json({
user,
});
});
};

exports.signin = (req, res) => {
// find the user based on email
const { email, password } = req.body;
User.findOne({ email }, (err, user) => {
if (err || !user) {
return res.status(400).json({
error: "User with that email does not exist. Please siginup",
});
}

// if user is found make sure the email and password match
// create authenticate method in user model
if (!user.authenticate(password)) {
return res.status(401).json({
error: "Email and password don't match",
});
}

// generate a signed token with user id and secret
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET);
// persist the thken as 't' in cookie with expiry date
res.cookie("t", token, { expire: new Date() + 9999 });
// return response with user and token to frontend client
const { _id, name, email, role } = user;
return res.json({ token, user: { _id, email, name, role } });
});
};

exports.signout = (req, res) => {
res.clearCookie("t");
res.json({ message: "Signout success" });
};
./helpers/dbErrorHandler.js - 翻譯 DB error message
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// ./helpers/dbErrorHandler.js
"use strict";

/**
* Get unique error field name
*/
const uniqueMessage = (error) => {
let output;

try {
let fieldName = error.message.substring(
error.message.lastIndexOf(".$") + 2,
error.message.lastIndexOf("_1")
);
output =
fieldName.charAt(0).toUpperCase() +
fieldName.slice(1) +
" already exists";
} catch (ex) {
output = "Unique field already exists";
}

return output;
};

/**
* Get the erroror message from error object
*/
exports.errorHandler = (error) => {
let message = "";

// 不知為何, 原來的內容如此(object),可提出 .message and .code
// MongoServerError: E11000 duplicate key error collection: myFirstDatabase.users index: email_1 dup key: { email: "tony@gmail.com" }
// console.log(`\n\rerror.message->${error.message}=`);
// console.log(`\n\rerror.code->${error.code}=`);

if (error.code) {
switch (error.code) {
case 11000:
case 11001:
message = uniqueMessage(error);
break;
default:
message = "Something went wrong";
}
} else {
for (let errorName in error.errorors) {
if (error.errorors[errorName].message)
message = error.errorors[errorName].message;
}
}

return message;
};
./models/user.js : access DB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// ./models/user.js
const mongoose = require("mongoose");
const crypto = require("crypto");
// fix error for uuidv1
// const uuidv1 = require("uuid/v1");
const { v1: uuidv1 } = require("uuid");

const userSchema = new mongoose.Schema(
{
name: {
type: String,
trim: true,
require: true,
maxlength: 32,
},
email: {
type: String,
trim: true,
require: true,
unique: 32,
},
hashed_password: {
type: String,
require: true,
},
about: {
type: String,
trim: true,
},
salt: String,
role: {
type: Number,
default: 0,
},
history: {
tyep: Array,
default: [],
},
},
{
timestamps: true,
}
);

// virtual field
userSchema
.virtual("password")
.set(function (password) {
this._password = password;
this.salt = uuidv1();
this.hashed_password = this.encryptPassword(password);
})
.get(function () {
return this._password;
});

userSchema.methods = {
// verify password
authenticate: function (plainText) {
return this.encryptPassword(plainText) === this.hashed_password;
},

encryptPassword: function (password) {
if (!password) return "";
try {
return crypto
.createHmac("sha1", this.salt)
.update(password)
.digest("hex");
} catch (err) {
return "";
}
},
};

module.exports = mongoose.model("User", userSchema);

Auth and Admin middlewares

  • ./controllers/user.js 改為 auth.js
  • ./routers/user.js 改為 auth.js
./app.js : add app.use(“/api”, userRoutes) for using Auth
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// ./app.js
const express = require("express");
// connect mangoDB altas
// using 2.2.12 or later's uri
const mongoose = require("mongoose");
// import routes
const authRoutes = require("./routes/auth");
const userRoutes = require("./routes/user");
// import morgan
const morgan = require("morgan");
// cookie-parser
const cookieParser = require("cookie-parser");
// express-validator
const expressValidator = require("express-validator");
// env
require("dotenv").config();

// app
const app = express();

// connect mangoDB altas
// using 2.2.12 or later's uri
mongoose
.connect(process.env.DATABASE, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log("MongoDB Connected…");
})
.catch((err) => console.log(err));

// middlewares
app.use(morgan("dev")); // morgan - http request log
app.use(express.json()); // body parser
app.use(cookieParser()); // cookie-parser
app.use(expressValidator()); // express-validator

// routes middleware
app.use("/api", authRoutes);
app.use("/api", userRoutes);

const port = process.env.PORT || 8080;

app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
  • ../controllers/auth
    • requireSignin : check token
    • isAuth : check user auth
    • isAdmin : check admin(role == 1)
  • router.param(“userId”, userById) : if include parameter userId call userById
  • add ./controllers/user.js for userById
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ./routes/user.js
const express = require("express");
const router = express.Router();
// add controller
const { userById } = require("../controllers/user");
// add controller
const { requireSignin, isAuth, isAdmin } = require("../controllers/auth");

router.get("/secrect/:userId", requireSignin, isAuth, (req, res) => {
res.json({
user: req.profile,
});
});
router.param("userId", userById);

module.exports = router;
./controller/auth.js : add requireSignin, isAuth, isAdmin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// ./controller/auth.js
const User = require("../models/user");
const { errorHandler } = require("../helpers/dbErrorHandler");
// jwt
const jwt = require("jsonwebtoken"); // to generate signed token
const expressJWT = require("express-jwt"); // for authorization check
require("dotenv").config();

exports.signup = (req, res) => {
console.log("req.body", req.body);
const user = new User(req.body);
user.save((err, user) => {
if (err) {
return res.status(400).json({
err: errorHandler(err),
});
}

res.json({
user,
});
});
};

exports.signin = (req, res) => {
// find the user based on email
const { email, password } = req.body;
User.findOne({ email }, (err, user) => {
if (err || !user) {
return res.status(400)({
error: "User with that email does not exist. Please siginup",
});
}

// if user is found make sure the email and password match
// create authenticate method in user model
if (!user.authenticate(password)) {
return res.status(401).json({
error: "Email and password don't match",
});
}

// generate a signed token with user id and secret
const token = jwt.sign({ _id: user._id }, process.env.JWT_SECRET);
console.log("token:", token);
// persist the thken as 't' in cookie with expiry date
res.cookie("t", token, { expire: new Date() + 9999 });
// return response with user and token to frontend client
const { _id, name, email, role } = user;
return res.json({ token, user: { _id, email, name, role } });
});
};

exports.signout = (req, res) => {
res.clearCookie("t");
res.json({ message: "Signout success" });
};

// need have cookie-parser
// for authorization check
exports.requireSignin = expressJWT({
secret: process.env.JWT_SECRET,
algorithms: ["HS256"], // added later
userProperty: "auth",
});

exports.isAuth = (req, res, next) => {
let user = req.profile && req.auth && req.profile._id == req.auth._id;
if (!user) {
return res.status(403).json({ error: "Access denied" });
}
next();
};

exports.isAdmin = (req, res, next) => {
if (req.profile.role == 0) {
return res.status(403).json({ error: "Admin resource! Access denied" });
}
next();
};
./controllers/user.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ./controllers/user.js
const User = require("../models/user");

exports.userById = (req, res, next, id) => {
User.findById(id).exec((err, user) => {
if (err || !user) {
return res.status(400).json({
error: "User not found",
});
}
req.profile = user;
next();
});
};

add Category and Product

install
1
npm i formidable lodash
./app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// ./app.js
const express = require("express");
// connect mangoDB altas
// using 2.2.12 or later's uri
const mongoose = require("mongoose");
// import routes
const authRoutes = require("./routes/auth");
const userRoutes = require("./routes/user");
// add Category and Product
const categoryRoutes = require("./routes/category");
const productRoutes = require("./routes/product");
// import morgan
const morgan = require("morgan");
// cookie-parser
const cookieParser = require("cookie-parser");
// express-validator
const expressValidator = require("express-validator");
// env
require("dotenv").config();

// app
const app = express();

// connect mangoDB altas
// using 2.2.12 or later's uri
mongoose
.connect(process.env.DATABASE, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log("MongoDB Connected…");
})
.catch((err) => console.log(err));

// middlewares
app.use(morgan("dev")); // morgan - http request log
app.use(express.json()); // body parser
app.use(cookieParser()); // cookie-parser
app.use(expressValidator()); // express-validator

// routes middleware
app.use("/api", authRoutes);
app.use("/api", userRoutes);
// add Category and Product
app.use("/api", categoryRoutes);
app.use("/api", productRoutes);

const port = process.env.PORT || 8080;

app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
./routes/category.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// ./routes/category.js
const express = require("express");
const router = express.Router();
// add controller
const {
create,
categoryById,
read,
remove,
update,
list,
} = require("../controllers/category");
const { requireSignin, isAuth, isAdmin } = require("../controllers/auth");

const { userById } = require("../controllers/user");

router.get("/category/:categoryId", read);
router.post("/category/create/:userId", requireSignin, isAuth, isAdmin, create);
router.delete(
"/category/:categoryId/:userId",
requireSignin,
isAuth,
isAdmin,
remove
);
router.put(
"/category/:categoryId/:userId",
requireSignin,
isAuth,
isAdmin,
update
);
router.get("/categories", list);

// category/create
// userId 參數驗證
router.param("userId", userById);
router.param("categoryId", categoryById);

module.exports = router;
./routes/product.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// ./routes/product.js
const express = require("express");
const router = express.Router();
// add controller
const {
create,
productById,
read,
remove,
update,
} = require("../controllers/product");
const { requireSignin, isAuth, isAdmin } = require("../controllers/auth");

const { userById } = require("../controllers/user");

router.get("/product/:productId", read);
router.post("/product/create/:userId", requireSignin, isAuth, isAdmin, create);
router.delete(
"/product/:productId/:userId",
requireSignin,
isAuth,
isAdmin,
remove
);
router.put(
"/product/:productId/:userId",
requireSignin,
isAuth,
isAdmin,
update
);

// product/create
// userId 參數驗證
router.param("userId", userById);
router.param("productId", productById);

module.exports = router;
./controller/category.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// ./controller/category.js
const Category = require("../models/category");
const { errorHandler } = require("../helpers/dbErrorHandler");

exports.categoryById = (req, res, next, id) => {
Category.findById(id).exec((err, category) => {
if (err || !category) {
return res.status(400).json({
error: "Category does not exist",
});
}
req.category = category;
console.log("1:", req.category);
next();
});
};

exports.read = (req, res) => {
return res.json(req.category);
};

exports.create = (req, res) => {
const category = new Ctegory(req.body);

category.save((err, data) => {
if (err) {
return res.status(400).json({
error: errorHandler(err),
});
}
// fix 含 data 欄位
res.json( data );
});
};

exports.remove = (req, res) => {
let category = req.category;
category.remove((err, deletedCategory) => {
if (err) {
return res.status(400).json({
error: errorHandler(err),
});
}
res.json({
message: "Category deleted successly",
});
});
};

exports.update = (req, res) => {
const category = req.category;

category.name = req.body.name;

category.save((err, data) => {
if (err) {
return res.status(400).json({
error: errorHandler(err),
});
}
// fix 含 data 欄位
res.json( data );
});
};

exports.list = (req, res) => {
Category.find().exec((err, data) => {
if (err) {
return res.status(400).json({
error: errorHandler(err),
});
}
// fix 含 data 欄位
res.json( data );
});
};
./controller/product.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
// ./controller/product.js
const formidable = require("formidable");
const _ = require("lodash");
const fs = require("fs");
const Product = require("../models/product");
const { errorHandler } = require("../helpers/dbErrorHandler");
const { runInNewContext } = require("vm");

exports.productById = (req, res, next, id) => {
Product.findById(id).exec((err, product) => {
// console.log("productById...");
if (err || !product) {
return res.status(400).json({
error: "Product does not exist",
});
}
req.product = product;
next();
});
};

exports.read = (req, res) => {
req.product.photo = undefined;
return res.json(req.product);
};

exports.create = (req, res) => {
let form = new formidable.IncomingForm();
form.keepExtensions = true;

form.parse(req, (err, fields, files) => {
if (err) {
return res.status(400).json({
error: "Image could not be uploaded",
});
}

// check for all fieldd
const { name, description, price, category, quantity, shipping } = fields;
if (
!name ||
!description ||
!price ||
!category ||
!quantity ||
!shipping
) {
return res.status(400).json({
error: "All field are required",
});
}

let product = new Product(fields);
if (files.photo) {
if (files.photo.size > 200000) {
return res.status(400).json({
error: "Image should be less 200k in size",
});
}

// change files.photo.file to files.photo.filepath
product.photo.data = fs.readFileSync(files.photo.filepath);
product.photo.contentType = files.photo.type;
}

product.save((err, result) => {
if (err) {
return res.status(400).json({
error: errorHandler(err),
});
}
res.json(result);
});
});
};

exports.remove = (req, res) => {
let product = req.product;
product.remove((err, deletedProduct) => {
if (err) {
return res.status(400).json({
error: errorHandler(err),
});
}
res.json({
message: "Product deleted successly",
});
});
};

exports.update = (req, res) => {
let form = new formidable.IncomingForm();
form.keepExtensions = true;

form.parse(req, (err, fields, files) => {
if (err) {
return res.status(400).json({
error: "Image could not be uploaded",
});
}

// check for all fieldd
const { name, description, price, category, quantity, shipping } = fields;
if (
!name ||
!description ||
!price ||
!category ||
!quantity ||
!shipping
) {
return res.status(400).json({
error: "All field are required",
});
}

let product = req.product;
// fields 蓋過 product
product = _.extend(product, fields);

if (files.photo) {
if (files.photo.size > 200000) {
return res.status(400).json({
error: "Image should be less 200k in size",
});
}

// change files.photo.file to files.photo.filepath
product.photo.data = fs.readFileSync(files.photo.filepath);
product.photo.contentType = files.photo.type;
}

product.save((err, result) => {
result.photo = undefined;
if (err) {
return res.status(400).json({
error: errorHandler(err),
});
}
res.json(result);
});
});
};
./models/category.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ./models/category.js
const mongoose = require("mongoose");

const categorySchema = new mongoose.Schema(
{
name: {
type: String,
trim: true,
require: true,
maxlength: 32,
},
},
{
timestamps: true,
}
);

module.exports = mongoose.model("Category", categorySchema);
./models/product.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// ./models/product.js
const mongoose = require("mongoose");
const { ObjectId } = mongoose.Schema;

const productSchema = new mongoose.Schema(
{
name: {
type: String,
trim: true,
required: true,
maxlength: 32,
},
description: {
type: String,
required: true,
maxlength: 2000,
},
price: {
type: Number,
trim: true,
required: true,
maxlength: 32,
},
category: {
type: ObjectId,
ref: "Category",
require: true,
},
quantity: {
type: Number,
},
photo: {
data: Buffer,
contentType: String,
},
shipping: {
required: false,
type: Boolean,
},
},
{
timestamps: true,
}
);

module.exports = mongoose.model("Product", productSchema);

add some for product and user

  • product list all : get all product
  • product list related : get other product same category
  • product list category : list product incluse category
  • product search : search product
  • product photo : get product photo
  • user read : read user info
  • user update : update user info
./routes/user.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ./routes/user.js
const express = require("express");
const router = express.Router();
// add controller
const { userById, read, update } = require("../controllers/user");
// add controller
const { requireSignin, isAuth, isAdmin } = require("../controllers/auth");

router.get("/secret/:userId", requireSignin, isAuth, (req, res) => {
res.json({
user: req.profile,
});
});

router.get("/user/:userId", requireSignin, isAuth, read);
router.put("/user/:userId", requireSignin, isAuth, update);

// userId 參數驗證
router.param("userId", userById);

module.exports = router;
./controllers/user.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// ./controllers/user.js
const User = require("../models/user");

exports.userById = (req, res, next, id) => {
User.findById(id).exec((err, user) => {
if (err || !user) {
return res.status(400).json({
error: "User not found",
});
}
req.profile = user;
next();
});
};

exports.read = (req, res) => {
req.profile.hashed_password = undefined;
req.profile.salt = undefined;
return res.json(req.profile);
};

exports.update = (req, res) => {
User.findOneAndUpdate(
{ _id: req.profile._id },
{ $set: req.body },
{ new: true },
(err, user) => {
if (err) {
return res.status(400).json({
error: "You are not authorized to perform this action",
});
}
req.profile.hashed_password = undefined;
req.profile.salt = undefined;
res.json(user);
}
);
};
./routes/product.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// ./routes/product.js
const express = require("express");
const router = express.Router();
// add controller
const {
create,
productById,
read,
remove,
update,
list,
listRelated,
listCategories,
listBySearch,
photo,
} = require("../controllers/product");
const { requireSignin, isAuth, isAdmin } = require("../controllers/auth");

const { userById } = require("../controllers/user");

router.get("/product/:productId", read);
router.post("/product/create/:userId", requireSignin, isAuth, isAdmin, create);
router.delete(
"/product/:productId/:userId",
requireSignin,
isAuth,
isAdmin,
remove
);
router.put(
"/product/:productId/:userId",
requireSignin,
isAuth,
isAdmin,
update
);

router.get("/products", list);
router.get("/products/related/:productId", listRelated);
router.get("/products/categories", listCategories);
router.post("/products/by/search", listBySearch);
router.get("/product/photo/:productId", photo);

// product/create
// userId 參數驗證
router.param("userId", userById);
router.param("productId", productById);

module.exports = router;
./controller/product.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
// ./controller/product.js
const formidable = require("formidable");
const _ = require("lodash");
const fs = require("fs");
const Product = require("../models/product");
const { errorHandler } = require("../helpers/dbErrorHandler");

exports.productById = (req, res, next, id) => {
Product.findById(id).exec((err, product) => {
if (err || !product) {
return res.status(400).json({
error: "Product does not exist",
});
}
req.product = product;
next();
});
};

exports.read = (req, res) => {
req.product.photo = undefined;
return res.json(req.product);
};

exports.create = (req, res) => {
let form = new formidable.IncomingForm();
form.keepExtensions = true;

form.parse(req, (err, fields, files) => {
if (err) {
return res.status(400).json({
error: "Image could not be uploaded",
});
}

// check for all fieldd
const { name, description, price, category, quantity, shipping } = fields;
if (
!name ||
!description ||
!price ||
!category ||
!quantity ||
!shipping
) {
return res.status(400).json({
error: "All field are required",
});
}

let product = new Product(fields);
if (files.photo) {
if (files.photo.size > 200000) {
return res.status(400).json({
error: "Image should be less 200k in size",
});
}

// change files.photo.file to files.photo.filepath
product.photo.data = fs.readFileSync(files.photo.filepath);
product.photo.contentType = files.photo.mimetype;
}

product.save((err, result) => {
if (err) {
return res.status(400).json({
error: errorHandler(err),
});
}

result.photo = undefined;
res.json(result);
});
});
};

exports.remove = (req, res) => {
let product = req.product;
product.remove((err, deletedProduct) => {
if (err) {
return res.status(400).json({
error: errorHandler(err),
});
}
res.json({
message: "Product deleted successly",
});
});
};

exports.update = (req, res) => {
let form = new formidable.IncomingForm();
form.keepExtensions = true;

form.parse(req, (err, fields, files) => {
if (err) {
return res.status(400).json({
error: "Image could not be uploaded",
});
}

// check for all fieldd
const { name, description, price, category, quantity, shipping } = fields;
if (
!name ||
!description ||
!price ||
!category ||
!quantity ||
!shipping
) {
return res.status(400).json({
error: "All field are required",
});
}

let product = req.product;
// fields 蓋過 product
product = _.extend(product, fields);

if (files.photo) {
if (files.photo.size > 200000) {
return res.status(400).json({
error: "Image should be less 200k in size",
});
}

// change files.photo.file to files.photo.filepath
product.photo.data = fs.readFileSync(files.photo.filepath);
product.photo.contentType = files.photo.mimetype;
}

product.save((err, result) => {
result.photo = undefined;
if (err) {
return res.status(400).json({
error: errorHandler(err),
});
}

result.photo = undefined;
res.json(result);
});
});
};

/**
* sel/arrival
* bye sell = /products?sortBy=sold&order=desc&limit=4
* bye arrival = /products?sortBy=createdAt&order=desc&limit=4
* if no parameter are sent, then all products are returned
*/
exports.list = (req, res) => {
let order = req.query.order ? req.query.order : "asc";
let sortBy = req.query.sortBy ? req.query.sortBy : "_id";
let limit = req.query.limit ? parseInt(req.query.limit) : 6;

Product.find()
.select("-photo")
.populate("category") // mapt to Category
.sort([[sortBy, order]])
.limit(limit)
.exec((err, products) => {
if (err) {
return res.status(400).json({
error: "Products not found",
});
}
res.json(products);
});
};

/**
* it will find the products based on the req product category
* other products that has the same category, will be return
*/

exports.listRelated = (req, res) => {
let limit = req.query.limit ? parseInt(req.query.limit) : 6;

// $ne: not include
Product.find({ _id: { $ne: req.product }, category: req.product.category })
.select("-photo")
.limit(limit)
.populate("category", "_id name")
.exec((err, products) => {
if (err) {
return res.status(400).json({
error: "Products not found",
});
}
res.json(products);
});
};

exports.listCategories = (req, res) => {
// distinct : 取出不同的 category
// {} : 2nd parameter doesn't need do no send value
Product.distinct("category", {}, (err, categories) => {
if (err) {
return res.status(400).json({
error: "Categories not found",
});
}
res.json(categories);
});
};

/**
* list products by search
* we will implement product search in react frontend
* we will show categories in checkbox and price range in radio buttons
* as the user clicks on those checkbox and radio buttons
* we will make api request and show the products to users based on what he wants
*/
// {
// "skip" : "1",
// "limit" : "2",
// "filters": {
// "name": "Note"
// }
// }
//
// >=2 and <=19
// {
// "filters": {
// "price": ["2", "19"]
// }
exports.listBySearch = (req, res) => {
let order = req.body.order ? req.body.order : "desc";
let sortBy = req.body.sortBy ? req.body.sortBy : "_id";
let limit = req.body.limit ? parseInt(req.body.limit) : 100;
let skip = req.body.skip ? parseInt(req.body.skip) : 0;
let findArgs = {};

for (let key in req.body.filters) {
if (req.body.filters[key].length > 0) {
if (key === "price") {
// gte - great than price
// lte - less than
findArgs[key] = {
$gte: req.body.filters[key][0],
$lte: req.body.filters[key][1],
};
} else {
findArgs[key] = new RegExp(req.body.filters[key]);
}
}
}

Product.find(findArgs)
.select("-photo")
.populate("category")
.sort([[sortBy, order]])
.skip(skip)
.limit(limit)
.exec((err, data) => {
if (err) {
return res.status(400).json({
error: "products not found",
});
}
res.json({
size: data.length,
data,
});
});
};

exports.photo = (req, res, next) => {
if (req.product.photo.data) {
res.set("Content-Type", req.product.photo.contentType);
return res.send(req.product.photo.data);
}
next();
};

CORS

install
1
npm i cors
js code
1
2
3
4
// cors
const cors = require("cors");

app.use(cors()); // cors

Error message

invalid token : error message
1
2
3
UnauthorizedError: invalid token
at D:\work\git-test\li\project\ecommerce\express\node_modules\express-jwt\lib\index.js:105:22
......

API

User

  • signup : 註冊

    1
    2
    3
    4
    5
    6
    7
    8
    POST api/signup
    Headers : [{"key":"Content-Type","value":"application/json","description":""}]
    Body :
    {
    "name": "pen2",
    "email": "pen2@gmail.com",
    "password": "rrrrrr5"
    }
  • signin : 登入

    1
    2
    3
    4
    5
    6
    7
    POST api/signin
    Headers : [{"key":"Content-Type","value":"application/json","description":""}]
    Body :
    {
    "email": "pen2@gmail.com",
    "password": "rrrrrr5"
    }
  • signout :登出

    1
    2
    GET api/signout
    Headers : [{"key":"Content-Type","value":"application/json","description":""}]
  • secrect : get user information

    1
    2
    3
    4
    5
    6
    GET api/secrect/{userId}
    Headers :
    [
    {"key":"Content-Type","value":"application/json","description":""},
    {"key":"Authorization","value":"Bearer token..","description":""}
    ]

Caegory

  • create : 新增

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    POST /api/category/create/{userId}
    Headers :
    [
    {"key":"Content-Type","value":"application/json","description":""},
    {"key":"Authorization","value":"Bearer token..","description":""}
    ]
    Body :
    {
    "name": "react2"
    }
  • read : 讀取一筆

    1
    2
    GET /api/category/{categoryID}
    Headers : [{"key":"Content-Type","value":"application/json","description":""}]
  • update : 更新

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    PUT /api/category/{categoryID}/{userId}
    Headers :
    [
    {"key":"Content-Type","value":"application/json","description":""},
    {"key":"Authorization","value":"Bearer token..","description":""}
    ]
    Body :
    {
    "name": "react2"
    }
  • delete : 刪除

    1
    2
    3
    4
    5
    6
    DEL /api/category/{categoryID}/{userId}
    Headers :
    [
    {"key":"Content-Type","value":"application/json","description":""},
    {"key":"Authorization","value":"Bearer token..","description":""}
    ]
  • list : 讀取全部

    1
    2
    GET /api/categories
    Headers : [{"key":"Content-Type","value":"application/json","description":""}]

Product

  • create : 新增

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    POST /api/product/create/{userId}
    Headers :
    [{"key":"Authorization","value":"Bearer token..","description":""}]
    Body : form-data
    name:PHP update
    description:My second book on PHP update
    price:20
    category:618cccaac104434a41b7e4e7
    shipping:false
    quantity:100
    photo --> file select
  • read : 讀取一筆

    1
    2
    GET /api/product/{ProductId}
    Headers : [{"key":"Content-Type","value":"application/json","description":""}] ??
  • update : 更新

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    PUT /api/product/{productID}/{userId}
    Headers :
    [
    {"key":"Content-Type","value":"application/json","description":""}, ??
    {"key":"Authorization","value":"Bearer token..","description":""}
    ]
    Body : form-data
    name:PHP update
    description:My second book on PHP update
    price:20
    category:618cccaac104434a41b7e4e7
    shipping:false
    quantity:100
    photo --> file select
  • delete : 刪除

    1
    2
    3
    4
    5
    6
    DEL /api/product/{productID}/{userId}
    Headers :
    [
    {"key":"Content-Type","value":"application/json","description":""}, ??
    {"key":"Authorization","value":"Bearer token..","description":""}
    ]

Postman

user api

signup
  • POST http://localhost:8000/api/signup

  • header application/json

  • body

    1
    2
    3
    4
    5
    {
    "name": "key2",
    "email": "key2@gmail.com",
    "password": "rrrrrr5"
    }
  • response

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    # 1st time
    {
    "user": {
    "name": "key2",
    "email": "key2@gmail.com",
    "hashed_password": "f36f604f6b3f085dea51bc1686d8ff18d915d038",
    "salt": "1740b470-405f-11ec-af6d-67dd2756041e",
    "role": 0,
    "history": {
    "type": [],
    "default": []
    },
    "_id": "6188c6e92a5c350c58aa6d98",
    "createdAt": "2021-11-08T06:42:49.790Z",
    "updatedAt": "2021-11-08T06:42:49.790Z",
    "__v": 0
    }
    }

    # 2nd times
    {
    "err": "11000 duplicate key error collection: ecmm.users index: email already exists"
    }
signin
  • POST http://localhost:8000/api/signin
  • header application/json
  • body
    1
    2
    3
    4
    {
    "email": "key2@gmail.com",
    "password": "rrrrrr5"
    }
  • response
    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYxODc5NTU2YWIyYWVlNzIyYjI2YzI0NSIsImlhdCI6MTYzNjI5Nzg2OX0.jXzr0AMfA7Rwc-tMcljQUc2FKbxcPMAxzqddCaDoqFk",
    "user": {
    "_id": "61879556ab2aee722b26c245",
    "email": "key2@gmail.com",
    "name": "key2",
    "role": 0
    }
    }
signout
read
  • GET http://localhost:8000/api/user/618d278d9e3c6d80ff6d0bd6
  • Headers :
    1
    2
    3
    4
    [
    {"key":"Content-Type","value":"application/json","description":""},
    {"key":"Authorization","value":"Bearer token..","description":""}
    ]
  • response
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    {
    "user": {
    "_id": "618b63c5e34b77bf26b6c8a1",
    "name": "key6",
    "email": "key6@gmail.com",
    "hashed_password": "887f81a60171906e267b37fc777d3e282fdffc7a",
    "salt": "c2c66800-41ed-11ec-97a8-83adda8782ce",
    "role": 1,
    "history": [],
    "createdAt": "2021-11-10T06:16:37.259Z",
    "updatedAt": "2021-11-10T06:16:37.259Z",
    "__v": 0
    }
    }
update
  • PUT http://localhost:8000/api/user/618d278d9e3c6d80ff6d0bd6
  • Headers :
    1
    2
    3
    4
    [
    {"key":"Content-Type","value":"application/json","description":""},
    {"key":"Authorization","value":"Bearer token..","description":""}
    ]
  • body
    1
    2
    3
    {
    "name": "Pen2 update"
    }
  • response
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    "_id": "618d278d9e3c6d80ff6d0bd6",
    "name": "Pen2 new",
    "email": "pen2@gmail.com",
    "hashed_password": "a696711c462e5dfd4526c8914dcb6bb286f4d08b",
    "salt": "0b6ead70-42fb-11ec-9c79-f353ea83f35d",
    "role": 1,
    "history": [],
    "createdAt": "2021-11-11T14:24:13.767Z",
    "updatedAt": "2021-11-15T01:25:40.367Z",
    "__v": 0
    }
secret : get all user info
  • GET http://localhost:8000/api/secret/618b63c5e34b77bf26b6c8a1
  • Headers :
    1
    2
    3
    4
    [
    {"key":"Content-Type","value":"application/json","description":""},
    {"key":"Authorization","value":"Bearer token..","description":""}
    ]
  • response
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    {
    "user": {
    "_id": "618b63c5e34b77bf26b6c8a1",
    "name": "key6",
    "email": "key6@gmail.com",
    "hashed_password": "887f81a60171906e267b37fc777d3e282fdffc7a",
    "salt": "c2c66800-41ed-11ec-97a8-83adda8782ce",
    "role": 1,
    "history": [],
    "createdAt": "2021-11-10T06:16:37.259Z",
    "updatedAt": "2021-11-10T06:16:37.259Z",
    "__v": 0
    }
    }

category

create
  • POST http://localhost:8000/api/category/create/618a148152decc596ebad50e
  • Headers :
    1
    2
    3
    4
    [
    {"key":"Content-Type","value":"application/json","description":""},
    {"key":"Authorization","value":"Bearer token..","description":""}
    ]
  • body
    1
    2
    3
    {
    "name": "Python"
    }
  • response
    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    {
    "name": "Python",
    "_id": "6191b9327a4a9765fa7ae859",
    "createdAt": "2021-11-15T01:34:42.504Z",
    "updatedAt": "2021-11-15T01:34:42.504Z",
    "__v": 0
    }
    }
read
update
  • PUT http://localhost:8000/api/category/6191b9327a4a9765fa7ae859/618a148152decc596ebad50e
  • Headers :
    1
    2
    3
    4
    [
    {"key":"Content-Type","value":"application/json","description":""},
    {"key":"Authorization","value":"Bearer token..","description":""}
    ]
  • body
    1
    2
    3
    {
    "name": "Python update"
    }
  • response
    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    {
    "_id": "6191b9327a4a9765fa7ae859",
    "name": "Python update",
    "createdAt": "2021-11-15T01:34:42.504Z",
    "updatedAt": "2021-11-15T01:44:47.140Z",
    "__v": 0
    }
    }
delet
list : list all category
  • GET http://localhost:8000/api/categories
  • Headers : []
  • response
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    {
    [
    {
    "_id": "618cccaac104434a41b7e4e7",
    "name": "Node",
    "createdAt": "2021-11-11T07:56:26.487Z",
    "updatedAt": "2021-11-11T07:56:26.487Z",
    "__v": 0
    },
    {
    "_id": "618d2aeb3cc4d2fb8b0ffa6c",
    "name": "python",
    "createdAt": "2021-11-11T14:38:35.706Z",
    "updatedAt": "2021-11-11T14:38:35.706Z",
    "__v": 0
    },
    {
    "_id": "618f0dd7e7a15a9c59fe3c5c",
    "name": "php",
    "createdAt": "2021-11-13T00:59:03.787Z",
    "updatedAt": "2021-11-13T00:59:03.787Z",
    "__v": 0
    }
    ]
    }

product

create
  • POST http://localhost:8000/api/product/create/618a148152decc596ebad50e
  • Headers :
    1
    2
    3
    [
    {"key":"Authorization","value":"Bearer token..","description":""}
    ]
  • body : form-data
    1
    2
    3
    4
    5
    6
    7
    name:node
    price:2
    category:618cccaac104434a41b7e4e7
    shipping:false
    quantity:100
    description:My second book on node
    photo --> file select
  • response
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    "name": "python",
    "description": "My second book on node",
    "price": 2,
    "category": "618cccaac104434a41b7e4e7",
    "quantity": 100,
    "sold": 0,
    "shipping": false,
    "_id": "6191c890b0f8b1de8b7014f8",
    "createdAt": "2021-11-15T02:40:16.238Z",
    "updatedAt": "2021-11-15T02:40:16.238Z",
    "__v": 0
    }
update
  • PUT http://localhost:8000/api/product/6191c9a31f6127dd22f091a9/618a148152decc596ebad50e
  • Headers :
    1
    2
    3
    [
    {"key":"Authorization","value":"Bearer token..","description":""}
    ]
  • body
    1
    2
    3
    4
    5
    6
    7
    name:python update
    price:2
    category:618cccaac104434a41b7e4e7
    shipping:false
    quantity:100
    description:My second book on node
    photo --> file select
  • response
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    "_id": "6191c9a31f6127dd22f091a9",
    "name": "python update",
    "description": "My second book on node",
    "price": 2,
    "category": "618cccaac104434a41b7e4e7",
    "quantity": 100,
    "sold": 0,
    "shipping": false,
    "createdAt": "2021-11-15T02:44:51.335Z",
    "updatedAt": "2021-11-15T02:50:09.319Z",
    "__v": 0
    }
read
  • GET http://localhost:8000/api/product/6191c9a31f6127dd22f091a9
  • Headers : []
  • response
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    "_id": "6191c9a31f6127dd22f091a9",
    "name": "python update",
    "description": "My second book on node",
    "price": 2,
    "category": "618cccaac104434a41b7e4e7",
    "quantity": 100,
    "sold": 0,
    "shipping": false,
    "createdAt": "2021-11-15T02:44:51.335Z",
    "updatedAt": "2021-11-15T02:50:09.319Z",
    "__v": 0
    }
delet
list all
  • GET http://localhost:8000/api/products
  • Headers : []
  • response
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    [
    {
    "_id": "618f0dd7e7a15a9c59fe3c5c",
    "name": "PHP",
    "description": "My second book on PHP ",
    "price": 2,
    "category": {
    "_id": "618f0dd7e7a15a9c59fe3c5c",
    "name": "php",
    "createdAt": "2021-11-13T00:59:03.787Z",
    "updatedAt": "2021-11-13T00:59:03.787Z",
    "__v": 0
    },
    "quantity": 100,
    "sold": 0,
    "shipping": false,
    "createdAt": "2021-11-14T07:14:08.776Z",
    "updatedAt": "2021-11-14T07:14:08.776Z",
    "__v": 0
    },
    {
    "sold": 0,
    "_id": "618f0e74d520011c21fee5be",
    "name": "Note Book #2",
    "description": "My second book on node js",
    "price": 20,
    "category": {
    "_id": "618cccaac104434a41b7e4e7",
    "name": "Node",
    "createdAt": "2021-11-11T07:56:26.487Z",
    "updatedAt": "2021-11-11T07:56:26.487Z",
    "__v": 0
    },
    "quantity": 100,
    "shipping": false,
    "createdAt": "2021-11-13T01:01:40.218Z",
    "updatedAt": "2021-11-13T01:01:40.218Z",
    "__v": 0
    },
    {
    "sold": 0,
    "_id": "618f1a5f5b6c11abb3d34d35",
    "name": "Note Book #2",
    "description": "My second book on node js",
    "price": 20,
    "category": {
    "_id": "618cccaac104434a41b7e4e7",
    "name": "Node",
    "createdAt": "2021-11-11T07:56:26.487Z",
    "updatedAt": "2021-11-11T07:56:26.487Z",
    "__v": 0
    },
    "quantity": 100,
    "shipping": false,
    "createdAt": "2021-11-13T01:52:31.721Z",
    "updatedAt": "2021-11-13T01:52:31.721Z",
    "__v": 0
    },
    ]
  • GET http://localhost:8000/api/products/related/6191c9a31f6127dd22f091a9
  • Headers : []
  • response
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    [
    {
    "sold": 0,
    "_id": "618f0e74d520011c21fee5be",
    "name": "Note Book #2",
    "description": "My second book on node js",
    "price": 20,
    "category": {
    "_id": "618cccaac104434a41b7e4e7",
    "name": "Node"
    },
    "quantity": 100,
    "shipping": false,
    "createdAt": "2021-11-13T01:01:40.218Z",
    "updatedAt": "2021-11-13T01:01:40.218Z",
    "__v": 0
    },
    {
    "sold": 0,
    "_id": "618f1a5f5b6c11abb3d34d35",
    "name": "Note Book #2",
    "description": "My second book on node js",
    "price": 20,
    "category": {
    "_id": "618cccaac104434a41b7e4e7",
    "name": "Node"
    },
    "quantity": 100,
    "shipping": false,
    "createdAt": "2021-11-13T01:52:31.721Z",
    "updatedAt": "2021-11-13T01:52:31.721Z",
    "__v": 0
    },
    {
    "sold": 0,
    "_id": "618f1ce44737b63742b4e61b",
    "name": "Note Book #2",
    "description": "My second book on node js",
    "price": 20,
    "category": {
    "_id": "618cccaac104434a41b7e4e7",
    "name": "Node"
    },
    "quantity": 100,
    "shipping": false,
    "createdAt": "2021-11-13T02:03:16.637Z",
    "updatedAt": "2021-11-13T02:03:16.637Z",
    "__v": 0
    },
    ]
list category
  • POST http://localhost:8000/api/products/by/search
  • Headers :
    1
    2
    3
    [
    {"key":"Content-Type","value":"application/json","description":""}
    ]
  • body
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // seach price
    {
    "skip" : "0",
    "limit" : "100",
    "filters": {
    "price": ["3", "20"]
    }
    }
    // search name
    {
    "filters": {
    "name": "Note"
    }
    }
  • response
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    {
    "size": 4,
    "data": [
    {
    "sold": 0,
    "_id": "618f39c32a40b25aab100325",
    "name": "PHP update",
    "description": "My second book on PHP update",
    "price": 20,
    "category": {
    "_id": "618f0dd7e7a15a9c59fe3c5c",
    "name": "php",
    "createdAt": "2021-11-13T00:59:03.787Z",
    "updatedAt": "2021-11-13T00:59:03.787Z",
    "__v": 0
    },
    "quantity": 100,
    "shipping": false,
    "createdAt": "2021-11-13T04:06:27.222Z",
    "updatedAt": "2021-11-13T04:16:44.822Z",
    "__v": 0
    },
    {
    "sold": 0,
    "_id": "618f1ce44737b63742b4e61b",
    "name": "Note Book #2",
    "description": "My second book on node js",
    "price": 20,
    "category": {
    "_id": "618cccaac104434a41b7e4e7",
    "name": "Node",
    "createdAt": "2021-11-11T07:56:26.487Z",
    "updatedAt": "2021-11-11T07:56:26.487Z",
    "__v": 0
    },
    "quantity": 100,
    "shipping": false,
    "createdAt": "2021-11-13T02:03:16.637Z",
    "updatedAt": "2021-11-13T02:03:16.637Z",
    "__v": 0
    },
    {
    "sold": 0,
    "_id": "618f1a5f5b6c11abb3d34d35",
    "name": "Note Book #2",
    "description": "My second book on node js",
    "price": 20,
    "category": {
    "_id": "618cccaac104434a41b7e4e7",
    "name": "Node",
    "createdAt": "2021-11-11T07:56:26.487Z",
    "updatedAt": "2021-11-11T07:56:26.487Z",
    "__v": 0
    },
    "quantity": 100,
    "shipping": false,
    "createdAt": "2021-11-13T01:52:31.721Z",
    "updatedAt": "2021-11-13T01:52:31.721Z",
    "__v": 0
    },
    {
    "sold": 0,
    "_id": "618f0e74d520011c21fee5be",
    "name": "Note Book #2",
    "description": "My second book on node js",
    "price": 20,
    "category": {
    "_id": "618cccaac104434a41b7e4e7",
    "name": "Node",
    "createdAt": "2021-11-11T07:56:26.487Z",
    "updatedAt": "2021-11-11T07:56:26.487Z",
    "__v": 0
    },
    "quantity": 100,
    "shipping": false,
    "createdAt": "2021-11-13T01:01:40.218Z",
    "updatedAt": "2021-11-13T01:01:40.218Z",
    "__v": 0
    }
    ]
    }
photo

braintree

get token
  • GET http://localhost:8000/api/braintree/getToken/6188c8128db0e4691c2f6727
  • Headers :
    1
    2
    3
    4
    {
    Content-Type:application/json
    Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTg4YzgxMjhkYjBlNDY5MWMyZjY3MjciLCJpYXQiOjE2Mzg0NTY1MDV9.7FWaG101Y0j3WhQdTMbO2-ew9pnb-S3tObemBH5DG28
    }
  • response
    1
    2
    3
    4
    {
    "clientToken": "eyJ2ZXJzaW9uIjoyLCJhdXRob3JpemF0aW9uRmluZ2VycHJpbnQiOiJleUowZVhBaU9pSktWMVFpTENKaGJHY2lPaUpGVXpJMU5pSXNJbXRwWkNJNklqSXdNVGd3TkRJMk1UWXRjMkZ1WkdKdmVDSXNJbWx6Y3lJNkltaDBkSEJ6T2k4dllYQnBMbk5oYm1SaWIzZ3VZbkpoYVc1MGNtVmxaMkYwWlhkaGVTNWpiMjBpZlEuZXlKbGVIQWlPakUyTXpnNU16UTNOVElzSW1wMGFTSTZJbU15TUdRd1l6RXlMVE15T0RndE5HSm1OaTA0T0RJMkxXUTFaVGcwTVdFM01HSTFaaUlzSW5OMVlpSTZJblJpTlhaNVpIcHlNMjFrY0dRMGVXUWlMQ0pwYzNNaU9pSm9kSFJ3Y3pvdkwyRndhUzV6WVc1a1ltOTRMbUp5WVdsdWRISmxaV2RoZEdWM1lYa3VZMjl0SWl3aWJXVnlZMmhoYm5RaU9uc2ljSFZpYkdsalgybGtJam9pZEdJMWRubGtlbkl6YldSd1pEUjVaQ0lzSW5abGNtbG1lVjlqWVhKa1gySjVYMlJsWm1GMWJIUWlPbVpoYkhObGZTd2ljbWxuYUhSeklqcGJJbTFoYm1GblpWOTJZWFZzZENKZExDSnpZMjl3WlNJNld5SkNjbUZwYm5SeVpXVTZWbUYxYkhRaVhTd2liM0IwYVc5dWN5STZleUp0WlhKamFHRnVkRjloWTJOdmRXNTBYMmxrSWpvaVpHaGxkMmQwYUhSNUluMTkuaWxYeHJBQlJEOVlhVTBlb2pRU0d3MlNObFRYSkk3Wm5pRUtOVmlmVlNFUVZ6bUVHeGY0R3E2bG9DUzJWTW9yNHlFMTh2aTc1RHpNMHUzS216czV4MnciLCJjb25maWdVcmwiOiJodHRwczovL2FwaS5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tOjQ0My9tZXJjaGFudHMvdGI1dnlkenIzbWRwZDR5ZC9jbGllbnRfYXBpL3YxL2NvbmZpZ3VyYXRpb24iLCJtZXJjaGFudEFjY291bnRJZCI6ImRoZXdndGh0eSIsImdyYXBoUUwiOnsidXJsIjoiaHR0cHM6Ly9wYXltZW50cy5zYW5kYm94LmJyYWludHJlZS1hcGkuY29tL2dyYXBocWwiLCJkYXRlIjoiMjAxOC0wNS0wOCIsImZlYXR1cmVzIjpbInRva2VuaXplX2NyZWRpdF9jYXJkcyJdfSwiY2xpZW50QXBpVXJsIjoiaHR0cHM6Ly9hcGkuc2FuZGJveC5icmFpbnRyZWVnYXRld2F5LmNvbTo0NDMvbWVyY2hhbnRzL3RiNXZ5ZHpyM21kcGQ0eWQvY2xpZW50X2FwaSIsImVudmlyb25tZW50Ijoic2FuZGJveCIsIm1lcmNoYW50SWQiOiJ0YjV2eWR6cjNtZHBkNHlkIiwiYXNzZXRzVXJsIjoiaHR0cHM6Ly9hc3NldHMuYnJhaW50cmVlZ2F0ZXdheS5jb20iLCJhdXRoVXJsIjoiaHR0cHM6Ly9hdXRoLnZlbm1vLnNhbmRib3guYnJhaW50cmVlZ2F0ZXdheS5jb20iLCJ2ZW5tbyI6Im9mZiIsImNoYWxsZW5nZXMiOlsiY3Z2Il0sInRocmVlRFNlY3VyZUVuYWJsZWQiOnRydWUsImFuYWx5dGljcyI6eyJ1cmwiOiJodHRwczovL29yaWdpbi1hbmFseXRpY3Mtc2FuZC5zYW5kYm94LmJyYWludHJlZS1hcGkuY29tL3RiNXZ5ZHpyM21kcGQ0eWQifSwicGF5cGFsRW5hYmxlZCI6dHJ1ZSwicGF5cGFsIjp7ImJpbGxpbmdBZ3JlZW1lbnRzRW5hYmxlZCI6dHJ1ZSwiZW52aXJvbm1lbnROb05ldHdvcmsiOmZhbHNlLCJ1bnZldHRlZE1lcmNoYW50IjpmYWxzZSwiYWxsb3dIdHRwIjp0cnVlLCJkaXNwbGF5TmFtZSI6IlRyeUJ5U2VsZiIsImNsaWVudElkIjoiQWRNT2dfS2JXTmxmMTJ0ejFRUHMxYS1USHFDZFVHRWR2UTVXb0V0Q2NCSVM1U3FzdnFsLTZyU2JjWFUxWEQyUmhGc0EzeC1vSkEybXZJbjgiLCJwcml2YWN5VXJsIjoiaHR0cDovL2V4YW1wbGUuY29tL3BwIiwidXNlckFncmVlbWVudFVybCI6Imh0dHA6Ly9leGFtcGxlLmNvbS90b3MiLCJiYXNlVXJsIjoiaHR0cHM6Ly9hc3NldHMuYnJhaW50cmVlZ2F0ZXdheS5jb20iLCJhc3NldHNVcmwiOiJodHRwczovL2NoZWNrb3V0LnBheXBhbC5jb20iLCJkaXJlY3RCYXNlVXJsIjpudWxsLCJlbnZpcm9ubWVudCI6Im9mZmxpbmUiLCJicmFpbnRyZWVDbGllbnRJZCI6Im1hc3RlcmNsaWVudDMiLCJtZXJjaGFudEFjY291bnRJZCI6ImRoZXdndGh0eSIsImN1cnJlbmN5SXNvQ29kZSI6IlRXRCJ9fQ==",
    "success": true
    }

參考資料