├── 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 |
    30 | { this.setState({ txtKeyword: e.target.value }) }} /> 31 | this.btnSearchClick(e)} /> 32 |
    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 |
    17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
    ID { this.setState({ txtID: e.target.value }) }} />
    Token { this.setState({ txtToken: e.target.value }) }} />
    this.btnActiveClick(e)} />
    33 |
    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 | 32 | 33 | 34 | {cates} 35 | 36 |
    IDName
    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 |
    20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
    Username { this.setState({ txtUsername: e.target.value }) }} />
    Password { this.setState({ txtPassword: e.target.value }) }} />
    this.btnLoginClick(e)} />
    36 |
    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 |
    20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
    Username { this.setState({ txtUsername: e.target.value }) }} />
    Password { this.setState({ txtPassword: e.target.value }) }} />
    this.btnLoginClick(e)} />
    36 |
    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 |
    18 |

    SIGN-UP

    19 |
    20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
    Username { this.setState({ txtUsername: e.target.value }) }} />
    Password { this.setState({ txtPassword: e.target.value }) }} />
    Name { this.setState({ txtName: e.target.value }) }} />
    Phone { this.setState({ txtPhone: e.target.value }) }} />
    Email { this.setState({ txtEmail: e.target.value }) }} />
    this.btnSignupClick(e)} />
    48 |
    49 |
    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 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {prods} 52 | 53 | 54 | 55 | 56 |
    IDNamePriceCreation dateCategoryImage
    {pagination}
    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 |
    25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
    ID:{prod._id}
    Name:{prod.name}
    Price:{prod.price}
    Category:{prod.category.name}
    Quantity: { this.setState({ txtQuantity: e.target.value }) }} />
    this.btnAdd2CartClick(e)} />
    53 |
    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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {mycart} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
    No.IDNameCategoryImagePriceQuantityAmountAction
    Total{CartUtil.getTotal(this.context.mycart)} this.lnkCheckoutClick()}>CHECKOUT
    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 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {orders} 59 | 60 |
    IDCreation dateCust.nameCust.phoneTotalStatus
    61 |
    62 | {this.state.order ? 63 |
    64 |

    ORDER DETAIL

    65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | {items} 77 | 78 |
    No.Prod.IDProd.nameImagePriceQuantityAmount
    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 |
    24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
    Username { this.setState({ txtUsername: e.target.value }) }} />
    Password { this.setState({ txtPassword: e.target.value }) }} />
    Name { this.setState({ txtName: e.target.value }) }} />
    Phone { this.setState({ txtPhone: e.target.value }) }} />
    Email { this.setState({ txtEmail: e.target.value }) }} />
    this.btnUpdateClick(e)} />
    52 |
    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 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | {orders} 63 | 64 |
    IDCreation dateCust.nameCust.phoneTotalStatusAction
    65 |
    66 | {this.state.order ? 67 |
    68 |

    ORDER DETAIL

    69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | {items} 81 | 82 |
    No.Prod.IDProd.nameImagePriceQuantityAmount
    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 |
    19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 36 | 37 | 38 |
    ID { this.setState({ txtID: e.target.value }) }} readOnly={true} />
    Name { this.setState({ txtName: e.target.value }) }} />
    32 | this.btnAddClick(e)} /> 33 | this.btnUpdateClick(e)} /> 34 | this.btnDeleteClick(e)} /> 35 |
    39 |
    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 | picture mern stack 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 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | {customers} 79 | 80 |
    IDUsernamePasswordNamePhoneEmailActiveAction
    81 |
    82 | {this.state.orders.length > 0 ? 83 |
    84 |

    ORDER LIST

    85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | {orders} 96 | 97 |
    IDCreation dateCust.nameCust.phoneTotalStatus
    98 |
    99 | :
    } 100 | {this.state.order ? 101 |
    102 |

    ORDER DETAIL

    103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | {items} 115 | 116 |
    No.Prod.IDProd.nameImagePriceQuantityAmount
    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 |
    30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 59 | 60 | 61 | 62 | 63 | 64 |
    ID { this.setState({ txtID: e.target.value }) }} readOnly={true} />
    Name { this.setState({ txtName: e.target.value }) }} />
    Price { this.setState({ txtPrice: e.target.value }) }} />
    Image this.previewImage(e)} />
    Category
    55 | this.btnAddClick(e)} /> 56 | this.btnUpdateClick(e)} /> 57 | this.btnDeleteClick(e)} /> 58 |
    65 |
    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 | --------------------------------------------------------------------------------