├── server
├── .gitignore
├── utils
│ ├── CryptoUtil.js
│ ├── MongooseUtil.js
│ ├── MyConstants.js
│ ├── EmailUtil.js
│ └── JwtUtil.js
├── models
│ ├── AdminDAO.js
│ ├── OrderDAO.js
│ ├── CategoryDAO.js
│ ├── CustomerDAO.js
│ ├── Models.js
│ └── ProductDAO.js
├── package.json
├── index.js
├── api
│ ├── customer.js
│ └── admin.js
└── package-lock.json
├── client-admin
├── .gitignore
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── index.html
├── src
│ ├── contexts
│ │ ├── MyContext.js
│ │ └── MyProvider.js
│ ├── setupTests.js
│ ├── App.test.js
│ ├── index.css
│ ├── reportWebVitals.js
│ ├── components
│ │ ├── HomeComponent.js
│ │ ├── MainComponent.js
│ │ ├── MenuComponent.js
│ │ ├── CategoryComponent.js
│ │ ├── LoginComponent.js
│ │ ├── ProductComponent.js
│ │ ├── OrderComponent.js
│ │ ├── CategoryDetailComponent.js
│ │ ├── CustomerComponent.js
│ │ └── ProductDetailComponent.js
│ ├── App.js
│ ├── index.js
│ ├── App.css
│ └── logo.svg
└── package.json
├── client-customer
├── .gitignore
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── index.html
├── src
│ ├── contexts
│ │ ├── MyContext.js
│ │ └── MyProvider.js
│ ├── utils
│ │ ├── CartUtil.js
│ │ └── withRouter.js
│ ├── setupTests.js
│ ├── App.test.js
│ ├── index.css
│ ├── reportWebVitals.js
│ ├── App.js
│ ├── index.js
│ ├── components
│ │ ├── InformComponent.js
│ │ ├── MainComponent.js
│ │ ├── MenuComponent.js
│ │ ├── ActiveComponent.js
│ │ ├── ProductComponent.js
│ │ ├── HomeComponent.js
│ │ ├── LoginComponent.js
│ │ ├── SignupComponent.js
│ │ ├── ProductDetailComponent.js
│ │ ├── MycartComponent.js
│ │ ├── MyordersComponent.js
│ │ └── MyprofileComponent.js
│ ├── App.css
│ └── logo.svg
└── package.json
├── resources
├── images
│ ├── iPad Air.jpg
│ ├── iPad Pro.jpg
│ ├── iPhone X.jpg
│ ├── Macbook 12.jpg
│ ├── Macbook Air.jpg
│ ├── Macbook Pro.jpg
│ ├── iPad Mini.jpg
│ ├── iPhone 11.jpg
│ ├── iPhone XR.jpg
│ ├── iPhone XS.jpg
│ ├── iPhone 11 Pro.jpg
│ └── iPhone 11 Pro Max.jpg
└── mongodb
│ ├── admins.json
│ ├── categories.json
│ └── customers.json
└── README.md
/server/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/client-admin/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/client-customer/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/client-admin/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/client-customer/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/resources/images/iPad Air.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiendung8a6/Online-Shopping-MERN-Stack/HEAD/resources/images/iPad Air.jpg
--------------------------------------------------------------------------------
/resources/images/iPad Pro.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiendung8a6/Online-Shopping-MERN-Stack/HEAD/resources/images/iPad Pro.jpg
--------------------------------------------------------------------------------
/resources/images/iPhone X.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiendung8a6/Online-Shopping-MERN-Stack/HEAD/resources/images/iPhone X.jpg
--------------------------------------------------------------------------------
/client-admin/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiendung8a6/Online-Shopping-MERN-Stack/HEAD/client-admin/public/favicon.ico
--------------------------------------------------------------------------------
/client-admin/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiendung8a6/Online-Shopping-MERN-Stack/HEAD/client-admin/public/logo192.png
--------------------------------------------------------------------------------
/client-admin/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiendung8a6/Online-Shopping-MERN-Stack/HEAD/client-admin/public/logo512.png
--------------------------------------------------------------------------------
/resources/images/Macbook 12.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiendung8a6/Online-Shopping-MERN-Stack/HEAD/resources/images/Macbook 12.jpg
--------------------------------------------------------------------------------
/resources/images/Macbook Air.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiendung8a6/Online-Shopping-MERN-Stack/HEAD/resources/images/Macbook Air.jpg
--------------------------------------------------------------------------------
/resources/images/Macbook Pro.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiendung8a6/Online-Shopping-MERN-Stack/HEAD/resources/images/Macbook Pro.jpg
--------------------------------------------------------------------------------
/resources/images/iPad Mini.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiendung8a6/Online-Shopping-MERN-Stack/HEAD/resources/images/iPad Mini.jpg
--------------------------------------------------------------------------------
/resources/images/iPhone 11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiendung8a6/Online-Shopping-MERN-Stack/HEAD/resources/images/iPhone 11.jpg
--------------------------------------------------------------------------------
/resources/images/iPhone XR.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiendung8a6/Online-Shopping-MERN-Stack/HEAD/resources/images/iPhone XR.jpg
--------------------------------------------------------------------------------
/resources/images/iPhone XS.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiendung8a6/Online-Shopping-MERN-Stack/HEAD/resources/images/iPhone XS.jpg
--------------------------------------------------------------------------------
/client-admin/src/contexts/MyContext.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | const MyContext = React.createContext();
3 | export default MyContext;
--------------------------------------------------------------------------------
/client-customer/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiendung8a6/Online-Shopping-MERN-Stack/HEAD/client-customer/public/favicon.ico
--------------------------------------------------------------------------------
/client-customer/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiendung8a6/Online-Shopping-MERN-Stack/HEAD/client-customer/public/logo192.png
--------------------------------------------------------------------------------
/client-customer/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiendung8a6/Online-Shopping-MERN-Stack/HEAD/client-customer/public/logo512.png
--------------------------------------------------------------------------------
/client-customer/src/contexts/MyContext.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | const MyContext = React.createContext();
3 | export default MyContext;
--------------------------------------------------------------------------------
/resources/images/iPhone 11 Pro.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiendung8a6/Online-Shopping-MERN-Stack/HEAD/resources/images/iPhone 11 Pro.jpg
--------------------------------------------------------------------------------
/resources/images/iPhone 11 Pro Max.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiendung8a6/Online-Shopping-MERN-Stack/HEAD/resources/images/iPhone 11 Pro Max.jpg
--------------------------------------------------------------------------------
/resources/mongodb/admins.json:
--------------------------------------------------------------------------------
1 | [{
2 | "_id": {
3 | "$oid": "643f4ea23ef68011c2177816"
4 | },
5 | "username": "admin",
6 | "password": "123456"
7 | }]
--------------------------------------------------------------------------------
/client-customer/src/utils/CartUtil.js:
--------------------------------------------------------------------------------
1 | const CartUtil = {
2 | getTotal(mycart) {
3 | var total = 0;
4 | for (const item of mycart) {
5 | total += item.product.price * item.quantity;
6 | }
7 | return total;
8 | }
9 | };
10 | export default CartUtil;
--------------------------------------------------------------------------------
/server/utils/CryptoUtil.js:
--------------------------------------------------------------------------------
1 | //CLI: npm install crypto --save
2 | const CryptoUtil = {
3 | md5(input) {
4 | const crypto = require('crypto');
5 | const hash = crypto.createHash('md5').update(input).digest('hex');
6 | return hash;
7 | }
8 | };
9 | module.exports = CryptoUtil;
--------------------------------------------------------------------------------
/client-admin/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/client-customer/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/client-admin/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render();
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/client-customer/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render();
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/client-customer/src/utils/withRouter.js:
--------------------------------------------------------------------------------
1 | // using withRouter in class-component
2 | import { useParams, useNavigate } from "react-router-dom";
3 |
4 | function withRouter(Component) {
5 | return (props) => (
6 |
7 | );
8 | }
9 | export default withRouter;
--------------------------------------------------------------------------------
/resources/mongodb/categories.json:
--------------------------------------------------------------------------------
1 | [{
2 | "_id": {
3 | "$oid": "6288b164708fabf8ab29ca0a"
4 | },
5 | "name": "iPad"
6 | },{
7 | "_id": {
8 | "$oid": "6288b174708fabf8ab29ca0d"
9 | },
10 | "name": "iPhone"
11 | },{
12 | "_id": {
13 | "$oid": "6288b180708fabf8ab29ca10"
14 | },
15 | "name": "Macbook"
16 | }]
--------------------------------------------------------------------------------
/server/models/AdminDAO.js:
--------------------------------------------------------------------------------
1 | require('../utils/MongooseUtil');
2 | const Models = require('./Models');
3 |
4 | const AdminDAO = {
5 | async selectByUsernameAndPassword(username, password) {
6 | const query = { username: username, password: password };
7 | const admin = await Models.Admin.findOne(query);
8 | return admin;
9 | }
10 | };
11 | module.exports = AdminDAO;
--------------------------------------------------------------------------------
/client-admin/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/client-admin/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/client-customer/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/client-customer/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/client-admin/src/components/HomeComponent.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | class Home extends Component {
4 | render() {
5 | return (
6 |
7 |
ADMIN HOME
8 |

9 |
10 | );
11 | }
12 | }
13 | export default Home;
--------------------------------------------------------------------------------
/server/utils/MongooseUtil.js:
--------------------------------------------------------------------------------
1 | //CLI: npm install mongoose --save
2 | const mongoose = require('mongoose');
3 | const MyConstants = require('./MyConstants');
4 | const uri = 'mongodb+srv://' + MyConstants.DB_USER + ':' + MyConstants.DB_PASS + '@' + MyConstants.DB_SERVER + '/' + MyConstants.DB_DATABASE;
5 | mongoose.connect(uri, { useNewUrlParser: true })
6 | .then(() => { console.log('Connected to ' + MyConstants.DB_SERVER + '/' + MyConstants.DB_DATABASE); })
7 | .catch((err) => { console.error(err); });
--------------------------------------------------------------------------------
/client-customer/src/App.js:
--------------------------------------------------------------------------------
1 | import './App.css';
2 | import React, { Component } from 'react';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import MyProvider from './contexts/MyProvider';
5 | import Main from './components/MainComponent';
6 |
7 | class App extends Component {
8 | render() {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | );
16 | }
17 | }
18 | export default App;
--------------------------------------------------------------------------------
/client-admin/src/App.js:
--------------------------------------------------------------------------------
1 | import './App.css';
2 | import React, { Component } from 'react';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import MyProvider from './contexts/MyProvider';
5 |
6 | import Login from './components/LoginComponent';
7 | import Main from './components/MainComponent';
8 |
9 | class App extends Component {
10 | render() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 | }
21 | export default App;
--------------------------------------------------------------------------------
/server/utils/MyConstants.js:
--------------------------------------------------------------------------------
1 | const MyConstants = {
2 | DB_SERVER: 'onlineshopping.7eavwgm.mongodb.net',
3 | DB_USER: 'tiendung8a6',
4 | DB_PASS: 'C5QavQodd71CrZ0c',
5 | DB_DATABASE: 'onlineshopping',
6 | JWT_SECRET: 'tiendung8a6',
7 | JWT_EXPIRES: '31556952000', // in milliseconds (01 year = 31556952000 ms)
8 | EMAIL_USER: 'ngotdung2002@hotmail.com', // gmail service --Microsoft mail service
9 | EMAIL_PASS: 'ngotiendung123'
10 | };
11 | module.exports = MyConstants;
12 |
13 | //mongodb+srv://tiendung8a6:C5QavQodd71CrZ0c@onlineshopping.7eavwgm.mongodb.net/?retryWrites=true&w=majority
--------------------------------------------------------------------------------
/client-admin/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 |
7 | const root = ReactDOM.createRoot(document.getElementById('root'));
8 | root.render(
9 |
10 |
11 |
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | reportWebVitals();
18 |
--------------------------------------------------------------------------------
/client-admin/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/client-customer/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 |
7 | const root = ReactDOM.createRoot(document.getElementById('root'));
8 | root.render(
9 |
10 |
11 |
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | reportWebVitals();
18 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "(npm install) && (cd ../client-admin && npm install && npm run build) && (cd ../client-customer && npm install && npm run build)",
8 | "start": "node index.js"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "body-parser": "^1.20.2",
15 | "crypto": "^1.0.1",
16 | "express": "^4.18.2",
17 | "jsonwebtoken": "^9.0.0",
18 | "mongoose": "^7.0.4",
19 | "nodemailer": "^6.9.4"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/client-customer/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/resources/mongodb/customers.json:
--------------------------------------------------------------------------------
1 | [{
2 | "_id": {
3 | "$oid": "64ca21eb850d6136953f8337"
4 | },
5 | "username": "tiendung",
6 | "password": "123456",
7 | "name": "TienDung",
8 | "phone": "0985872885",
9 | "email": "tiendung8a6@gmail.com",
10 | "active": 1,
11 | "token": "b0533956c280059ffd46a6968f0472ae"
12 | },
13 | {
14 | "_id": {
15 | "$oid": "64ca234f850d6136953f833c"
16 | },
17 | "username": "tiendung_account2",
18 | "password": "123456",
19 | "name": "TienDung_account2",
20 | "phone": "0985872885",
21 | "email": "ngotdung2002@gmail.com",
22 | "active": 0,
23 | "token": "ef1cf711f592b724bcfedef9e7dd04bd"
24 | }]
--------------------------------------------------------------------------------
/client-admin/src/contexts/MyProvider.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import MyContext from './MyContext';
3 |
4 | class MyProvider extends Component {
5 | constructor(props) {
6 | super(props);
7 | this.state = { // global state
8 | // variables
9 | token: '',
10 | username: '',
11 | // functions
12 | setToken: this.setToken,
13 | setUsername: this.setUsername
14 | };
15 | }
16 | setToken = (value) => {
17 | this.setState({ token: value });
18 | }
19 | setUsername = (value) => {
20 | this.setState({ username: value });
21 | }
22 | render() {
23 | return (
24 |
25 | {this.props.children}
26 |
27 | );
28 | }
29 | }
30 | export default MyProvider;
--------------------------------------------------------------------------------
/client-customer/src/contexts/MyProvider.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import MyContext from './MyContext';
3 |
4 | class MyProvider extends Component {
5 | constructor(props) {
6 | super(props);
7 | this.state = { // global state
8 | // variables
9 | token: '',
10 | customer: null,
11 | mycart: [],
12 | // functions
13 | setToken: this.setToken,
14 | setCustomer: this.setCustomer,
15 | setMycart: this.setMycart
16 | };
17 | }
18 | setToken = (value) => {
19 | this.setState({ token: value });
20 | }
21 | setCustomer = (value) => {
22 | this.setState({ customer: value });
23 | }
24 | setMycart = (value) => {
25 | this.setState({ mycart: value });
26 | }
27 | render() {
28 | return (
29 |
30 | {this.props.children}
31 |
32 | );
33 | }
34 | }
35 | export default MyProvider;
--------------------------------------------------------------------------------
/server/models/OrderDAO.js:
--------------------------------------------------------------------------------
1 | require('../utils/MongooseUtil');
2 | const Models = require('./Models');
3 |
4 | const OrderDAO = {
5 | async insert(order) {
6 | const mongoose = require('mongoose');
7 | order._id = new mongoose.Types.ObjectId();
8 | const result = await Models.Order.create(order);
9 | return result;
10 | },
11 | async selectByCustID(_cid) {
12 | const query = { 'customer._id': _cid };
13 | const orders = await Models.Order.find(query).exec();
14 | return orders;
15 | },
16 | async selectAll() {
17 | const query = {};
18 | const mysort = { cdate: -1 }; // descending
19 | const orders = await Models.Order.find(query).sort(mysort).exec();
20 | return orders;
21 | },
22 | async update(_id, newStatus) {
23 | const newvalues = { status: newStatus };
24 | const result = await Models.Order.findByIdAndUpdate(_id, newvalues, { new: true });
25 | return result;
26 | }
27 | };
28 | module.exports = OrderDAO;
--------------------------------------------------------------------------------
/server/models/CategoryDAO.js:
--------------------------------------------------------------------------------
1 | require('../utils/MongooseUtil');
2 | const Models = require('./Models');
3 |
4 | const CategoryDAO = {
5 | async selectAll() {
6 | const query = {};
7 | const categories = await Models.Category.find(query).exec();
8 | return categories;
9 | },
10 | async insert(category) {
11 | const mongoose = require('mongoose');
12 | category._id = new mongoose.Types.ObjectId();
13 | const result = await Models.Category.create(category);
14 | return result;
15 | },
16 | async update(category) {
17 | const newvalues = { name: category.name }
18 | const result = await Models.Category.findByIdAndUpdate(category._id, newvalues, { new: true });
19 | return result;
20 | },
21 | async delete(_id) {
22 | const result = await Models.Category.findByIdAndRemove(_id);
23 | return result;
24 | },
25 | async selectByID(_id) {
26 | const category = await Models.Category.findById(_id).exec();
27 | return category;
28 | }
29 | };
30 | module.exports = CategoryDAO;
--------------------------------------------------------------------------------
/server/utils/EmailUtil.js:
--------------------------------------------------------------------------------
1 | //CLI: npm install nodemailer --save
2 | const nodemailer = require('nodemailer');
3 | const MyConstants = require('./MyConstants');
4 | const transporter = nodemailer.createTransport({
5 | host: 'smtp.office365.com',
6 | port: 587,
7 | secure: false,
8 | auth: {
9 | user: MyConstants.EMAIL_USER,
10 | pass: MyConstants.EMAIL_PASS
11 | }
12 | });
13 | const EmailUtil = {
14 | send(email, id, token) {
15 | const text = 'Thanks for signing up, please input these informations to activate your account:\n\t .id: ' + id + '\n\t .token: ' + token;
16 | return new Promise(function (resolve, reject) {
17 | const mailOptions = {
18 | from: MyConstants.EMAIL_USER,
19 | to: email,
20 | subject: 'Signup | Verification',
21 | text: text
22 | };
23 | transporter.sendMail(mailOptions, function (err, result) {
24 | if (err) reject(err);
25 | resolve(true);
26 | });
27 | });
28 | }
29 | };
30 | module.exports = EmailUtil;
--------------------------------------------------------------------------------
/server/utils/JwtUtil.js:
--------------------------------------------------------------------------------
1 | //CLI: npm install jsonwebtoken --save
2 | const jwt = require('jsonwebtoken');
3 | const MyConstants = require('./MyConstants');
4 | const JwtUtil = {
5 | genToken(username, password) {
6 | const token = jwt.sign(
7 | { username: username, password: password },
8 | MyConstants.JWT_SECRET,
9 | { expiresIn: MyConstants.JWT_EXPIRES }
10 | );
11 | return token;
12 | },
13 | checkToken(req, res, next) {
14 | const token = req.headers['x-access-token'] || req.headers['authorization'];
15 | if (token) {
16 | jwt.verify(token, MyConstants.JWT_SECRET, (err, decoded) => {
17 | if (err) {
18 | return res.json({
19 | success: false,
20 | message: 'Token is not valid'
21 | });
22 | } else {
23 | req.decoded = decoded;
24 | next();
25 | }
26 | });
27 | } else {
28 | return res.json({
29 | success: false,
30 | message: 'Auth token is not supplied'
31 | });
32 | }
33 | }
34 | };
35 | module.exports = JwtUtil;
--------------------------------------------------------------------------------
/client-admin/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client-admin",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.16.5",
7 | "@testing-library/react": "^13.4.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "axios": "^1.3.5",
10 | "react": "^18.2.0",
11 | "react-dom": "^18.2.0",
12 | "react-router-dom": "^6.10.0",
13 | "react-scripts": "5.0.1",
14 | "web-vitals": "^2.1.4"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject"
21 | },
22 | "eslintConfig": {
23 | "extends": [
24 | "react-app",
25 | "react-app/jest"
26 | ]
27 | },
28 | "browserslist": {
29 | "production": [
30 | ">0.2%",
31 | "not dead",
32 | "not op_mini all"
33 | ],
34 | "development": [
35 | "last 1 chrome version",
36 | "last 1 firefox version",
37 | "last 1 safari version"
38 | ]
39 | },
40 | "homepage": "/admin",
41 | "proxy": "http://localhost:3000"
42 | }
43 |
--------------------------------------------------------------------------------
/client-customer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client-customer",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.16.5",
7 | "@testing-library/react": "^13.4.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "axios": "^1.3.5",
10 | "react": "^18.2.0",
11 | "react-dom": "^18.2.0",
12 | "react-router-dom": "^6.10.0",
13 | "react-scripts": "5.0.1",
14 | "web-vitals": "^2.1.4"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject"
21 | },
22 | "eslintConfig": {
23 | "extends": [
24 | "react-app",
25 | "react-app/jest"
26 | ]
27 | },
28 | "browserslist": {
29 | "production": [
30 | ">0.2%",
31 | "not dead",
32 | "not op_mini all"
33 | ],
34 | "development": [
35 | "last 1 chrome version",
36 | "last 1 firefox version",
37 | "last 1 safari version"
38 | ]
39 | },
40 | "homepage": "/",
41 | "proxy": "http://localhost:3000"
42 | }
43 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | //CLI: npm install express body-parser --save
2 | const express = require('express');
3 | const app = express();
4 | const PORT = process.env.PORT || 3000;
5 | app.listen(PORT, () => {
6 | console.log(`Server listening on ${PORT}`);
7 | });
8 | // middlewares
9 | const bodyParser = require('body-parser');
10 | app.use(bodyParser.json({ limit: '10mb' }));
11 | app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' }));
12 | // apis
13 | app.get('/hello', (req, res) => { res.json({ message: 'Hello from server!' }); });
14 | app.use('/api/admin', require('./api/admin'));
15 | app.use('/api/customer', require('./api/customer'));
16 |
17 | // deployment
18 | const path = require('path');
19 | // '/admin' serve the files at client-admin/build/* as static files
20 | app.use('/admin', express.static(path.resolve(__dirname, '../client-admin/build')));
21 | app.get('admin/*', (req, res) => {
22 | res.sendFile(path.resolve(__dirname, '../client-admin/build', 'index.html'))
23 | });
24 | // '/' serve the files at client-customer/build/* as static files
25 | app.use('/', express.static(path.resolve(__dirname, '../client-customer/build')));
26 | app.get('*', (req, res) => {
27 | res.sendFile(path.resolve(__dirname, '../client-customer/build', 'index.html'));
28 | });
--------------------------------------------------------------------------------
/client-customer/src/components/InformComponent.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import MyContext from '../contexts/MyContext';
4 |
5 | class Inform extends Component {
6 | static contextType = MyContext; // using this.context to access global state
7 | render() {
8 | return (
9 |
10 |
11 | {this.context.token === '' ?
12 |
Login | Sign-up | Active
13 | :
14 |
Hello {this.context.customer.name} | this.lnkLogoutClick()}>Logout | My profile | My orders
15 | }
16 |
17 |
18 | My cart have {this.context.mycart.length} items
19 |
20 |
21 |
22 | );
23 | }
24 | // event-handlers
25 | lnkLogoutClick() {
26 | this.context.setToken('');
27 | this.context.setCustomer(null);
28 | this.context.setMycart([]);
29 | }
30 | }
31 | export default Inform;
--------------------------------------------------------------------------------
/client-admin/src/components/MainComponent.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Routes, Route, Navigate } from 'react-router-dom';
3 | import MyContext from '../contexts/MyContext';
4 |
5 | import Menu from './MenuComponent';
6 | import Home from './HomeComponent';
7 | import Category from './CategoryComponent';
8 | import Product from './ProductComponent';
9 | import Order from './OrderComponent';
10 | import Customer from './CustomerComponent';
11 |
12 | class Main extends Component {
13 | static contextType = MyContext; // using this.context to access global state
14 | render() {
15 | if (this.context.token !== '') {
16 | return (
17 |
18 |
19 |
20 | } />
21 | } />
22 | } />
23 | } />
24 | } />
25 | } />
26 |
27 |
28 | );
29 | }
30 | return ();
31 | }
32 | }
33 | export default Main;
--------------------------------------------------------------------------------
/client-admin/src/components/MenuComponent.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import MyContext from '../contexts/MyContext';
4 |
5 | class Menu extends Component {
6 | static contextType = MyContext; // using this.context to access global state
7 | render() {
8 | return (
9 |
10 |
11 |
12 | - Home
13 | - Category
14 | - Product
15 | - Order
16 | - Customer
17 |
18 |
19 |
20 | Hello {this.context.username} | this.lnkLogoutClick()}>Logout
21 |
22 |
23 |
24 | );
25 | }
26 | // event-handlers
27 | lnkLogoutClick() {
28 | this.context.setToken('');
29 | this.context.setUsername('');
30 | }
31 | }
32 | export default Menu;
--------------------------------------------------------------------------------
/client-customer/src/components/MainComponent.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Routes, Route, Navigate } from 'react-router-dom';
3 |
4 | import Menu from './MenuComponent';
5 | import Inform from './InformComponent';
6 | import Home from './HomeComponent';
7 | import Product from './ProductComponent';
8 | import ProductDetail from './ProductDetailComponent';
9 | import Signup from './SignupComponent';
10 | import Active from './ActiveComponent';
11 | import Login from './LoginComponent';
12 | import Myprofile from './MyprofileComponent';
13 | import Mycart from './MycartComponent';
14 | import Myorders from './MyordersComponent';
15 |
16 | class Main extends Component {
17 | render() {
18 | return (
19 |
20 |
21 |
22 |
23 | } />
24 | } />
25 | } />
26 | } />
27 | } />
28 | } />
29 | } />
30 | } />
31 | } />
32 | } />
33 | } />
34 |
35 |
36 | );
37 | }
38 | }
39 | export default Main;
--------------------------------------------------------------------------------
/server/models/CustomerDAO.js:
--------------------------------------------------------------------------------
1 | require('../utils/MongooseUtil');
2 | const Models = require('./Models');
3 |
4 | const CustomerDAO = {
5 | async selectByUsernameOrEmail(username, email) {
6 | const query = { $or: [{ username: username }, { email: email }] };
7 | const customer = await Models.Customer.findOne(query);
8 | return customer;
9 | },
10 | async insert(customer) {
11 | const mongoose = require('mongoose');
12 | customer._id = new mongoose.Types.ObjectId();
13 | const result = await Models.Customer.create(customer);
14 | return result;
15 | },
16 | async active(_id, token, active) {
17 | const query = { _id: _id, token: token };
18 | const newvalues = { active: active };
19 | const result = await Models.Customer.findOneAndUpdate(query, newvalues, { new: true });
20 | return result;
21 | },
22 | async selectByUsernameAndPassword(username, password) {
23 | const query = { username: username, password: password };
24 | const customer = await Models.Customer.findOne(query);
25 | return customer;
26 | },
27 | async update(customer) {
28 | const newvalues = { username: customer.username, password: customer.password, name: customer.name, phone: customer.phone, email: customer.email };
29 | const result = await Models.Customer.findByIdAndUpdate(customer._id, newvalues, { new: true });
30 | return result;
31 | },
32 | async selectAll() {
33 | const query = {};
34 | const customers = await Models.Customer.find(query).exec();
35 | return customers;
36 | },
37 | async selectByID(_id) {
38 | const customer = await Models.Customer.findById(_id).exec();
39 | return customer;
40 | }
41 | };
42 | module.exports = CustomerDAO;
--------------------------------------------------------------------------------
/server/models/Models.js:
--------------------------------------------------------------------------------
1 | //CLI: npm install mongoose --save
2 | const mongoose = require('mongoose');
3 | // schemas
4 | const AdminSchema = mongoose.Schema({
5 | _id: mongoose.Schema.Types.ObjectId,
6 | username: String,
7 | password: String
8 | }, { versionKey: false });
9 |
10 | const CategorySchema = mongoose.Schema({
11 | _id: mongoose.Schema.Types.ObjectId,
12 | name: String
13 | }, { versionKey: false });
14 |
15 | const CustomerSchema = mongoose.Schema({
16 | _id: mongoose.Schema.Types.ObjectId,
17 | username: String,
18 | password: String,
19 | name: String,
20 | phone: String,
21 | email: String,
22 | active: Number,
23 | token: String,
24 | }, { versionKey: false });
25 |
26 | const ProductSchema = mongoose.Schema({
27 | _id: mongoose.Schema.Types.ObjectId,
28 | name: String,
29 | price: Number,
30 | image: String,
31 | cdate: Number,
32 | category: CategorySchema
33 | }, { versionKey: false });
34 |
35 | const ItemSchema = mongoose.Schema({
36 | product: ProductSchema,
37 | quantity: Number
38 | }, { versionKey: false, _id: false });
39 |
40 | const OrderSchema = mongoose.Schema({
41 | _id: mongoose.Schema.Types.ObjectId,
42 | cdate: Number,
43 | total: Number,
44 | status: String,
45 | customer: CustomerSchema,
46 | items: [ItemSchema]
47 | }, { versionKey: false });
48 |
49 | // models
50 | const Admin = mongoose.model('Admin', AdminSchema);
51 | const Category = mongoose.model('Category', CategorySchema);
52 | const Customer = mongoose.model('Customer', CustomerSchema);
53 | const Product = mongoose.model('Product', ProductSchema);
54 | const Order = mongoose.model('Order', OrderSchema);
55 | module.exports = { Admin, Category, Customer, Product, Order };
--------------------------------------------------------------------------------
/client-customer/src/components/MenuComponent.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { Component } from 'react';
3 | import { Link } from 'react-router-dom';
4 | import withRouter from '../utils/withRouter';
5 |
6 | class Menu extends Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | categories: [],
11 | txtKeyword: ''
12 | };
13 | }
14 | render() {
15 | const cates = this.state.categories.map((item) => {
16 | return (
17 | {item.name}
18 | );
19 | });
20 | return (
21 |
22 |
23 |
24 | - Home
25 | {cates}
26 |
27 |
28 |
29 |
33 |
34 |
35 |
36 | );
37 | }
38 | componentDidMount() {
39 | this.apiGetCategories();
40 | }
41 | // event-handlers
42 | btnSearchClick(e) {
43 | e.preventDefault();
44 | this.props.navigate('/product/search/' + this.state.txtKeyword);
45 | }
46 | // apis
47 | apiGetCategories() {
48 | axios.get('/api/customer/categories').then((res) => {
49 | const result = res.data;
50 | this.setState({ categories: result });
51 | });
52 | }
53 | }
54 | export default withRouter(Menu);
--------------------------------------------------------------------------------
/client-admin/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/client-customer/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/client-customer/src/components/ActiveComponent.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { Component } from 'react';
3 |
4 | class Active extends Component {
5 | constructor(props) {
6 | super(props);
7 | this.state = {
8 | txtID: '',
9 | txtToken: ''
10 | };
11 | }
12 | render() {
13 | return (
14 |
15 |
ACTIVE ACCOUNT
16 |
34 |
35 | );
36 | }
37 | // event-handlers
38 | btnActiveClick(e) {
39 | e.preventDefault();
40 | const id = this.state.txtID;
41 | const token = this.state.txtToken;
42 | if (id && token) {
43 | this.apiActive(id, token);
44 | } else {
45 | alert('Please input id and token');
46 | }
47 | }
48 | // apis
49 | apiActive(id, token) {
50 | const body = { id: id, token: token };
51 | axios.post('/api/customer/active', body).then((res) => {
52 | const result = res.data;
53 | if (result) {
54 | alert('Good job!');
55 | } else {
56 | alert('Error! An error occurred. Please try again later.');
57 | }
58 | });
59 | }
60 | }
61 | export default Active;
--------------------------------------------------------------------------------
/client-admin/src/components/CategoryComponent.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { Component } from 'react';
3 | import MyContext from '../contexts/MyContext';
4 | import CategoryDetail from './CategoryDetailComponent';
5 |
6 | class Category extends Component {
7 | static contextType = MyContext; // using this.context to access global state
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | categories: [],
12 | itemSelected: null
13 | };
14 | }
15 | render() {
16 | const cates = this.state.categories.map((item) => {
17 | return (
18 | this.trItemClick(item)}>
19 | | {item._id} |
20 | {item.name} |
21 |
22 | );
23 | });
24 | return (
25 |
26 |
27 |
CATEGORY LIST
28 |
29 |
30 |
31 | | ID |
32 | Name |
33 |
34 | {cates}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | }
44 | componentDidMount() {
45 | this.apiGetCategories();
46 | }
47 | updateCategories = (categories) => { // arrow-function
48 | this.setState({ categories: categories });
49 | }
50 | // event-handlers
51 | trItemClick(item) {
52 | this.setState({ itemSelected: item });
53 | }
54 | // apis
55 | apiGetCategories() {
56 | const config = { headers: { 'x-access-token': this.context.token } };
57 | axios.get('/api/admin/categories', config).then((res) => {
58 | const result = res.data;
59 | this.setState({ categories: result });
60 | });
61 | }
62 | }
63 | export default Category;
--------------------------------------------------------------------------------
/client-admin/src/App.css:
--------------------------------------------------------------------------------
1 | /* styles.css */
2 | .body-customer {
3 | width: 1150px;
4 | height: fit-content;
5 | margin: 0px auto;
6 | }
7 |
8 | .body-admin {
9 | width: fit-content;
10 | height: fit-content;
11 | margin: 0px auto;
12 | }
13 |
14 | div.inline {
15 | display: inline-block;
16 | }
17 |
18 | figure.caption-right {
19 | display: flex;
20 | align-items: center;
21 | }
22 |
23 | .align-center {
24 | width: fit-content;
25 | margin: 0px auto;
26 | }
27 |
28 | .align-valign-center {
29 | position: absolute;
30 | top: 50%;
31 | left: 50%;
32 | transform: translate(-50%, -50%);
33 | padding: 0px 20px 20px 20px;
34 | border: 1px solid black;
35 | }
36 |
37 | .text-center {
38 | text-align: center;
39 | }
40 |
41 | span.link {
42 | cursor: pointer;
43 | color: blue;
44 | text-decoration: underline;
45 | }
46 |
47 | /* menu.css */
48 | div.border-bottom {
49 | border-bottom: 1px solid black;
50 | padding: 10px 0px 10px 0px;
51 | }
52 |
53 | div.float-left {
54 | float: left;
55 | }
56 |
57 | div.float-right {
58 | float: right;
59 | }
60 |
61 | div.float-clear {
62 | clear: both;
63 | }
64 |
65 | ul.menu {
66 | margin: 0px;
67 | padding: 0px;
68 | }
69 |
70 | li.menu {
71 | display: inline;
72 | margin-right: 20px;
73 | text-transform: uppercase;
74 | }
75 |
76 | form.search {
77 | margin: 0px;
78 | }
79 |
80 | input.keyword {
81 | width: 110px;
82 | }
83 |
84 | /* datatable.css */
85 | .datatable {
86 | text-align: center;
87 | border-collapse: collapse;
88 | }
89 |
90 | /* header */
91 | tr:first-child.datatable {
92 | background-color: #ffcc00;
93 | }
94 |
95 | /* even rows, except header */
96 | tr:not(:first-child):nth-child(even).datatable {
97 | background-color: #f5f5f5;
98 | }
99 |
100 | /* odd rows, except header */
101 | tr:not(:first-child):nth-child(odd).datatable {
102 | background-color: #ffeeaa;
103 | }
104 |
105 | /* except header */
106 | tr:not(:first-child):hover.datatable {
107 | background-color: #dcfcfc;
108 | cursor: pointer;
109 | }
--------------------------------------------------------------------------------
/client-customer/src/App.css:
--------------------------------------------------------------------------------
1 | /* styles.css */
2 | .body-customer {
3 | width: 1150px;
4 | height: fit-content;
5 | margin: 0px auto;
6 | }
7 |
8 | .body-admin {
9 | width: fit-content;
10 | height: fit-content;
11 | margin: 0px auto;
12 | }
13 |
14 | div.inline {
15 | display: inline-block;
16 | }
17 |
18 | figure.caption-right {
19 | display: flex;
20 | align-items: center;
21 | }
22 |
23 | .align-center {
24 | width: fit-content;
25 | margin: 0px auto;
26 | }
27 |
28 | .align-valign-center {
29 | position: absolute;
30 | top: 50%;
31 | left: 50%;
32 | transform: translate(-50%, -50%);
33 | padding: 0px 20px 20px 20px;
34 | border: 1px solid black;
35 | }
36 |
37 | .text-center {
38 | text-align: center;
39 | }
40 |
41 | span.link {
42 | cursor: pointer;
43 | color: blue;
44 | text-decoration: underline;
45 | }
46 |
47 | /* menu.css */
48 | div.border-bottom {
49 | border-bottom: 1px solid black;
50 | padding: 10px 0px 10px 0px;
51 | }
52 |
53 | div.float-left {
54 | float: left;
55 | }
56 |
57 | div.float-right {
58 | float: right;
59 | }
60 |
61 | div.float-clear {
62 | clear: both;
63 | }
64 |
65 | ul.menu {
66 | margin: 0px;
67 | padding: 0px;
68 | }
69 |
70 | li.menu {
71 | display: inline;
72 | margin-right: 20px;
73 | text-transform: uppercase;
74 | }
75 |
76 | form.search {
77 | margin: 0px;
78 | }
79 |
80 | input.keyword {
81 | width: 110px;
82 | }
83 |
84 | /* datatable.css */
85 | .datatable {
86 | text-align: center;
87 | border-collapse: collapse;
88 | }
89 |
90 | /* header */
91 | tr:first-child.datatable {
92 | background-color: #ffcc00;
93 | }
94 |
95 | /* even rows, except header */
96 | tr:not(:first-child):nth-child(even).datatable {
97 | background-color: #f5f5f5;
98 | }
99 |
100 | /* odd rows, except header */
101 | tr:not(:first-child):nth-child(odd).datatable {
102 | background-color: #ffeeaa;
103 | }
104 |
105 | /* except header */
106 | tr:not(:first-child):hover.datatable {
107 | background-color: #dcfcfc;
108 | cursor: pointer;
109 | }
--------------------------------------------------------------------------------
/client-customer/src/components/ProductComponent.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { Component } from 'react';
3 | import { Link } from 'react-router-dom';
4 | import withRouter from '../utils/withRouter';
5 |
6 | class Product extends Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | products: []
11 | };
12 | }
13 | render() {
14 | const prods = this.state.products.map((item) => {
15 | return (
16 |
17 |
18 |
19 | {item.name}
Price: {item.price}
20 |
21 |
22 | );
23 | });
24 | return (
25 |
26 |
LIST PRODUCTS
27 | {prods}
28 |
29 | );
30 | }
31 | componentDidMount() { // first: /product/...
32 | const params = this.props.params;
33 | if (params.cid) {
34 | this.apiGetProductsByCatID(params.cid);
35 | } else if (params.keyword) {
36 | this.apiGetProductsByKeyword(params.keyword);
37 | }
38 | }
39 | componentDidUpdate(prevProps) { // changed: /product/...
40 | const params = this.props.params;
41 | if (params.cid && params.cid !== prevProps.params.cid) {
42 | this.apiGetProductsByCatID(params.cid);
43 | } else if (params.keyword && params.keyword !== prevProps.params.keyword) {
44 | this.apiGetProductsByKeyword(params.keyword);
45 | }
46 | }
47 | // apis
48 | apiGetProductsByCatID(cid) {
49 | axios.get('/api/customer/products/category/' + cid).then((res) => {
50 | const result = res.data;
51 | this.setState({ products: result });
52 | });
53 | }
54 | apiGetProductsByKeyword(keyword) {
55 | axios.get('/api/customer/products/search/' + keyword).then((res) => {
56 | const result = res.data;
57 | this.setState({ products: result });
58 | });
59 | }
60 | }
61 | export default withRouter(Product);
--------------------------------------------------------------------------------
/client-customer/src/components/HomeComponent.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { Component } from 'react';
3 | import { Link } from 'react-router-dom';
4 |
5 | class Home extends Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | newprods: [],
10 | hotprods: []
11 | };
12 | }
13 | render() {
14 | const newprods = this.state.newprods.map((item) => {
15 | return (
16 |
17 |
18 |
19 | {item.name}
Price: {item.price}
20 |
21 |
22 | );
23 | });
24 | const hotprods = this.state.hotprods.map((item) => {
25 | return (
26 |
27 |
28 |
29 | {item.name}
Price: {item.price}
30 |
31 |
32 | );
33 | });
34 | return (
35 |
36 |
37 |
NEW PRODUCTS
38 | {newprods}
39 |
40 | {this.state.hotprods.length > 0 ?
41 |
42 |
HOT PRODUCTS
43 | {hotprods}
44 |
45 | :
}
46 |
47 | );
48 | }
49 | componentDidMount() {
50 | this.apiGetNewProducts();
51 | this.apiGetHotProducts();
52 | }
53 | // apis
54 | apiGetNewProducts() {
55 | axios.get('/api/customer/products/new').then((res) => {
56 | const result = res.data;
57 | this.setState({ newprods: result });
58 | });
59 | }
60 | apiGetHotProducts() {
61 | axios.get('/api/customer/products/hot').then((res) => {
62 | const result = res.data;
63 | this.setState({ hotprods: result });
64 | });
65 | }
66 | }
67 | export default Home;
--------------------------------------------------------------------------------
/client-customer/src/components/LoginComponent.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { Component } from 'react';
3 | import MyContext from '../contexts/MyContext';
4 | import withRouter from '../utils/withRouter';
5 |
6 | class Login extends Component {
7 | static contextType = MyContext; // using this.context to access global state
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | txtUsername: 'tiendung',
12 | txtPassword: '123456'
13 | };
14 | }
15 | render() {
16 | return (
17 |
18 |
CUSTOMER LOGIN
19 |
37 |
38 | );
39 | }
40 | // event-handlers
41 | btnLoginClick(e) {
42 | e.preventDefault();
43 | const username = this.state.txtUsername;
44 | const password = this.state.txtPassword;
45 | if (username && password) {
46 | const account = { username: username, password: password };
47 | this.apiLogin(account);
48 | } else {
49 | alert('Please input username and password');
50 | }
51 | }
52 | // apis
53 | apiLogin(account) {
54 | axios.post('/api/customer/login', account).then((res) => {
55 | const result = res.data;
56 | if (result.success === true) {
57 | this.context.setToken(result.token);
58 | this.context.setCustomer(result.customer);
59 | this.props.navigate('/home');
60 | } else {
61 | alert(result.message);
62 | }
63 | });
64 | }
65 | }
66 | export default withRouter(Login);
--------------------------------------------------------------------------------
/client-admin/src/components/LoginComponent.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { Component } from 'react';
3 | import MyContext from '../contexts/MyContext';
4 |
5 | class Login extends Component {
6 | static contextType = MyContext; // using this.context to access global state
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | txtUsername: 'admin',
11 | txtPassword: '123456'
12 | };
13 | }
14 | render() {
15 | if (this.context.token === '') {
16 | return (
17 |
18 |
ADMIN LOGIN
19 |
37 |
38 | );
39 | }
40 | return ();
41 | }
42 | // event-handlers
43 | btnLoginClick(e) {
44 | e.preventDefault();
45 | const username = this.state.txtUsername;
46 | const password = this.state.txtPassword;
47 | if (username && password) {
48 | const account = { username: username, password: password };
49 | this.apiLogin(account);
50 | } else {
51 | alert('Please input username and password');
52 | }
53 | }
54 | // apis
55 | apiLogin(account) {
56 | axios.post('/api/admin/login', account).then((res) => {
57 | const result = res.data;
58 | if (result.success === true) {
59 | this.context.setToken(result.token);
60 | this.context.setUsername(account.username);
61 | } else {
62 | alert(result.message);
63 | this.setState({ txtUsername: '', txtPassword: '' });
64 | }
65 | });
66 | }
67 | }
68 | export default Login;
--------------------------------------------------------------------------------
/client-admin/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client-customer/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/models/ProductDAO.js:
--------------------------------------------------------------------------------
1 | require('../utils/MongooseUtil');
2 | const Models = require('./Models');
3 |
4 | const ProductDAO = {
5 | async selectByCount() {
6 | const query = {};
7 | const noProducts = await Models.Product.find(query).count().exec();
8 | return noProducts;
9 | },
10 | async selectBySkipLimit(skip, limit) {
11 | const query = {};
12 | const products = await Models.Product.find(query).skip(skip).limit(limit).exec();
13 | return products;
14 | },
15 | async insert(product) {
16 | const mongoose = require('mongoose');
17 | product._id = new mongoose.Types.ObjectId();
18 | const result = await Models.Product.create(product);
19 | return result;
20 | },
21 | async selectByID(_id) {
22 | const product = await Models.Product.findById(_id).exec();
23 | return product;
24 | },
25 | async update(product) {
26 | const newvalues = { name: product.name, price: product.price, image: product.image, category: product.category };
27 | const result = await Models.Product.findByIdAndUpdate(product._id, newvalues, { new: true });
28 | return result;
29 | },
30 | async delete(_id) {
31 | const result = await Models.Product.findByIdAndRemove(_id);
32 | return result;
33 | },
34 | async selectByID(_id) {
35 | const product = await Models.Product.findById(_id).exec();
36 | return product;
37 | },
38 | async selectTopNew(top) {
39 | const query = {};
40 | const mysort = { cdate: -1 }; // descending
41 | const products = await Models.Product.find(query).sort(mysort).limit(top).exec();
42 | return products;
43 | },
44 | async selectTopHot(top) {
45 | const items = await Models.Order.aggregate([
46 | { $match: { status: 'APPROVED' } },
47 | { $unwind: '$items' },
48 | { $group: { _id: '$items.product._id', sum: { $sum: '$items.quantity' } } },
49 | { $sort: { sum: -1 } }, // descending
50 | { $limit: top }
51 | ]).exec();
52 | var products = [];
53 | for (const item of items) {
54 | const product = await ProductDAO.selectByID(item._id);
55 | products.push(product);
56 | }
57 | return products;
58 | },
59 | async selectByCatID(_cid) {
60 | const query = { 'category._id': _cid };
61 | const products = await Models.Product.find(query).exec();
62 | return products;
63 | },
64 | async selectByKeyword(keyword) {
65 | const query = { name: { $regex: new RegExp(keyword, "i") } };
66 | const products = await Models.Product.find(query).exec();
67 | return products;
68 | }
69 | };
70 | module.exports = ProductDAO;
--------------------------------------------------------------------------------
/client-customer/src/components/SignupComponent.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { Component } from 'react';
3 |
4 | class Signup extends Component {
5 | constructor(props) {
6 | super(props);
7 | this.state = {
8 | txtUsername: 'tiendung',
9 | txtPassword: '123456',
10 | txtName: 'TienDung',
11 | txtPhone: '0985872885',
12 | txtEmail: 'tiendung8a6@gmail.com'
13 | };
14 | }
15 | render() {
16 | return (
17 |
50 | );
51 | }
52 | // event-handlers
53 | btnSignupClick(e) {
54 | e.preventDefault();
55 | const username = this.state.txtUsername;
56 | const password = this.state.txtPassword;
57 | const name = this.state.txtName;
58 | const phone = this.state.txtPhone;
59 | const email = this.state.txtEmail;
60 | if (username && password && name && phone && email) {
61 | const account = { username: username, password: password, name: name, phone: phone, email: email };
62 | this.apiSignup(account);
63 | } else {
64 | alert('Please input username and password and name and phone and email');
65 | }
66 | }
67 | // apis
68 | apiSignup(account) {
69 | axios.post('/api/customer/signup', account).then((res) => {
70 | const result = res.data;
71 | alert(result.message);
72 | });
73 | }
74 | }
75 | export default Signup;
--------------------------------------------------------------------------------
/client-admin/src/components/ProductComponent.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { Component } from 'react';
3 | import MyContext from '../contexts/MyContext';
4 | import ProductDetail from './ProductDetailComponent';
5 |
6 | class Product extends Component {
7 | static contextType = MyContext; // using this.context to access global state
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | products: [],
12 | noPages: 0,
13 | curPage: 1,
14 | itemSelected: null
15 | };
16 | }
17 | render() {
18 | const prods = this.state.products.map((item) => {
19 | return (
20 | this.trItemClick(item)}>
21 | | {item._id} |
22 | {item.name} |
23 | {item.price} |
24 | {new Date(item.cdate).toLocaleString()} |
25 | {item.category.name} |
26 |  |
27 |
28 | );
29 | });
30 | const pagination = Array.from({ length: this.state.noPages }, (_, index) => {
31 | if ((index + 1) === this.state.curPage) {
32 | return (| {index + 1} |);
33 | } else {
34 | return ( this.lnkPageClick(index + 1)}>| {index + 1} |);
35 | }
36 | });
37 | return (
38 |
39 |
40 |
PRODUCT LIST
41 |
42 |
43 |
44 | | ID |
45 | Name |
46 | Price |
47 | Creation date |
48 | Category |
49 | Image |
50 |
51 | {prods}
52 |
53 | | {pagination} |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | );
63 | }
64 | componentDidMount() {
65 | this.apiGetProducts(this.state.curPage);
66 | }
67 | updateProducts = (products, noPages, curPage) => { // arrow-function
68 | this.setState({ products: products, noPages: noPages, curPage: curPage });
69 | }
70 | // event-handlers
71 | lnkPageClick(index) {
72 | this.apiGetProducts(index);
73 | }
74 | trItemClick(item) {
75 | this.setState({ itemSelected: item });
76 | }
77 | // apis
78 | apiGetProducts(page) {
79 | const config = { headers: { 'x-access-token': this.context.token } };
80 | axios.get('/api/admin/products?page=' + page, config).then((res) => {
81 | const result = res.data;
82 | this.setState({ products: result.products, noPages: result.noPages, curPage: result.curPage });
83 | });
84 | }
85 | }
86 | export default Product;
--------------------------------------------------------------------------------
/client-customer/src/components/ProductDetailComponent.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { Component } from 'react';
3 | import MyContext from '../contexts/MyContext';
4 | import withRouter from '../utils/withRouter';
5 |
6 | class ProductDetail extends Component {
7 | static contextType = MyContext; // using this.context to access global state
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | product: null,
12 | txtQuantity: 1
13 | };
14 | }
15 | render() {
16 | const prod = this.state.product;
17 | if (prod != null) {
18 | return (
19 |
20 |
PRODUCT DETAILS
21 |
22 |
23 |
24 |
54 |
55 |
56 |
57 | );
58 | }
59 | return ();
60 | }
61 | componentDidMount() {
62 | const params = this.props.params;
63 | this.apiGetProduct(params.id);
64 | }
65 | // event-handlers
66 | btnAdd2CartClick(e) {
67 | e.preventDefault();
68 | const product = this.state.product;
69 | const quantity = parseInt(this.state.txtQuantity);
70 | if (quantity) {
71 | const mycart = this.context.mycart;
72 | const index = mycart.findIndex(x => x.product._id === product._id); // check if the _id exists in mycart
73 | if (index === -1) { // not found, push newItem
74 | const newItem = { product: product, quantity: quantity };
75 | mycart.push(newItem);
76 | } else { // increasing the quantity
77 | mycart[index].quantity += quantity;
78 | }
79 | this.context.setMycart(mycart);
80 | alert('Good job!');
81 | } else {
82 | alert('Please input quantity');
83 | }
84 | }
85 | // apis
86 | apiGetProduct(id) {
87 | axios.get('/api/customer/products/' + id).then((res) => {
88 | const result = res.data;
89 | this.setState({ product: result });
90 | });
91 | }
92 | }
93 | export default withRouter(ProductDetail);
--------------------------------------------------------------------------------
/client-customer/src/components/MycartComponent.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { Component } from 'react';
3 | import MyContext from '../contexts/MyContext';
4 | import CartUtil from '../utils/CartUtil';
5 | import withRouter from '../utils/withRouter';
6 |
7 | class Mycart extends Component {
8 | static contextType = MyContext; // using this.context to access global state
9 | render() {
10 | const mycart = this.context.mycart.map((item, index) => {
11 | return (
12 |
13 | | {index + 1} |
14 | {item.product._id} |
15 | {item.product.name} |
16 | {item.product.category.name} |
17 |  |
18 | {item.product.price} |
19 | {item.quantity} |
20 | {item.product.price * item.quantity} |
21 | this.lnkRemoveClick(item.product._id)}>Remove |
22 |
23 | );
24 | });
25 | return (
26 |
27 |
ITEM LIST
28 |
29 |
30 |
31 | | No. |
32 | ID |
33 | Name |
34 | Category |
35 | Image |
36 | Price |
37 | Quantity |
38 | Amount |
39 | Action |
40 |
41 | {mycart}
42 |
43 | |
44 | Total |
45 | {CartUtil.getTotal(this.context.mycart)} |
46 | this.lnkCheckoutClick()}>CHECKOUT |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 | // event-handlers
54 | lnkRemoveClick(id) {
55 | const mycart = this.context.mycart;
56 | const index = mycart.findIndex(x => x.product._id === id);
57 | if (index !== -1) { // found, remove item
58 | mycart.splice(index, 1);
59 | this.context.setMycart(mycart);
60 | }
61 | }
62 | lnkCheckoutClick() {
63 | if (window.confirm('ARE YOU SURE?')) {
64 | if (this.context.mycart.length > 0) {
65 | const total = CartUtil.getTotal(this.context.mycart);
66 | const items = this.context.mycart;
67 | const customer = this.context.customer;
68 | if (customer) {
69 | this.apiCheckout(total, items, customer);
70 | } else {
71 | this.props.navigate('/login');
72 | }
73 | } else {
74 | alert('Your cart is empty');
75 | }
76 | }
77 | }
78 | // apis
79 | apiCheckout(total, items, customer) {
80 | const body = { total: total, items: items, customer: customer };
81 | const config = { headers: { 'x-access-token': this.context.token } };
82 | axios.post('/api/customer/checkout', body, config).then((res) => {
83 | const result = res.data;
84 | if (result) {
85 | alert('Good job!');
86 | this.context.setMycart([]);
87 | this.props.navigate('/home');
88 | } else {
89 | alert('Error! An error occurred. Please try again later.');
90 | }
91 | });
92 | }
93 | }
94 | export default withRouter(Mycart);
--------------------------------------------------------------------------------
/client-customer/src/components/MyordersComponent.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { Component } from 'react';
3 | import { Navigate } from 'react-router-dom';
4 | import MyContext from '../contexts/MyContext';
5 |
6 | class Myorders extends Component {
7 | static contextType = MyContext; // using this.context to access global state
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | orders: [],
12 | order: null
13 | };
14 | }
15 | render() {
16 | if (this.context.token === '') return ();
17 | const orders = this.state.orders.map((item) => {
18 | return (
19 | this.trItemClick(item)}>
20 | | {item._id} |
21 | {new Date(item.cdate).toLocaleString()} |
22 | {item.customer.name} |
23 | {item.customer.phone} |
24 | {item.total} |
25 | {item.status} |
26 |
27 | );
28 | });
29 | if (this.state.order) {
30 | var items = this.state.order.items.map((item, index) => {
31 | return (
32 |
33 | | {index + 1} |
34 | {item.product._id} |
35 | {item.product.name} |
36 |  |
37 | {item.product.price} |
38 | {item.quantity} |
39 | {item.product.price * item.quantity} |
40 |
41 | );
42 | });
43 | }
44 | return (
45 |
46 |
47 |
ORDER LIST
48 |
49 |
50 |
51 | | ID |
52 | Creation date |
53 | Cust.name |
54 | Cust.phone |
55 | Total |
56 | Status |
57 |
58 | {orders}
59 |
60 |
61 |
62 | {this.state.order ?
63 |
64 |
ORDER DETAIL
65 |
66 |
67 |
68 | | No. |
69 | Prod.ID |
70 | Prod.name |
71 | Image |
72 | Price |
73 | Quantity |
74 | Amount |
75 |
76 | {items}
77 |
78 |
79 |
80 | :
}
81 |
82 | );
83 | }
84 | componentDidMount() {
85 | if (this.context.customer) {
86 | const cid = this.context.customer._id;
87 | this.apiGetOrdersByCustID(cid);
88 | }
89 | }
90 | // event-handlers
91 | trItemClick(item) {
92 | this.setState({ order: item });
93 | }
94 | // apis
95 | apiGetOrdersByCustID(cid) {
96 | const config = { headers: { 'x-access-token': this.context.token } };
97 | axios.get('/api/customer/orders/customer/' + cid, config).then((res) => {
98 | const result = res.data;
99 | this.setState({ orders: result });
100 | });
101 | }
102 | }
103 | export default Myorders;
--------------------------------------------------------------------------------
/client-customer/src/components/MyprofileComponent.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { Component } from 'react';
3 | import { Navigate } from 'react-router-dom';
4 | import MyContext from '../contexts/MyContext';
5 |
6 | class Myprofile extends Component {
7 | static contextType = MyContext; // using this.context to access global state
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | txtUsername: '',
12 | txtPassword: '',
13 | txtName: '',
14 | txtPhone: '',
15 | txtEmail: ''
16 | };
17 | }
18 | render() {
19 | if (this.context.token === '') return ();
20 | return (
21 |
22 |
MY PROFILE
23 |
53 |
54 | );
55 | }
56 | componentDidMount() {
57 | if (this.context.customer) {
58 | this.setState({
59 | txtUsername: this.context.customer.username,
60 | txtPassword: this.context.customer.password,
61 | txtName: this.context.customer.name,
62 | txtPhone: this.context.customer.phone,
63 | txtEmail: this.context.customer.email
64 | });
65 | }
66 | }
67 | // event-handlers
68 | btnUpdateClick(e) {
69 | e.preventDefault();
70 | const username = this.state.txtUsername;
71 | const password = this.state.txtPassword;
72 | const name = this.state.txtName;
73 | const phone = this.state.txtPhone;
74 | const email = this.state.txtEmail;
75 | if (username && password && name && phone && email) {
76 | const customer = { username: username, password: password, name: name, phone: phone, email: email };
77 | this.apiPutCustomer(this.context.customer._id, customer);
78 | } else {
79 | alert('Please input username and password and name and phone and email');
80 | }
81 | }
82 | // apis
83 | apiPutCustomer(id, customer) {
84 | const config = { headers: { 'x-access-token': this.context.token } };
85 | axios.put('/api/customer/customers/' + id, customer, config).then((res) => {
86 | const result = res.data;
87 | if (result) {
88 | alert('Good job!');
89 | this.context.setCustomer(result);
90 | } else {
91 | alert('Error! An error occurred. Please try again later.');
92 | }
93 | });
94 | }
95 | }
96 | export default Myprofile;
--------------------------------------------------------------------------------
/client-admin/src/components/OrderComponent.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { Component } from 'react';
3 | import MyContext from '../contexts/MyContext';
4 |
5 | class Order extends Component {
6 | static contextType = MyContext; // using this.context to access global state
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | orders: [],
11 | order: null
12 | };
13 | }
14 | render() {
15 | const orders = this.state.orders.map((item) => {
16 | return (
17 | this.trItemClick(item)}>
18 | | {item._id} |
19 | {new Date(item.cdate).toLocaleString()} |
20 | {item.customer.name} |
21 | {item.customer.phone} |
22 | {item.total} |
23 | {item.status} |
24 |
25 | {item.status === 'PENDING' ?
26 | this.lnkApproveClick(item._id)}>APPROVE || this.lnkCancelClick(item._id)}>CANCEL
27 | : }
28 | |
29 |
30 | );
31 | });
32 | if (this.state.order) {
33 | var items = this.state.order.items.map((item, index) => {
34 | return (
35 |
36 | | {index + 1} |
37 | {item.product._id} |
38 | {item.product.name} |
39 |  |
40 | {item.product.price} |
41 | {item.quantity} |
42 | {item.product.price * item.quantity} |
43 |
44 | );
45 | });
46 | }
47 | return (
48 |
49 |
50 |
ORDER LIST
51 |
52 |
53 |
54 | | ID |
55 | Creation date |
56 | Cust.name |
57 | Cust.phone |
58 | Total |
59 | Status |
60 | Action |
61 |
62 | {orders}
63 |
64 |
65 |
66 | {this.state.order ?
67 |
68 |
ORDER DETAIL
69 |
70 |
71 |
72 | | No. |
73 | Prod.ID |
74 | Prod.name |
75 | Image |
76 | Price |
77 | Quantity |
78 | Amount |
79 |
80 | {items}
81 |
82 |
83 |
84 | :
}
85 |
86 | );
87 | }
88 | componentDidMount() {
89 | this.apiGetOrders();
90 | }
91 | // event-handlers
92 | trItemClick(item) {
93 | this.setState({ order: item });
94 | }
95 | lnkApproveClick(id) {
96 | this.apiPutOrderStatus(id, 'APPROVED');
97 | }
98 | lnkCancelClick(id) {
99 | this.apiPutOrderStatus(id, 'CANCELED');
100 | }
101 | // apis
102 | apiGetOrders() {
103 | const config = { headers: { 'x-access-token': this.context.token } };
104 | axios.get('/api/admin/orders', config).then((res) => {
105 | const result = res.data;
106 | this.setState({ orders: result });
107 | });
108 | }
109 | apiPutOrderStatus(id, status) {
110 | const body = { status: status };
111 | const config = { headers: { 'x-access-token': this.context.token } };
112 | axios.put('/api/admin/orders/status/' + id, body, config).then((res) => {
113 | const result = res.data;
114 | if (result) {
115 | this.apiGetOrders();
116 | } else {
117 | alert('Error! An error occurred. Please try again later.');
118 | }
119 | });
120 | }
121 | }
122 | export default Order;
--------------------------------------------------------------------------------
/client-admin/src/components/CategoryDetailComponent.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { Component } from 'react';
3 | import MyContext from '../contexts/MyContext';
4 |
5 | class CategoryDetail extends Component {
6 | static contextType = MyContext; // using this.context to access global state
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | txtID: '',
11 | txtName: ''
12 | };
13 | }
14 | render() {
15 | return (
16 |
17 |
CATEGORY DETAIL
18 |
40 |
41 | );
42 | }
43 | componentDidUpdate(prevProps) {
44 | if (this.props.item !== prevProps.item) {
45 | this.setState({ txtID: this.props.item._id, txtName: this.props.item.name });
46 | }
47 | }
48 | // event-handlers
49 | btnAddClick(e) {
50 | e.preventDefault();
51 | const name = this.state.txtName;
52 | if (name) {
53 | const cate = { name: name };
54 | this.apiPostCategory(cate);
55 | } else {
56 | alert('Please input name');
57 | }
58 | }
59 | btnUpdateClick(e) {
60 | e.preventDefault();
61 | const id = this.state.txtID;
62 | const name = this.state.txtName;
63 | if (id && name) {
64 | const cate = { name: name };
65 | this.apiPutCategory(id, cate);
66 | } else {
67 | alert('Please input id and name');
68 | }
69 | }
70 | btnDeleteClick(e) {
71 | e.preventDefault();
72 | if (window.confirm('ARE YOU SURE?')) {
73 | const id = this.state.txtID;
74 | if (id) {
75 | this.apiDeleteCategory(id);
76 | } else {
77 | alert('Please input id');
78 | }
79 | }
80 | }
81 | // apis
82 | apiPostCategory(cate) {
83 | const config = { headers: { 'x-access-token': this.context.token } };
84 | axios.post('/api/admin/categories', cate, config).then((res) => {
85 | const result = res.data;
86 | if (result) {
87 | alert('Good job!');
88 | this.apiGetCategories();
89 | } else {
90 | alert('Error! An error occurred. Please try again later.');
91 | }
92 | });
93 | }
94 | apiPutCategory(id, cate) {
95 | const config = { headers: { 'x-access-token': this.context.token } };
96 | axios.put('/api/admin/categories/' + id, cate, config).then((res) => {
97 | const result = res.data;
98 | if (result) {
99 | alert('Good job!');
100 | this.apiGetCategories();
101 | } else {
102 | alert('Error! An error occurred. Please try again later.');
103 | }
104 | });
105 | }
106 | apiDeleteCategory(id) {
107 | const config = { headers: { 'x-access-token': this.context.token } };
108 | axios.delete('/api/admin/categories/' + id, config).then((res) => {
109 | const result = res.data;
110 | if (result) {
111 | alert('Good job!');
112 | this.apiGetCategories();
113 | } else {
114 | alert('Error! An error occurred. Please try again later.');
115 | }
116 | });
117 | }
118 | apiGetCategories() {
119 | const config = { headers: { 'x-access-token': this.context.token } };
120 | axios.get('/api/admin/categories', config).then((res) => {
121 | const result = res.data;
122 | this.props.updateCategories(result);
123 | });
124 | }
125 | }
126 | export default CategoryDetail;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Online Shopping - MERN Stack
2 |
3 | ## Introduction
4 | Welcome to the README for the Online Shopping project built using the MERN Stack. This is not a real e-commerce website but rather an educational project for learning and practicing technologies and skills related to the MERN Stack.
5 |
6 | ## MERN Stack Overview
7 | The MERN stack is a free and open-source JavaScript software stack for building dynamic web sites and web applications which has the following components:
8 | - **M**: MongoDB - the standard NoSQL database.
9 | - **E**: Express.js - the default web applications framework for building RESTful APIs.
10 | - **R**: React.js - the JavaScript library used for building UI components.
11 | - **N**: Node.js - the cross-platform, open-source server environment that can run on
12 | Windows, Linux, Unix, macOS, and more...
13 |
14 | By combining these technologies, we can efficiently build single-page web applications with fast performance and smooth user experiences.
15 |
16 |
17 |
18 |
19 |
20 | ## Project Objectives
21 |
22 | The main objective of the Online Shopping project is to create a web application that allows users to view product listings, add products to the cart, and simulate the checkout process. This project will help us practice and gain a deeper understanding of the following aspects:
23 |
24 | - Building a MongoDB database to store product information and orders.
25 | - Using Express.js to create APIs for interacting with the database.
26 | - Creating interactive user interfaces using React.
27 | - Handling user registration, login, and account management.
28 | - Integrating shopping cart and payment functionalities.
29 |
30 | ## Getting Started
31 |
32 | If you want to run this project on your computer, follow these steps:
33 |
34 | #### 1. Clone the repository from GitHub:
35 |
36 | ```bash
37 | git clone https://github.com/tiendung8a6/Online-Shopping-MERN-Stack.git
38 | ```
39 | #### 2. Install dependencies:
40 | ```bash
41 | cd server
42 | npm install
43 | ```
44 | ```bash
45 | cd client-admin
46 | npm install
47 | ```
48 | ```bash
49 | cd client-customer
50 | npm install
51 | ```
52 | #### 3. Configure the MongoDB database:
53 | Ensure you have MongoDB installed and running on your computer, and update the connection URL in the `server/utils/MyConstants.js` file.
54 |
55 | #### 4. Run the application:
56 | ```bash
57 | cd server
58 | npm start
59 | ```
60 | ```bash
61 | cd client-admin
62 | npm start
63 | ```
64 | ```bash
65 | cd client-customer
66 | npm start
67 | ```
68 | ## Directory Structure
69 |
70 | The project directory includes the following folders:
71 |
72 | - **client-admin**: Contains the client-side source code for the admin interface built with React.js.
73 | - **client-customer**: Contains the client-side source code for the customer interface built with React.js.
74 | - **resources**: Holds various project resources.
75 | - ***images***: Contains images used in the project.
76 | - ***mongodb***: Contains JSON files that can be imported into the MongoDB database.
77 | - ***postman***: Contains the API collection for Postman, facilitating API testing and documentation.
78 | - **server**: Holds the server-side source code built with Node.js and Express.js.
79 |
80 | ## How to Use
81 |
82 | After accessing the website, you can register, log in, view the product list, add products to the cart and simulate the checkout process. If you want to manage customer accounts, manage products, manage categories, manage orders, then you can login to the admin page.
83 |
84 | ## Main Technologies Used
85 |
86 | + **Front-end**: React.js, HTML, CSS, JavaScript
87 | + **Back-end**: Node.js, Express.js, MongoDB
88 | + **Package and Tool Management**: npm, Git
89 |
90 | ## References
91 |
92 | + [Lab-01.pdf](https://drive.google.com/file/d/1D6JsGJ97Z4orF8oZet4JBQ4NJZWZTs0B/view?usp=sharing)
93 | + [Lab-02.pdf](https://drive.google.com/file/d/1NqOYNc0meQyOAIyL0hnFiGrEH-Uw5S_d/view?usp=sharing)
94 | + [Lab-03.pdf](https://drive.google.com/file/d/1pEDyCfB1XLRcvosGFzSnbFm_G245UoU0/view?usp=sharing)
95 | + [Lab-04.pdf](https://drive.google.com/file/d/1PIeC8OYLWIj3tXVxCkSQkfi-h4p24DMh/view?usp=sharing)
96 | + [Lab-05.pdf](https://drive.google.com/file/d/1PzYW9zkeuNeyDoZkVOYHiUnGmdLNKb_V/view?usp=sharing)
97 | + [Lab-06.pdf](https://drive.google.com/file/d/1Nj_fXcGgIp9fOQDu-hrPX44BOmy8309b/view?usp=sharing)
98 | + [Lab-07.pdf](https://drive.google.com/file/d/1vtQ_BYIev6a89jPs4egksBCjKZ5U9VdP/view?usp=sharing)
99 | + [Lab-08.pdf](https://drive.google.com/file/d/1DcL7ozWP1lkBxXXacHiUl0wvh2aoXJnF/view?usp=sharing)
100 | + [Lab-09.pdf](https://drive.google.com/file/d/11RfLKb_mCW7gvGGWFBqQPfMLz3TUkUSm/view?usp=sharing)
101 | + [Lab-10.pdf](https://drive.google.com/file/d/1zJO-AY2ALDBPvdI3YR5U-FXPOzhJEDEr/view?usp=sharing)
102 | + [Pro MERN Stack: Full Stack Web App Development with Mongo, Express, React, and Node.pdf](https://drive.google.com/file/d/1jHtj0QknhbtrVoQifMZnCUCnO66GnbjK/view?usp=sharing)
103 |
104 | ## Author
105 | - **Name**: Ngo Tien Dung
106 | - **Contact**: tiendung8a6@gmail.com
107 | - **Phone**: +84 985872885
108 |
--------------------------------------------------------------------------------
/server/api/customer.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | // utils
4 | const CryptoUtil = require('../utils/CryptoUtil');
5 | const EmailUtil = require('../utils/EmailUtil');
6 | const JwtUtil = require('../utils/JwtUtil');
7 | // daos
8 | const CategoryDAO = require('../models/CategoryDAO');
9 | const ProductDAO = require('../models/ProductDAO');
10 | const CustomerDAO = require('../models/CustomerDAO');
11 | const OrderDAO = require('../models/OrderDAO');
12 | // category
13 | router.get('/categories', async function (req, res) {
14 | const categories = await CategoryDAO.selectAll();
15 | res.json(categories);
16 | });
17 | // product
18 | router.get('/products/new', async function (req, res) {
19 | const products = await ProductDAO.selectTopNew(3);
20 | res.json(products);
21 | });
22 | router.get('/products/hot', async function (req, res) {
23 | const products = await ProductDAO.selectTopHot(3);
24 | res.json(products);
25 | });
26 | router.get('/products/category/:cid', async function (req, res) {
27 | const _cid = req.params.cid;
28 | const products = await ProductDAO.selectByCatID(_cid);
29 | res.json(products);
30 | });
31 | router.get('/products/search/:keyword', async function (req, res) {
32 | const keyword = req.params.keyword;
33 | const products = await ProductDAO.selectByKeyword(keyword);
34 | res.json(products);
35 | });
36 | router.get('/products/:id', async function (req, res) {
37 | const _id = req.params.id;
38 | const product = await ProductDAO.selectByID(_id);
39 | res.json(product);
40 | });
41 | // customer
42 | router.post('/signup', async function (req, res) {
43 | const username = req.body.username;
44 | const password = req.body.password;
45 | const name = req.body.name;
46 | const phone = req.body.phone;
47 | const email = req.body.email;
48 | const dbCust = await CustomerDAO.selectByUsernameOrEmail(username, email);
49 | if (dbCust) {
50 | res.json({ success: false, message: 'Exists username or email' });
51 | } else {
52 | const now = new Date().getTime(); // milliseconds
53 | const token = CryptoUtil.md5(now.toString());
54 | const newCust = { username: username, password: password, name: name, phone: phone, email: email, active: 0, token: token };
55 | const result = await CustomerDAO.insert(newCust);
56 | if (result) {
57 | const send = await EmailUtil.send(email, result._id, token);
58 | if (send) {
59 | res.json({ success: true, message: 'Please check email' });
60 | } else {
61 | res.json({ success: false, message: 'Email failure' });
62 | }
63 | } else {
64 | res.json({ success: false, message: 'Insert failure' });
65 | }
66 | }
67 | });
68 | router.post('/active', async function (req, res) {
69 | const _id = req.body.id;
70 | const token = req.body.token;
71 | const result = await CustomerDAO.active(_id, token, 1);
72 | res.json(result);
73 | });
74 | router.post('/login', async function (req, res) {
75 | const username = req.body.username;
76 | const password = req.body.password;
77 | if (username && password) {
78 | const customer = await CustomerDAO.selectByUsernameAndPassword(username, password);
79 | if (customer) {
80 | if (customer.active === 1) {
81 | const token = JwtUtil.genToken();
82 | res.json({ success: true, message: 'Authentication successful', token: token, customer: customer });
83 | } else {
84 | res.json({ success: false, message: 'Account is deactive' });
85 | }
86 | } else {
87 | res.json({ success: false, message: 'Incorrect username or password' });
88 | }
89 | } else {
90 | res.json({ success: false, message: 'Please input username and password' });
91 | }
92 | });
93 | router.get('/token', JwtUtil.checkToken, function (req, res) {
94 | const token = req.headers['x-access-token'] || req.headers['authorization'];
95 | res.json({ success: true, message: 'Token is valid', token: token });
96 | });
97 | // myprofile
98 | router.put('/customers/:id', JwtUtil.checkToken, async function (req, res) {
99 | const _id = req.params.id;
100 | const username = req.body.username;
101 | const password = req.body.password;
102 | const name = req.body.name;
103 | const phone = req.body.phone;
104 | const email = req.body.email;
105 | const customer = { _id: _id, username: username, password: password, name: name, phone: phone, email: email };
106 | const result = await CustomerDAO.update(customer);
107 | res.json(result);
108 | });
109 | // mycart
110 | router.post('/checkout', JwtUtil.checkToken, async function (req, res) {
111 | const now = new Date().getTime(); // milliseconds
112 | const total = req.body.total;
113 | const items = req.body.items;
114 | const customer = req.body.customer;
115 | const order = { cdate: now, total: total, status: 'PENDING', customer: customer, items: items };
116 | const result = await OrderDAO.insert(order);
117 | res.json(result);
118 | });
119 | // myorders
120 | router.get('/orders/customer/:cid', JwtUtil.checkToken, async function (req, res) {
121 | const _cid = req.params.cid;
122 | const orders = await OrderDAO.selectByCustID(_cid);
123 | res.json(orders);
124 | });
125 | module.exports = router;
--------------------------------------------------------------------------------
/server/api/admin.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | // utils
4 | const JwtUtil = require('../utils/JwtUtil');
5 | const EmailUtil = require('../utils/EmailUtil');
6 | // daos
7 | const AdminDAO = require('../models/AdminDAO');
8 | const CategoryDAO = require('../models/CategoryDAO');
9 | const ProductDAO = require('../models/ProductDAO');
10 | const OrderDAO = require('../models/OrderDAO');
11 | const CustomerDAO = require('../models/CustomerDAO');
12 | // login
13 | router.post('/login', async function (req, res) {
14 | const username = req.body.username;
15 | const password = req.body.password;
16 | if (username && password) {
17 | const admin = await AdminDAO.selectByUsernameAndPassword(username, password);
18 | if (admin) {
19 | const token = JwtUtil.genToken();
20 | res.json({ success: true, message: 'Authentication successful', token: token });
21 | } else {
22 | res.json({ success: false, message: 'Incorrect username or password' });
23 | }
24 | } else {
25 | res.json({ success: false, message: 'Please input username and password' });
26 | }
27 | });
28 | router.get('/token', JwtUtil.checkToken, function (req, res) {
29 | const token = req.headers['x-access-token'] || req.headers['authorization'];
30 | res.json({ success: true, message: 'Token is valid', token: token });
31 | });
32 | // category
33 | router.get('/categories', JwtUtil.checkToken, async function (req, res) {
34 | const categories = await CategoryDAO.selectAll();
35 | res.json(categories);
36 | });
37 | router.post('/categories', JwtUtil.checkToken, async function (req, res) {
38 | const name = req.body.name;
39 | const category = { name: name };
40 | const result = await CategoryDAO.insert(category);
41 | res.json(result);
42 | });
43 | router.put('/categories/:id', JwtUtil.checkToken, async function (req, res) {
44 | const _id = req.params.id;
45 | const name = req.body.name;
46 | const category = { _id: _id, name: name };
47 | const result = await CategoryDAO.update(category);
48 | res.json(result);
49 | });
50 | router.delete('/categories/:id', JwtUtil.checkToken, async function (req, res) {
51 | const _id = req.params.id;
52 | const result = await CategoryDAO.delete(_id);
53 | res.json(result);
54 | });
55 | // product
56 | router.get('/products', JwtUtil.checkToken, async function (req, res) {
57 | // pagination
58 | const noProducts = await ProductDAO.selectByCount();
59 | const sizePage = 4;
60 | const noPages = Math.ceil(noProducts / sizePage);
61 | var curPage = 1;
62 | if (req.query.page) curPage = parseInt(req.query.page); // /products?page=xxx
63 | const skip = (curPage - 1) * sizePage;
64 | const products = await ProductDAO.selectBySkipLimit(skip, sizePage);
65 | // return
66 | const result = { products: products, noPages: noPages, curPage: curPage };
67 | res.json(result);
68 | });
69 | router.post('/products', JwtUtil.checkToken, async function (req, res) {
70 | const name = req.body.name;
71 | const price = req.body.price;
72 | const cid = req.body.category;
73 | const image = req.body.image;
74 | const now = new Date().getTime(); // milliseconds
75 | const category = await CategoryDAO.selectByID(cid);
76 | const product = { name: name, price: price, image: image, cdate: now, category: category };
77 | const result = await ProductDAO.insert(product);
78 | res.json(result);
79 | });
80 | router.put('/products/:id', JwtUtil.checkToken, async function (req, res) {
81 | const _id = req.params.id;
82 | const name = req.body.name;
83 | const price = req.body.price;
84 | const cid = req.body.category;
85 | const image = req.body.image;
86 | const now = new Date().getTime(); // milliseconds
87 | const category = await CategoryDAO.selectByID(cid);
88 | const product = { _id: _id, name: name, price: price, image: image, cdate: now, category: category };
89 | const result = await ProductDAO.update(product);
90 | res.json(result);
91 | });
92 | router.delete('/products/:id', JwtUtil.checkToken, async function (req, res) {
93 | const _id = req.params.id;
94 | const result = await ProductDAO.delete(_id);
95 | res.json(result);
96 | });
97 | // order
98 | router.get('/orders', JwtUtil.checkToken, async function (req, res) {
99 | const orders = await OrderDAO.selectAll();
100 | res.json(orders);
101 | });
102 | router.get('/orders/customer/:cid', JwtUtil.checkToken, async function (req, res) {
103 | const _cid = req.params.cid;
104 | const orders = await OrderDAO.selectByCustID(_cid);
105 | res.json(orders);
106 | });
107 | router.put('/orders/status/:id', JwtUtil.checkToken, async function (req, res) {
108 | const _id = req.params.id;
109 | const newStatus = req.body.status;
110 | const result = await OrderDAO.update(_id, newStatus);
111 | res.json(result);
112 | });
113 | // customer
114 | router.get('/customers', JwtUtil.checkToken, async function (req, res) {
115 | const customers = await CustomerDAO.selectAll();
116 | res.json(customers);
117 | });
118 | router.put('/customers/deactive/:id', JwtUtil.checkToken, async function (req, res) {
119 | const _id = req.params.id;
120 | const token = req.body.token;
121 | const result = await CustomerDAO.active(_id, token, 0);
122 | res.json(result);
123 | });
124 | router.get('/customers/sendmail/:id', JwtUtil.checkToken, async function (req, res) {
125 | const _id = req.params.id;
126 | const cust = await CustomerDAO.selectByID(_id);
127 | if (cust) {
128 | const send = await EmailUtil.send(cust.email, cust._id, cust.token);
129 | if (send) {
130 | res.json({ success: true, message: 'Please check email' });
131 | } else {
132 | res.json({ success: false, message: 'Email failure' });
133 | }
134 | } else {
135 | res.json({ success: false, message: 'Not exists customer' });
136 | }
137 | });
138 | module.exports = router;
--------------------------------------------------------------------------------
/client-admin/src/components/CustomerComponent.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { Component } from 'react';
3 | import MyContext from '../contexts/MyContext';
4 |
5 | class Customer extends Component {
6 | static contextType = MyContext; // using this.context to access global state
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | customers: [],
11 | orders: [],
12 | order: null
13 | };
14 | }
15 | render() {
16 | const customers = this.state.customers.map((item) => {
17 | return (
18 | this.trCustomerClick(item)}>
19 | | {item._id} |
20 | {item.username} |
21 | {item.password} |
22 | {item.name} |
23 | {item.phone} |
24 | {item.email} |
25 | {item.active} |
26 |
27 | {item.active === 0 ?
28 | this.lnkEmailClick(item)}>EMAIL
29 | :
30 | this.lnkDeactiveClick(item)}>DEACTIVE}
31 | |
32 |
33 | );
34 | });
35 | const orders = this.state.orders.map((item) => {
36 | return (
37 | this.trOrderClick(item)}>
38 | | {item._id} |
39 | {new Date(item.cdate).toLocaleString()} |
40 | {item.customer.name} |
41 | {item.customer.phone} |
42 | {item.total} |
43 | {item.status} |
44 |
45 | );
46 | });
47 | if (this.state.order) {
48 | var items = this.state.order.items.map((item, index) => {
49 | return (
50 |
51 | | {index + 1} |
52 | {item.product._id} |
53 | {item.product.name} |
54 |  |
55 | {item.product.price} |
56 | {item.quantity} |
57 | {item.product.price * item.quantity} |
58 |
59 | );
60 | });
61 | }
62 | return (
63 |
64 |
65 |
CUSTOMER LIST
66 |
67 |
68 |
69 | | ID |
70 | Username |
71 | Password |
72 | Name |
73 | Phone |
74 | Email |
75 | Active |
76 | Action |
77 |
78 | {customers}
79 |
80 |
81 |
82 | {this.state.orders.length > 0 ?
83 |
84 |
ORDER LIST
85 |
86 |
87 |
88 | | ID |
89 | Creation date |
90 | Cust.name |
91 | Cust.phone |
92 | Total |
93 | Status |
94 |
95 | {orders}
96 |
97 |
98 |
99 | :
}
100 | {this.state.order ?
101 |
102 |
ORDER DETAIL
103 |
104 |
105 |
106 | | No. |
107 | Prod.ID |
108 | Prod.name |
109 | Image |
110 | Price |
111 | Quantity |
112 | Amount |
113 |
114 | {items}
115 |
116 |
117 |
118 | :
}
119 |
120 | );
121 | }
122 | componentDidMount() {
123 | this.apiGetCustomers();
124 | }
125 | // event-handlers
126 | trCustomerClick(item) {
127 | this.setState({ orders: [], order: null });
128 | this.apiGetOrdersByCustID(item._id);
129 | }
130 | trOrderClick(item) {
131 | this.setState({ order: item });
132 | }
133 | lnkDeactiveClick(item) {
134 | this.apiPutCustomerDeactive(item._id, item.token);
135 | }
136 | lnkEmailClick(item) {
137 | this.apiGetCustomerSendmail(item._id);
138 | }
139 | // apis
140 | apiGetCustomers() {
141 | const config = { headers: { 'x-access-token': this.context.token } };
142 | axios.get('/api/admin/customers', config).then((res) => {
143 | const result = res.data;
144 | this.setState({ customers: result });
145 | });
146 | }
147 | apiGetOrdersByCustID(cid) {
148 | const config = { headers: { 'x-access-token': this.context.token } };
149 | axios.get('/api/admin/orders/customer/' + cid, config).then((res) => {
150 | const result = res.data;
151 | this.setState({ orders: result });
152 | });
153 | }
154 | apiPutCustomerDeactive(id, token) {
155 | const body = { token: token };
156 | const config = { headers: { 'x-access-token': this.context.token } };
157 | axios.put('/api/admin/customers/deactive/' + id, body, config).then((res) => {
158 | const result = res.data;
159 | if (result) {
160 | this.apiGetCustomers();
161 | } else {
162 | alert('Error! An error occurred. Please try again later.');
163 | }
164 | });
165 | }
166 | apiGetCustomerSendmail(id) {
167 | const config = { headers: { 'x-access-token': this.context.token } };
168 | axios.get('/api/admin/customers/sendmail/' + id, config).then((res) => {
169 | const result = res.data;
170 | alert(result.message);
171 | });
172 | }
173 | }
174 | export default Customer;
--------------------------------------------------------------------------------
/client-admin/src/components/ProductDetailComponent.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { Component } from 'react';
3 | import MyContext from '../contexts/MyContext';
4 |
5 | class ProductDetail extends Component {
6 | static contextType = MyContext; // using this.context to access global state
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | categories: [],
11 | txtID: '',
12 | txtName: '',
13 | txtPrice: 0,
14 | cmbCategory: '',
15 | imgProduct: '',
16 | };
17 | }
18 | render() {
19 | const cates = this.state.categories.map((cate) => {
20 | if (this.props.item != null) {
21 | return ();
22 | } else {
23 | return ();
24 | }
25 | });
26 | return (
27 |
28 |
PRODUCT DETAIL
29 |
66 |
67 | );
68 | }
69 | componentDidMount() {
70 | this.apiGetCategories();
71 | }
72 | componentDidUpdate(prevProps) {
73 | if (this.props.item !== prevProps.item) {
74 | this.setState({
75 | txtID: this.props.item._id,
76 | txtName: this.props.item.name,
77 | txtPrice: this.props.item.price,
78 | cmbCategory: this.props.item.category._id,
79 | imgProduct: 'data:image/jpg;base64,' + this.props.item.image
80 | });
81 | }
82 | }
83 | // event-handlers
84 | previewImage(e) {
85 | const file = e.target.files[0];
86 | if (file) {
87 | const reader = new FileReader();
88 | reader.onload = (evt) => {
89 | this.setState({ imgProduct: evt.target.result });
90 | }
91 | reader.readAsDataURL(file);
92 | }
93 | }
94 | btnAddClick(e) {
95 | e.preventDefault();
96 | const name = this.state.txtName;
97 | const price = parseInt(this.state.txtPrice);
98 | const category = this.state.cmbCategory;
99 | const image = this.state.imgProduct.replace(/^data:image\/[a-z]+;base64,/, ''); // remove "data:image/...;base64,"
100 | if (name && price && category && image) {
101 | const prod = { name: name, price: price, category: category, image: image };
102 | this.apiPostProduct(prod);
103 | } else {
104 | alert('Please input name and price and category and image');
105 | }
106 | }
107 | btnUpdateClick(e) {
108 | e.preventDefault();
109 | const id = this.state.txtID;
110 | const name = this.state.txtName;
111 | const price = parseInt(this.state.txtPrice);
112 | const category = this.state.cmbCategory;
113 | const image = this.state.imgProduct.replace(/^data:image\/[a-z]+;base64,/, ''); // remove "data:image/...;base64,"
114 | if (id && name && price && category && image) {
115 | const prod = { name: name, price: price, category: category, image: image };
116 | this.apiPutProduct(id, prod);
117 | } else {
118 | alert('Please input id and name and price and category and image');
119 | }
120 | }
121 | btnDeleteClick(e) {
122 | e.preventDefault();
123 | if (window.confirm('ARE YOU SURE?')) {
124 | const id = this.state.txtID;
125 | if (id) {
126 | this.apiDeleteProduct(id);
127 | } else {
128 | alert('Please input id');
129 | }
130 | }
131 | }
132 | // apis
133 | apiGetCategories() {
134 | const config = { headers: { 'x-access-token': this.context.token } };
135 | axios.get('/api/admin/categories', config).then((res) => {
136 | const result = res.data;
137 | this.setState({ categories: result });
138 | });
139 | }
140 | apiPostProduct(prod) {
141 | const config = { headers: { 'x-access-token': this.context.token } };
142 | axios.post('/api/admin/products', prod, config).then((res) => {
143 | const result = res.data;
144 | if (result) {
145 | alert('Good job!');
146 | this.apiGetProducts();
147 | } else {
148 | alert('Error! An error occurred. Please try again later.');
149 | }
150 | });
151 | }
152 | apiPutProduct(id, prod) {
153 | const config = { headers: { 'x-access-token': this.context.token } };
154 | axios.put('/api/admin/products/' + id, prod, config).then((res) => {
155 | const result = res.data;
156 | if (result) {
157 | alert('Good job!');
158 | this.apiGetProducts();
159 | } else {
160 | alert('Error! An error occurred. Please try again later.');
161 | }
162 | });
163 | }
164 | apiDeleteProduct(id) {
165 | const config = { headers: { 'x-access-token': this.context.token } };
166 | axios.delete('/api/admin/products/' + id, config).then((res) => {
167 | const result = res.data;
168 | if (result) {
169 | alert('Good job!');
170 | this.apiGetProducts();
171 | } else {
172 | alert('Error! An error occurred. Please try again later.');
173 | }
174 | });
175 | }
176 | apiGetProducts() {
177 | const config = { headers: { 'x-access-token': this.context.token } };
178 | axios.get('/api/admin/products?page=' + this.props.curPage, config).then((res) => {
179 | const result = res.data;
180 | if (result.products.length !== 0) {
181 | this.props.updateProducts(result.products, result.noPages, result.curPage);
182 | } else {
183 | const curPage = this.props.curPage - 1;
184 | axios.get('/api/admin/products?page=' + curPage, config).then((res) => {
185 | const result = res.data;
186 | this.props.updateProducts(result.products, result.noPages, curPage);
187 | });
188 | }
189 | });
190 | }
191 | }
192 | export default ProductDetail;
--------------------------------------------------------------------------------
/server/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "server",
9 | "version": "1.0.0",
10 | "license": "ISC",
11 | "dependencies": {
12 | "body-parser": "^1.20.2",
13 | "crypto": "^1.0.1",
14 | "express": "^4.18.2",
15 | "jsonwebtoken": "^9.0.0",
16 | "mongoose": "^7.0.4",
17 | "nodemailer": "^6.9.4"
18 | }
19 | },
20 | "node_modules/@types/node": {
21 | "version": "20.4.1",
22 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.1.tgz",
23 | "integrity": "sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg=="
24 | },
25 | "node_modules/@types/webidl-conversions": {
26 | "version": "7.0.0",
27 | "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
28 | "integrity": "sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog=="
29 | },
30 | "node_modules/@types/whatwg-url": {
31 | "version": "8.2.2",
32 | "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz",
33 | "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
34 | "dependencies": {
35 | "@types/node": "*",
36 | "@types/webidl-conversions": "*"
37 | }
38 | },
39 | "node_modules/accepts": {
40 | "version": "1.3.8",
41 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
42 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
43 | "dependencies": {
44 | "mime-types": "~2.1.34",
45 | "negotiator": "0.6.3"
46 | },
47 | "engines": {
48 | "node": ">= 0.6"
49 | }
50 | },
51 | "node_modules/array-flatten": {
52 | "version": "1.1.1",
53 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
54 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
55 | },
56 | "node_modules/body-parser": {
57 | "version": "1.20.2",
58 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
59 | "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
60 | "dependencies": {
61 | "bytes": "3.1.2",
62 | "content-type": "~1.0.5",
63 | "debug": "2.6.9",
64 | "depd": "2.0.0",
65 | "destroy": "1.2.0",
66 | "http-errors": "2.0.0",
67 | "iconv-lite": "0.4.24",
68 | "on-finished": "2.4.1",
69 | "qs": "6.11.0",
70 | "raw-body": "2.5.2",
71 | "type-is": "~1.6.18",
72 | "unpipe": "1.0.0"
73 | },
74 | "engines": {
75 | "node": ">= 0.8",
76 | "npm": "1.2.8000 || >= 1.4.16"
77 | }
78 | },
79 | "node_modules/bson": {
80 | "version": "5.4.0",
81 | "resolved": "https://registry.npmjs.org/bson/-/bson-5.4.0.tgz",
82 | "integrity": "sha512-WRZ5SQI5GfUuKnPTNmAYPiKIof3ORXAF4IRU5UcgmivNIon01rWQlw5RUH954dpu8yGL8T59YShVddIPaU/gFA==",
83 | "engines": {
84 | "node": ">=14.20.1"
85 | }
86 | },
87 | "node_modules/buffer-equal-constant-time": {
88 | "version": "1.0.1",
89 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
90 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
91 | },
92 | "node_modules/bytes": {
93 | "version": "3.1.2",
94 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
95 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
96 | "engines": {
97 | "node": ">= 0.8"
98 | }
99 | },
100 | "node_modules/call-bind": {
101 | "version": "1.0.2",
102 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
103 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
104 | "dependencies": {
105 | "function-bind": "^1.1.1",
106 | "get-intrinsic": "^1.0.2"
107 | },
108 | "funding": {
109 | "url": "https://github.com/sponsors/ljharb"
110 | }
111 | },
112 | "node_modules/content-disposition": {
113 | "version": "0.5.4",
114 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
115 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
116 | "dependencies": {
117 | "safe-buffer": "5.2.1"
118 | },
119 | "engines": {
120 | "node": ">= 0.6"
121 | }
122 | },
123 | "node_modules/content-type": {
124 | "version": "1.0.5",
125 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
126 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
127 | "engines": {
128 | "node": ">= 0.6"
129 | }
130 | },
131 | "node_modules/cookie": {
132 | "version": "0.5.0",
133 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
134 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
135 | "engines": {
136 | "node": ">= 0.6"
137 | }
138 | },
139 | "node_modules/cookie-signature": {
140 | "version": "1.0.6",
141 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
142 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
143 | },
144 | "node_modules/crypto": {
145 | "version": "1.0.1",
146 | "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
147 | "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
148 | "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in."
149 | },
150 | "node_modules/debug": {
151 | "version": "2.6.9",
152 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
153 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
154 | "dependencies": {
155 | "ms": "2.0.0"
156 | }
157 | },
158 | "node_modules/depd": {
159 | "version": "2.0.0",
160 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
161 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
162 | "engines": {
163 | "node": ">= 0.8"
164 | }
165 | },
166 | "node_modules/destroy": {
167 | "version": "1.2.0",
168 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
169 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
170 | "engines": {
171 | "node": ">= 0.8",
172 | "npm": "1.2.8000 || >= 1.4.16"
173 | }
174 | },
175 | "node_modules/ecdsa-sig-formatter": {
176 | "version": "1.0.11",
177 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
178 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
179 | "dependencies": {
180 | "safe-buffer": "^5.0.1"
181 | }
182 | },
183 | "node_modules/ee-first": {
184 | "version": "1.1.1",
185 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
186 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
187 | },
188 | "node_modules/encodeurl": {
189 | "version": "1.0.2",
190 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
191 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
192 | "engines": {
193 | "node": ">= 0.8"
194 | }
195 | },
196 | "node_modules/escape-html": {
197 | "version": "1.0.3",
198 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
199 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
200 | },
201 | "node_modules/etag": {
202 | "version": "1.8.1",
203 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
204 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
205 | "engines": {
206 | "node": ">= 0.6"
207 | }
208 | },
209 | "node_modules/express": {
210 | "version": "4.18.2",
211 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
212 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
213 | "dependencies": {
214 | "accepts": "~1.3.8",
215 | "array-flatten": "1.1.1",
216 | "body-parser": "1.20.1",
217 | "content-disposition": "0.5.4",
218 | "content-type": "~1.0.4",
219 | "cookie": "0.5.0",
220 | "cookie-signature": "1.0.6",
221 | "debug": "2.6.9",
222 | "depd": "2.0.0",
223 | "encodeurl": "~1.0.2",
224 | "escape-html": "~1.0.3",
225 | "etag": "~1.8.1",
226 | "finalhandler": "1.2.0",
227 | "fresh": "0.5.2",
228 | "http-errors": "2.0.0",
229 | "merge-descriptors": "1.0.1",
230 | "methods": "~1.1.2",
231 | "on-finished": "2.4.1",
232 | "parseurl": "~1.3.3",
233 | "path-to-regexp": "0.1.7",
234 | "proxy-addr": "~2.0.7",
235 | "qs": "6.11.0",
236 | "range-parser": "~1.2.1",
237 | "safe-buffer": "5.2.1",
238 | "send": "0.18.0",
239 | "serve-static": "1.15.0",
240 | "setprototypeof": "1.2.0",
241 | "statuses": "2.0.1",
242 | "type-is": "~1.6.18",
243 | "utils-merge": "1.0.1",
244 | "vary": "~1.1.2"
245 | },
246 | "engines": {
247 | "node": ">= 0.10.0"
248 | }
249 | },
250 | "node_modules/express/node_modules/body-parser": {
251 | "version": "1.20.1",
252 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
253 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
254 | "dependencies": {
255 | "bytes": "3.1.2",
256 | "content-type": "~1.0.4",
257 | "debug": "2.6.9",
258 | "depd": "2.0.0",
259 | "destroy": "1.2.0",
260 | "http-errors": "2.0.0",
261 | "iconv-lite": "0.4.24",
262 | "on-finished": "2.4.1",
263 | "qs": "6.11.0",
264 | "raw-body": "2.5.1",
265 | "type-is": "~1.6.18",
266 | "unpipe": "1.0.0"
267 | },
268 | "engines": {
269 | "node": ">= 0.8",
270 | "npm": "1.2.8000 || >= 1.4.16"
271 | }
272 | },
273 | "node_modules/express/node_modules/raw-body": {
274 | "version": "2.5.1",
275 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
276 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
277 | "dependencies": {
278 | "bytes": "3.1.2",
279 | "http-errors": "2.0.0",
280 | "iconv-lite": "0.4.24",
281 | "unpipe": "1.0.0"
282 | },
283 | "engines": {
284 | "node": ">= 0.8"
285 | }
286 | },
287 | "node_modules/finalhandler": {
288 | "version": "1.2.0",
289 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
290 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
291 | "dependencies": {
292 | "debug": "2.6.9",
293 | "encodeurl": "~1.0.2",
294 | "escape-html": "~1.0.3",
295 | "on-finished": "2.4.1",
296 | "parseurl": "~1.3.3",
297 | "statuses": "2.0.1",
298 | "unpipe": "~1.0.0"
299 | },
300 | "engines": {
301 | "node": ">= 0.8"
302 | }
303 | },
304 | "node_modules/forwarded": {
305 | "version": "0.2.0",
306 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
307 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
308 | "engines": {
309 | "node": ">= 0.6"
310 | }
311 | },
312 | "node_modules/fresh": {
313 | "version": "0.5.2",
314 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
315 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
316 | "engines": {
317 | "node": ">= 0.6"
318 | }
319 | },
320 | "node_modules/function-bind": {
321 | "version": "1.1.1",
322 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
323 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
324 | },
325 | "node_modules/get-intrinsic": {
326 | "version": "1.2.1",
327 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
328 | "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
329 | "dependencies": {
330 | "function-bind": "^1.1.1",
331 | "has": "^1.0.3",
332 | "has-proto": "^1.0.1",
333 | "has-symbols": "^1.0.3"
334 | },
335 | "funding": {
336 | "url": "https://github.com/sponsors/ljharb"
337 | }
338 | },
339 | "node_modules/has": {
340 | "version": "1.0.3",
341 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
342 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
343 | "dependencies": {
344 | "function-bind": "^1.1.1"
345 | },
346 | "engines": {
347 | "node": ">= 0.4.0"
348 | }
349 | },
350 | "node_modules/has-proto": {
351 | "version": "1.0.1",
352 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
353 | "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
354 | "engines": {
355 | "node": ">= 0.4"
356 | },
357 | "funding": {
358 | "url": "https://github.com/sponsors/ljharb"
359 | }
360 | },
361 | "node_modules/has-symbols": {
362 | "version": "1.0.3",
363 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
364 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
365 | "engines": {
366 | "node": ">= 0.4"
367 | },
368 | "funding": {
369 | "url": "https://github.com/sponsors/ljharb"
370 | }
371 | },
372 | "node_modules/http-errors": {
373 | "version": "2.0.0",
374 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
375 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
376 | "dependencies": {
377 | "depd": "2.0.0",
378 | "inherits": "2.0.4",
379 | "setprototypeof": "1.2.0",
380 | "statuses": "2.0.1",
381 | "toidentifier": "1.0.1"
382 | },
383 | "engines": {
384 | "node": ">= 0.8"
385 | }
386 | },
387 | "node_modules/iconv-lite": {
388 | "version": "0.4.24",
389 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
390 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
391 | "dependencies": {
392 | "safer-buffer": ">= 2.1.2 < 3"
393 | },
394 | "engines": {
395 | "node": ">=0.10.0"
396 | }
397 | },
398 | "node_modules/inherits": {
399 | "version": "2.0.4",
400 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
401 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
402 | },
403 | "node_modules/ip": {
404 | "version": "2.0.0",
405 | "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
406 | "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ=="
407 | },
408 | "node_modules/ipaddr.js": {
409 | "version": "1.9.1",
410 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
411 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
412 | "engines": {
413 | "node": ">= 0.10"
414 | }
415 | },
416 | "node_modules/jsonwebtoken": {
417 | "version": "9.0.1",
418 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz",
419 | "integrity": "sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg==",
420 | "dependencies": {
421 | "jws": "^3.2.2",
422 | "lodash": "^4.17.21",
423 | "ms": "^2.1.1",
424 | "semver": "^7.3.8"
425 | },
426 | "engines": {
427 | "node": ">=12",
428 | "npm": ">=6"
429 | }
430 | },
431 | "node_modules/jsonwebtoken/node_modules/ms": {
432 | "version": "2.1.3",
433 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
434 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
435 | },
436 | "node_modules/jwa": {
437 | "version": "1.4.1",
438 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
439 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
440 | "dependencies": {
441 | "buffer-equal-constant-time": "1.0.1",
442 | "ecdsa-sig-formatter": "1.0.11",
443 | "safe-buffer": "^5.0.1"
444 | }
445 | },
446 | "node_modules/jws": {
447 | "version": "3.2.2",
448 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
449 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
450 | "dependencies": {
451 | "jwa": "^1.4.1",
452 | "safe-buffer": "^5.0.1"
453 | }
454 | },
455 | "node_modules/kareem": {
456 | "version": "2.5.1",
457 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz",
458 | "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==",
459 | "engines": {
460 | "node": ">=12.0.0"
461 | }
462 | },
463 | "node_modules/lodash": {
464 | "version": "4.17.21",
465 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
466 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
467 | },
468 | "node_modules/lru-cache": {
469 | "version": "6.0.0",
470 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
471 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
472 | "dependencies": {
473 | "yallist": "^4.0.0"
474 | },
475 | "engines": {
476 | "node": ">=10"
477 | }
478 | },
479 | "node_modules/media-typer": {
480 | "version": "0.3.0",
481 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
482 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
483 | "engines": {
484 | "node": ">= 0.6"
485 | }
486 | },
487 | "node_modules/memory-pager": {
488 | "version": "1.5.0",
489 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
490 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
491 | "optional": true
492 | },
493 | "node_modules/merge-descriptors": {
494 | "version": "1.0.1",
495 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
496 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
497 | },
498 | "node_modules/methods": {
499 | "version": "1.1.2",
500 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
501 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
502 | "engines": {
503 | "node": ">= 0.6"
504 | }
505 | },
506 | "node_modules/mime": {
507 | "version": "1.6.0",
508 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
509 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
510 | "bin": {
511 | "mime": "cli.js"
512 | },
513 | "engines": {
514 | "node": ">=4"
515 | }
516 | },
517 | "node_modules/mime-db": {
518 | "version": "1.52.0",
519 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
520 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
521 | "engines": {
522 | "node": ">= 0.6"
523 | }
524 | },
525 | "node_modules/mime-types": {
526 | "version": "2.1.35",
527 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
528 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
529 | "dependencies": {
530 | "mime-db": "1.52.0"
531 | },
532 | "engines": {
533 | "node": ">= 0.6"
534 | }
535 | },
536 | "node_modules/mongodb": {
537 | "version": "5.6.0",
538 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.6.0.tgz",
539 | "integrity": "sha512-z8qVs9NfobHJm6uzK56XBZF8XwM9H294iRnB7wNjF0SnY93si5HPziIJn+qqvUR5QOff/4L0gCD6SShdR/GtVQ==",
540 | "dependencies": {
541 | "bson": "^5.3.0",
542 | "mongodb-connection-string-url": "^2.6.0",
543 | "socks": "^2.7.1"
544 | },
545 | "engines": {
546 | "node": ">=14.20.1"
547 | },
548 | "optionalDependencies": {
549 | "saslprep": "^1.0.3"
550 | },
551 | "peerDependencies": {
552 | "@aws-sdk/credential-providers": "^3.201.0",
553 | "mongodb-client-encryption": ">=2.3.0 <3",
554 | "snappy": "^7.2.2"
555 | },
556 | "peerDependenciesMeta": {
557 | "@aws-sdk/credential-providers": {
558 | "optional": true
559 | },
560 | "mongodb-client-encryption": {
561 | "optional": true
562 | },
563 | "snappy": {
564 | "optional": true
565 | }
566 | }
567 | },
568 | "node_modules/mongodb-connection-string-url": {
569 | "version": "2.6.0",
570 | "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz",
571 | "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==",
572 | "dependencies": {
573 | "@types/whatwg-url": "^8.2.1",
574 | "whatwg-url": "^11.0.0"
575 | }
576 | },
577 | "node_modules/mongoose": {
578 | "version": "7.3.2",
579 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.3.2.tgz",
580 | "integrity": "sha512-Z86m5ASwYYFyT++wPQTtuTl5Jh052w6G1IM8LxPu/6iuqxQo6nUOaEoGZfMy0ovw3Dyw3415Jue3pYXkRqPkfA==",
581 | "dependencies": {
582 | "bson": "^5.3.0",
583 | "kareem": "2.5.1",
584 | "mongodb": "5.6.0",
585 | "mpath": "0.9.0",
586 | "mquery": "5.0.0",
587 | "ms": "2.1.3",
588 | "sift": "16.0.1"
589 | },
590 | "engines": {
591 | "node": ">=14.20.1"
592 | },
593 | "funding": {
594 | "type": "opencollective",
595 | "url": "https://opencollective.com/mongoose"
596 | }
597 | },
598 | "node_modules/mongoose/node_modules/ms": {
599 | "version": "2.1.3",
600 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
601 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
602 | },
603 | "node_modules/mpath": {
604 | "version": "0.9.0",
605 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
606 | "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
607 | "engines": {
608 | "node": ">=4.0.0"
609 | }
610 | },
611 | "node_modules/mquery": {
612 | "version": "5.0.0",
613 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz",
614 | "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==",
615 | "dependencies": {
616 | "debug": "4.x"
617 | },
618 | "engines": {
619 | "node": ">=14.0.0"
620 | }
621 | },
622 | "node_modules/mquery/node_modules/debug": {
623 | "version": "4.3.4",
624 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
625 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
626 | "dependencies": {
627 | "ms": "2.1.2"
628 | },
629 | "engines": {
630 | "node": ">=6.0"
631 | },
632 | "peerDependenciesMeta": {
633 | "supports-color": {
634 | "optional": true
635 | }
636 | }
637 | },
638 | "node_modules/mquery/node_modules/ms": {
639 | "version": "2.1.2",
640 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
641 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
642 | },
643 | "node_modules/ms": {
644 | "version": "2.0.0",
645 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
646 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
647 | },
648 | "node_modules/negotiator": {
649 | "version": "0.6.3",
650 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
651 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
652 | "engines": {
653 | "node": ">= 0.6"
654 | }
655 | },
656 | "node_modules/nodemailer": {
657 | "version": "6.9.4",
658 | "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.4.tgz",
659 | "integrity": "sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA==",
660 | "engines": {
661 | "node": ">=6.0.0"
662 | }
663 | },
664 | "node_modules/object-inspect": {
665 | "version": "1.12.3",
666 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
667 | "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
668 | "funding": {
669 | "url": "https://github.com/sponsors/ljharb"
670 | }
671 | },
672 | "node_modules/on-finished": {
673 | "version": "2.4.1",
674 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
675 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
676 | "dependencies": {
677 | "ee-first": "1.1.1"
678 | },
679 | "engines": {
680 | "node": ">= 0.8"
681 | }
682 | },
683 | "node_modules/parseurl": {
684 | "version": "1.3.3",
685 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
686 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
687 | "engines": {
688 | "node": ">= 0.8"
689 | }
690 | },
691 | "node_modules/path-to-regexp": {
692 | "version": "0.1.7",
693 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
694 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
695 | },
696 | "node_modules/proxy-addr": {
697 | "version": "2.0.7",
698 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
699 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
700 | "dependencies": {
701 | "forwarded": "0.2.0",
702 | "ipaddr.js": "1.9.1"
703 | },
704 | "engines": {
705 | "node": ">= 0.10"
706 | }
707 | },
708 | "node_modules/punycode": {
709 | "version": "2.3.0",
710 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
711 | "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
712 | "engines": {
713 | "node": ">=6"
714 | }
715 | },
716 | "node_modules/qs": {
717 | "version": "6.11.0",
718 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
719 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
720 | "dependencies": {
721 | "side-channel": "^1.0.4"
722 | },
723 | "engines": {
724 | "node": ">=0.6"
725 | },
726 | "funding": {
727 | "url": "https://github.com/sponsors/ljharb"
728 | }
729 | },
730 | "node_modules/range-parser": {
731 | "version": "1.2.1",
732 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
733 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
734 | "engines": {
735 | "node": ">= 0.6"
736 | }
737 | },
738 | "node_modules/raw-body": {
739 | "version": "2.5.2",
740 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
741 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
742 | "dependencies": {
743 | "bytes": "3.1.2",
744 | "http-errors": "2.0.0",
745 | "iconv-lite": "0.4.24",
746 | "unpipe": "1.0.0"
747 | },
748 | "engines": {
749 | "node": ">= 0.8"
750 | }
751 | },
752 | "node_modules/safe-buffer": {
753 | "version": "5.2.1",
754 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
755 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
756 | "funding": [
757 | {
758 | "type": "github",
759 | "url": "https://github.com/sponsors/feross"
760 | },
761 | {
762 | "type": "patreon",
763 | "url": "https://www.patreon.com/feross"
764 | },
765 | {
766 | "type": "consulting",
767 | "url": "https://feross.org/support"
768 | }
769 | ]
770 | },
771 | "node_modules/safer-buffer": {
772 | "version": "2.1.2",
773 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
774 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
775 | },
776 | "node_modules/saslprep": {
777 | "version": "1.0.3",
778 | "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
779 | "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
780 | "optional": true,
781 | "dependencies": {
782 | "sparse-bitfield": "^3.0.3"
783 | },
784 | "engines": {
785 | "node": ">=6"
786 | }
787 | },
788 | "node_modules/semver": {
789 | "version": "7.5.4",
790 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
791 | "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
792 | "dependencies": {
793 | "lru-cache": "^6.0.0"
794 | },
795 | "bin": {
796 | "semver": "bin/semver.js"
797 | },
798 | "engines": {
799 | "node": ">=10"
800 | }
801 | },
802 | "node_modules/send": {
803 | "version": "0.18.0",
804 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
805 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
806 | "dependencies": {
807 | "debug": "2.6.9",
808 | "depd": "2.0.0",
809 | "destroy": "1.2.0",
810 | "encodeurl": "~1.0.2",
811 | "escape-html": "~1.0.3",
812 | "etag": "~1.8.1",
813 | "fresh": "0.5.2",
814 | "http-errors": "2.0.0",
815 | "mime": "1.6.0",
816 | "ms": "2.1.3",
817 | "on-finished": "2.4.1",
818 | "range-parser": "~1.2.1",
819 | "statuses": "2.0.1"
820 | },
821 | "engines": {
822 | "node": ">= 0.8.0"
823 | }
824 | },
825 | "node_modules/send/node_modules/ms": {
826 | "version": "2.1.3",
827 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
828 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
829 | },
830 | "node_modules/serve-static": {
831 | "version": "1.15.0",
832 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
833 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
834 | "dependencies": {
835 | "encodeurl": "~1.0.2",
836 | "escape-html": "~1.0.3",
837 | "parseurl": "~1.3.3",
838 | "send": "0.18.0"
839 | },
840 | "engines": {
841 | "node": ">= 0.8.0"
842 | }
843 | },
844 | "node_modules/setprototypeof": {
845 | "version": "1.2.0",
846 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
847 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
848 | },
849 | "node_modules/side-channel": {
850 | "version": "1.0.4",
851 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
852 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
853 | "dependencies": {
854 | "call-bind": "^1.0.0",
855 | "get-intrinsic": "^1.0.2",
856 | "object-inspect": "^1.9.0"
857 | },
858 | "funding": {
859 | "url": "https://github.com/sponsors/ljharb"
860 | }
861 | },
862 | "node_modules/sift": {
863 | "version": "16.0.1",
864 | "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz",
865 | "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ=="
866 | },
867 | "node_modules/smart-buffer": {
868 | "version": "4.2.0",
869 | "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
870 | "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
871 | "engines": {
872 | "node": ">= 6.0.0",
873 | "npm": ">= 3.0.0"
874 | }
875 | },
876 | "node_modules/socks": {
877 | "version": "2.7.1",
878 | "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz",
879 | "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==",
880 | "dependencies": {
881 | "ip": "^2.0.0",
882 | "smart-buffer": "^4.2.0"
883 | },
884 | "engines": {
885 | "node": ">= 10.13.0",
886 | "npm": ">= 3.0.0"
887 | }
888 | },
889 | "node_modules/sparse-bitfield": {
890 | "version": "3.0.3",
891 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
892 | "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
893 | "optional": true,
894 | "dependencies": {
895 | "memory-pager": "^1.0.2"
896 | }
897 | },
898 | "node_modules/statuses": {
899 | "version": "2.0.1",
900 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
901 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
902 | "engines": {
903 | "node": ">= 0.8"
904 | }
905 | },
906 | "node_modules/toidentifier": {
907 | "version": "1.0.1",
908 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
909 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
910 | "engines": {
911 | "node": ">=0.6"
912 | }
913 | },
914 | "node_modules/tr46": {
915 | "version": "3.0.0",
916 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
917 | "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
918 | "dependencies": {
919 | "punycode": "^2.1.1"
920 | },
921 | "engines": {
922 | "node": ">=12"
923 | }
924 | },
925 | "node_modules/type-is": {
926 | "version": "1.6.18",
927 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
928 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
929 | "dependencies": {
930 | "media-typer": "0.3.0",
931 | "mime-types": "~2.1.24"
932 | },
933 | "engines": {
934 | "node": ">= 0.6"
935 | }
936 | },
937 | "node_modules/unpipe": {
938 | "version": "1.0.0",
939 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
940 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
941 | "engines": {
942 | "node": ">= 0.8"
943 | }
944 | },
945 | "node_modules/utils-merge": {
946 | "version": "1.0.1",
947 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
948 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
949 | "engines": {
950 | "node": ">= 0.4.0"
951 | }
952 | },
953 | "node_modules/vary": {
954 | "version": "1.1.2",
955 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
956 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
957 | "engines": {
958 | "node": ">= 0.8"
959 | }
960 | },
961 | "node_modules/webidl-conversions": {
962 | "version": "7.0.0",
963 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
964 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
965 | "engines": {
966 | "node": ">=12"
967 | }
968 | },
969 | "node_modules/whatwg-url": {
970 | "version": "11.0.0",
971 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
972 | "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
973 | "dependencies": {
974 | "tr46": "^3.0.0",
975 | "webidl-conversions": "^7.0.0"
976 | },
977 | "engines": {
978 | "node": ">=12"
979 | }
980 | },
981 | "node_modules/yallist": {
982 | "version": "4.0.0",
983 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
984 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
985 | }
986 | }
987 | }
988 |
--------------------------------------------------------------------------------