diff --git a/README.md b/README.md index a7fa470..6eeff56 100644 --- a/README.md +++ b/README.md @@ -100,12 +100,20 @@ Please note that the backend server may take a few seconds to wake up if it has - Material-UI for styling - Axios for API requests - `react-credit-cards-2` for credit card visualization + - `react-router-dom` for routing + - `react-hook-form` for form validation + - `react-toastify` for toast notifications + - Jest and React Testing Library for testing - **Backend:** - Node.js - Express.js - MongoDB (with Mongoose ODM) - Axios for external API requests + - JsonWebToken for user authentication + - Bcrypt for password hashing + - Dotenv for environment variables + - Cors for cross-origin resource sharing - Swagger for API documentation - Nodemon for server hot-reloading diff --git a/backend/docs/swagger.js b/backend/docs/swagger.js index e9a16ab..221905c 100644 --- a/backend/docs/swagger.js +++ b/backend/docs/swagger.js @@ -5,9 +5,19 @@ const swaggerUi = require('swagger-ui-express'); const swaggerDefinition = { openapi: '3.0.0', info: { - title: 'Fusion E-Commerce Backend APIs', - version: '1.0.0', - description: 'API documentation for the Fusion E-Commerce backend server.', + title: 'Fusion E-Commerce Backend APIs', // API title + version: '1.1.0', // API version + description: 'API documentation for the Fusion E-Commerce backend server. This documentation provides detailed information on all available endpoints for managing products, users, authentication, and more.', + termsOfService: 'https://mern-stack-ecommerce-app-nine.vercel.app', + contact: { + name: 'Fusion E-Commerce Website', + url: 'https://mern-stack-ecommerce-app-nine.vercel.app', + email: 'hoangson091104@gmail.com', // Contact email + }, + license: { + name: 'MIT License', + url: 'https://opensource.org/licenses/MIT', // License link + }, }, servers: [ { @@ -15,16 +25,124 @@ const swaggerDefinition = { description: 'Production server', }, { - url: 'http://localhost:5000', + url: 'http://localhost:8000', description: 'Development server', } ], + components: { + securitySchemes: { + BearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + schemas: { + Product: { + type: 'object', + required: ['name', 'price', 'description', 'category'], + properties: { + id: { + type: 'string', + description: 'Product ID', + }, + name: { + type: 'string', + description: 'Name of the product', + }, + description: { + type: 'string', + description: 'Detailed description of the product', + }, + price: { + type: 'number', + description: 'Price of the product in USD', + }, + category: { + type: 'string', + description: 'Category the product belongs to', + }, + brand: { + type: 'string', + description: 'Brand of the product', + }, + stock: { + type: 'integer', + description: 'Stock count available', + }, + rating: { + type: 'number', + description: 'Average rating of the product', + }, + numReviews: { + type: 'integer', + description: 'Number of reviews for the product', + }, + image: { + type: 'string', + description: 'URL of the product image', + }, + }, + example: { + id: '507f1f77bcf86cd799439011', + name: 'Wireless Headphones', + description: 'Noise-cancelling wireless headphones with long battery life.', + price: 99.99, + category: 'Electronics', + brand: 'Fusion', + stock: 150, + rating: 4.7, + numReviews: 89, + image: 'https://example.com/product.jpg', + }, + }, + User: { + type: 'object', + required: ['name', 'email', 'password'], + properties: { + id: { + type: 'string', + description: 'User ID', + }, + name: { + type: 'string', + description: 'Full name of the user', + }, + email: { + type: 'string', + description: 'Email address of the user', + }, + password: { + type: 'string', + description: 'Password for the user account', + }, + createdAt: { + type: 'string', + format: 'date-time', + description: 'Account creation date', + }, + }, + example: { + id: '507f1f77bcf86cd799439011', + name: 'John Doe', + email: 'john.doe@example.com', + password: 'password123', + createdAt: '2023-10-21T14:21:00Z', + }, + }, + }, + }, + security: [ + { + BearerAuth: [], + }, + ], }; // Options for the swagger docs const options = { swaggerDefinition, - apis: ['./routes/*.js'], + apis: ['./routes/*.js'], // Specify the path to API files with JSDoc comments }; // Initialize swagger-jsdoc diff --git a/backend/index.js b/backend/index.js index 759472e..7319f9a 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,14 +1,15 @@ +const dotenv = require('dotenv'); +dotenv.config(); + const express = require('express'); const cors = require('cors'); const mongoose = require('mongoose'); -const dotenv = require('dotenv'); const seedDB = require('./seed/productSeeds'); const productRoutes = require('./routes/products'); const checkoutRoutes = require('./routes/checkout'); +const authRoutes = require('./routes/auth'); const { swaggerUi, swaggerSpec } = require('./docs/swagger'); -dotenv.config(); - // Create Express App const app = express(); const PORT = process.env.PORT || 8000; @@ -33,6 +34,7 @@ app.get('/', (req, res) => { app.use('/api/products', productRoutes); app.use('/api/checkout', checkoutRoutes); app.use('/api/search', require('./routes/search')); +app.use('/api/auth', authRoutes); app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); // Seed database on startup diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js new file mode 100644 index 0000000..369cdc1 --- /dev/null +++ b/backend/middleware/auth.js @@ -0,0 +1,26 @@ +const jwt = require('jsonwebtoken'); +const JWT_SECRET = process.env.JWT_SECRET; + +// Authentication middleware for protected routes +module.exports = function (req, res, next) { + // Get token from the header + const token = req.header('x-auth-token'); + + // Check if there's no token + if (!token) { + return res.status(401).json({ msg: 'No token, authorization denied' }); + } + + try { + // Verify the token + const decoded = jwt.verify(token, JWT_SECRET); + + // Attach the user payload to the request object + req.user = decoded.user; + + // Call next middleware + next(); + } catch (err) { + res.status(401).json({ msg: 'Token is not valid' }); + } +}; diff --git a/backend/models/user.js b/backend/models/user.js new file mode 100644 index 0000000..acf29bc --- /dev/null +++ b/backend/models/user.js @@ -0,0 +1,23 @@ +const mongoose = require('mongoose'); + +const UserSchema = new mongoose.Schema({ + name: { + type: String, + required: true + }, + email: { + type: String, + required: true, + unique: true + }, + password: { + type: String, + required: true + }, + date: { + type: Date, + default: Date.now + } +}); + +module.exports = mongoose.model('User', UserSchema); diff --git a/backend/package-lock.json b/backend/package-lock.json index cabf6ad..3e1a832 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,6 +12,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", + "express-validator": "^7.2.0", "mongoose": "^8.4.3", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1" @@ -464,6 +465,18 @@ "node": ">= 0.10.0" } }, + "node_modules/express-validator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.0.tgz", + "integrity": "sha512-I2ByKD8panjtr8Y05l21Wph9xk7kk64UMyvJCl/fFM/3CTJq8isXYPLeKW/aZBCdb/LYNv63PwhY8khw8VWocA==", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.12.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -766,6 +779,11 @@ "node": ">=12.0.0" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -1899,6 +1917,15 @@ "vary": "~1.1.2" } }, + "express-validator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.0.tgz", + "integrity": "sha512-I2ByKD8panjtr8Y05l21Wph9xk7kk64UMyvJCl/fFM/3CTJq8isXYPLeKW/aZBCdb/LYNv63PwhY8khw8VWocA==", + "requires": { + "lodash": "^4.17.21", + "validator": "~13.12.0" + } + }, "fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2111,6 +2138,11 @@ "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==" }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", diff --git a/backend/package.json b/backend/package.json index c71d39b..68d0c63 100644 --- a/backend/package.json +++ b/backend/package.json @@ -28,6 +28,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", + "express-validator": "^7.2.0", "mongoose": "^8.4.3", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1" diff --git a/backend/routes/auth.js b/backend/routes/auth.js new file mode 100644 index 0000000..ddec963 --- /dev/null +++ b/backend/routes/auth.js @@ -0,0 +1,326 @@ +const express = require('express'); +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); +const { check, validationResult } = require('express-validator'); +const User = require('../models/user'); +const router = express.Router(); + +const JWT_SECRET = process.env.JWT_SECRET; + +/** + * @swagger + * components: + * schemas: + * User: + * type: object + * required: + * - name + * - email + * - password + * properties: + * id: + * type: string + * description: The auto-generated ID of the user + * name: + * type: string + * description: The user's name + * email: + * type: string + * description: The user's email + * password: + * type: string + * description: The user's hashed password + * example: + * id: 60c72b2f5f1b2c0012a4c56e + * name: John Doe + * email: john@example.com + * password: password123 + */ + +/** + * @swagger + * /api/auth/register: + * post: + * summary: Register a new user + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * description: User's name + * email: + * type: string + * description: User's email + * password: + * type: string + * description: User's password + * example: + * name: John Doe + * email: john@example.com + * password: password123 + * responses: + * 200: + * description: User registered and JWT token returned + * content: + * application/json: + * schema: + * type: object + * properties: + * token: + * type: string + * description: JWT token for user authentication + * 400: + * description: Bad request - Validation error or user already exists + * 500: + * description: Server error + */ +router.post( + '/register', + [ + check('name', 'Name is required').not().isEmpty(), + check('email', 'Please include a valid email').isEmail(), + check('password', 'Please enter a password with 6 or more characters').isLength({ min: 6 }) + ], + async (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const { name, email, password } = req.body; + + try { + let user = await User.findOne({ email }); + if (user) { + return res.status(400).json({ msg: 'User already exists' }); + } + + user = new User({ + name, + email, + password + }); + + const salt = await bcrypt.genSalt(10); + user.password = await bcrypt.hash(password, salt); + + await user.save(); + + const payload = { + user: { + id: user.id + } + }; + + jwt.sign(payload, JWT_SECRET, { expiresIn: '1h' }, (err, token) => { + if (err) throw err; + res.json({ token }); + }); + } catch (err) { + console.error(err.message); + res.status(500).send('Server error'); + } + } +); + +/** + * @swagger + * /api/auth/login: + * post: + * summary: Authenticate a user and return a JWT token + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * description: User's email + * password: + * type: string + * description: User's password + * example: + * email: john@example.com + * password: password123 + * responses: + * 200: + * description: User authenticated and JWT token returned + * content: + * application/json: + * schema: + * type: object + * properties: + * token: + * type: string + * description: JWT token for user authentication + * 400: + * description: Invalid credentials or validation error + * 500: + * description: Server error + */ +router.post( + '/login', + [ + check('email', 'Please include a valid email').isEmail(), + check('password', 'Password is required').exists() + ], + async (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const { email, password } = req.body; + + try { + let user = await User.findOne({ email }); + if (!user) { + return res.status(400).json({ msg: 'Invalid credentials' }); + } + + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) { + return res.status(400).json({ msg: 'Invalid credentials' }); + } + + const payload = { + user: { + id: user.id + } + }; + + jwt.sign(payload, JWT_SECRET, { expiresIn: '1h' }, (err, token) => { + if (err) throw err; + res.json({ token }); + }); + } catch (err) { + console.error(err.message); + res.status(500).send('Server error'); + } + } +); + +/** + * @swagger + * /api/auth/verify-email: + * post: + * summary: Verify if an email exists in the system + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * description: User's email to verify + * example: + * email: john@example.com + * responses: + * 200: + * description: Email exists and is valid for password reset + * 400: + * description: Invalid email or user not found + * 500: + * description: Server error + */ +router.post( + '/verify-email', + [ + check('email', 'Please include a valid email').isEmail() + ], + async (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const { email } = req.body; + + try { + let user = await User.findOne({ email }); + if (!user) { + return res.status(400).json({ msg: 'Invalid email. User not found' }); + } + + res.json({ msg: 'Email is valid, you can proceed to reset your password' }); + } catch (err) { + console.error(err.message); + res.status(500).send('Server error'); + } + } +); + +/** + * @swagger + * /api/auth/reset-password: + * post: + * summary: Reset a user's password + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * description: User's email + * password: + * type: string + * description: New password for the user + * example: + * email: john@example.com + * password: newpassword123 + * responses: + * 200: + * description: Password reset successfully + * 400: + * description: Invalid email or validation error + * 500: + * description: Server error + */ +router.post( + '/reset-password', + [ + check('email', 'Please include a valid email').isEmail(), + check('password', 'Please enter a password with 6 or more characters').isLength({ min: 6 }) + ], + async (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const { email, password } = req.body; + + try { + let user = await User.findOne({ email }); + if (!user) { + return res.status(400).json({ msg: 'Invalid email. User not found' }); + } + + const salt = await bcrypt.genSalt(10); + user.password = await bcrypt.hash(password, salt); + await user.save(); + + res.json({ msg: 'Password successfully reset' }); + } catch (err) { + console.error(err.message); + res.status(500).send('Server error'); + } + } +); + +module.exports = router; diff --git a/package-lock.json b/package-lock.json index 80d4f71..de965b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,8 +22,11 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.7.2", + "bcryptjs": "^2.4.3", "concurrently": "^9.0.1", "http-proxy-middleware": "^3.0.0", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.7.2", "prettier": "^3.3.3", "react": "^18.3.1", "react-credit-cards-2": "^1.0.2", @@ -3397,6 +3400,14 @@ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==" }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", + "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, "node_modules/@mui/base": { "version": "5.0.0-beta.40", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", @@ -5012,6 +5023,19 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, "node_modules/@types/ws": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", @@ -6238,6 +6262,11 @@ "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==" }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "node_modules/bfj": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.1.0.tgz", @@ -6399,6 +6428,19 @@ "node-int64": "^0.4.0" } }, + "node_modules/bson": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz", + "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -7832,6 +7874,14 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -13016,6 +13066,27 @@ "node": ">=0.10.0" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -13030,6 +13101,33 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -13162,6 +13260,36 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -13172,6 +13300,11 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -13287,6 +13420,11 @@ "node": ">= 4.0.0" } }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -13433,6 +13571,136 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mongodb": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.9.0.tgz", + "integrity": "sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA==", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.7.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", + "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/mongoose": { + "version": "8.7.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.7.2.tgz", + "integrity": "sha512-Ok4VzMds9p5G3ZSUhmvBm1GdxanbzhS29jpSn02SPj+IXEVFnIdfwAlHHXWkyNscZKlcn8GuMi68FH++jo0flg==", + "dependencies": { + "bson": "^6.7.0", + "kareem": "2.6.3", + "mongodb": "6.9.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -16839,6 +17107,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -16967,6 +17240,14 @@ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "deprecated": "Please use @jridgewell/sourcemap-codec instead" }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/spdy": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", @@ -21537,6 +21818,14 @@ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==" }, + "@mongodb-js/saslprep": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", + "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, "@mui/base": { "version": "5.0.0-beta.40", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", @@ -22677,6 +22966,19 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, + "@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "requires": { + "@types/webidl-conversions": "*" + } + }, "@types/ws": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", @@ -23566,6 +23868,11 @@ "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==" }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "bfj": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.1.0.tgz", @@ -23687,6 +23994,16 @@ "node-int64": "^0.4.0" } }, + "bson": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz", + "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==" + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -24700,6 +25017,14 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -28424,6 +28749,23 @@ "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==" }, + "jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + } + }, "jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -28435,6 +28777,30 @@ "object.values": "^1.1.6" } }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==" + }, "keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -28537,6 +28903,36 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -28547,6 +28943,11 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -28640,6 +29041,11 @@ "fs-monkey": "^1.0.4" } }, + "memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -28737,6 +29143,83 @@ "minimist": "^1.2.6" } }, + "mongodb": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.9.0.tgz", + "integrity": "sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA==", + "requires": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.7.0", + "mongodb-connection-string-url": "^3.0.0" + } + }, + "mongodb-connection-string-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", + "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "requires": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + }, + "dependencies": { + "tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "requires": { + "punycode": "^2.3.0" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" + }, + "whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "requires": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + } + } + } + }, + "mongoose": { + "version": "8.7.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.7.2.tgz", + "integrity": "sha512-Ok4VzMds9p5G3ZSUhmvBm1GdxanbzhS29jpSn02SPj+IXEVFnIdfwAlHHXWkyNscZKlcn8GuMi68FH++jo0flg==", + "requires": { + "bson": "^6.7.0", + "kareem": "2.6.3", + "mongodb": "6.9.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==" + }, + "mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "requires": { + "debug": "4.x" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -30980,6 +31463,11 @@ "object-inspect": "^1.13.1" } }, + "sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" + }, "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -31088,6 +31576,14 @@ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" }, + "sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "requires": { + "memory-pager": "^1.0.2" + } + }, "spdy": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", diff --git a/package.json b/package.json index 9666fb7..4bfeeec 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,11 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.7.2", + "bcryptjs": "^2.4.3", "concurrently": "^9.0.1", "http-proxy-middleware": "^3.0.0", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.7.2", "prettier": "^3.3.3", "react": "^18.3.1", "react-credit-cards-2": "^1.0.2", diff --git a/src/App.jsx b/src/App.jsx index 5af0115..1a5ad30 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -9,6 +9,10 @@ import Cart from './pages/Cart'; import Checkout from './pages/Checkout'; import OrderSuccess from './pages/OrderSuccess'; import ProductDetails from './pages/ProductDetails'; +import Login from './pages/Login'; +import Register from './pages/Register'; +import ForgotPassword from './pages/ForgotPassword'; +import ResetPassword from './pages/ResetPassword'; const theme = createTheme({ palette: { @@ -68,6 +72,14 @@ function App() { } /> } /> + + } /> + + } /> + + } /> + + } /> diff --git a/src/components/NavigationBar.jsx b/src/components/NavigationBar.jsx index 39a3c01..c19d9ae 100644 --- a/src/components/NavigationBar.jsx +++ b/src/components/NavigationBar.jsx @@ -1,20 +1,37 @@ import * as React from 'react'; -import { AppBar, Toolbar, Typography, Button, IconButton, Menu, MenuItem, Badge, InputBase, useMediaQuery, Box } from '@mui/material'; +import { AppBar, Toolbar, Typography, Button, IconButton, Menu, MenuItem, Badge, InputBase, useMediaQuery, Box, CircularProgress } from '@mui/material'; import MenuIcon from '@mui/icons-material/Menu'; import SearchIcon from '@mui/icons-material/Search'; import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; -import { Link, useLocation } from 'react-router-dom'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; import axios from 'axios'; import SearchResults from './SearchResults'; +import { debounce } from 'lodash'; // Debounce function from lodash function NavigationBar({ cartItemCount }) { const [anchorEl, setAnchorEl] = React.useState(null); const [searchQuery, setSearchQuery] = React.useState(''); const [searchResults, setSearchResults] = React.useState([]); + const [loading, setLoading] = React.useState(false); // Loading state for search + const [isLoggedIn, setIsLoggedIn] = React.useState(false); // State to track login status const searchBarRef = React.useRef(null); + const searchResultsRef = React.useRef(null); // To detect clicks outside search results const open = Boolean(anchorEl); const location = useLocation(); - const isMobile = useMediaQuery('(max-width:600px)'); + const navigate = useNavigate(); + const isMobile = useMediaQuery('(max-width:900px)'); + + // Check if user is logged in by looking for token in localStorage + React.useEffect(() => { + const checkToken = () => { + const token = localStorage.getItem('MERNEcommerceToken'); + setIsLoggedIn(!!token); // Set loggedIn state based on token presence + }; + checkToken(); // Initial check + const interval = setInterval(checkToken, 2000); // Check every 2 seconds + + return () => clearInterval(interval); // Cleanup interval on component unmount + }, []); const handleClick = event => { setAnchorEl(event.currentTarget); @@ -26,23 +43,61 @@ function NavigationBar({ cartItemCount }) { const handleSearchChange = event => { setSearchQuery(event.target.value); + debouncedSearch(event.target.value); // Trigger the debounced search }; const handleSearchResultClick = () => { setSearchResults([]); }; - const handleSearchSubmit = async event => { - event.preventDefault(); - try { - const response = await axios.get(`https://mern-stack-ecommerce-app-h5wb.onrender.com/api/search?q=${searchQuery}`); // Specify port 5000 - setSearchResults(response.data); - } catch (error) { - console.error('Error fetching search results:', error); - setSearchResults([]); - } + const handleLogout = () => { + localStorage.removeItem('MERNEcommerceToken'); // Remove token from localStorage + setIsLoggedIn(false); + navigate('/'); // Redirect to homepage after logout }; + // Debounced function to prevent triggering the search too often + const debouncedSearch = React.useCallback( + debounce(async (query) => { + if (query.trim() === '') { + setSearchResults([]); // Clear search results if the query is empty + setLoading(false); + return; + } + setLoading(true); // Set loading to true when search is triggered + try { + const response = await axios.get(`https://mern-stack-ecommerce-app-h5wb.onrender.com/api/search?q=${query}`); + setSearchResults(response.data); + } catch (error) { + console.error('Error fetching search results:', error); + setSearchResults([]); + } finally { + setLoading(false); // Stop loading when the API call finishes + } + }, 300), // 300ms debounce delay + [] + ); + + // Event listener to hide search results if clicking outside search bar or results + React.useEffect(() => { + const handleClickOutside = (event) => { + // Check if click is outside search bar and search results + if ( + searchBarRef.current && + !searchBarRef.current.contains(event.target) && + searchResultsRef.current && + !searchResultsRef.current.contains(event.target) + ) { + setSearchResults([]); // Hide search results on outside click + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); // Cleanup listener on unmount + }; + }, []); + return ( -
+ e.preventDefault()}> - + + {/* Display loading spinner if search is in progress */} + {loading && ( + + )} @@ -128,11 +199,42 @@ function NavigationBar({ cartItemCount }) { component={Link} to="/shop" className={location.pathname === '/shop' ? 'active' : ''} - sx={{ fontSize: '1rem', marginLeft: '1rem', marginRight: '1rem' }} + sx={{ fontSize: '1rem', marginLeft: '0.5rem', marginRight: '0.5rem' }} > Shop - + + {/* Login/Logout and Register */} + {isLoggedIn ? ( + + ) : ( + <> + + + )} + + + {/* Cart Icon */} + @@ -141,27 +243,25 @@ function NavigationBar({ cartItemCount }) { )} - {searchResults.length > 0 && ( + {searchResults.length > 0 && searchBarRef.current && ( - - - + )}
diff --git a/src/components/SearchResults.jsx b/src/components/SearchResults.jsx index 5d15f57..326c1c8 100644 --- a/src/components/SearchResults.jsx +++ b/src/components/SearchResults.jsx @@ -13,10 +13,10 @@ function SearchResults({ results, onResultClick, setSearchResults }) { elevation={3} sx={{ width: '100%', - maxWidth: 360, - maxHeight: '300px', + maxWidth: '50vw', + maxHeight: '50vh', overflowY: 'auto', - padding: '0.5rem', + padding: '1rem', }} > @@ -30,6 +30,7 @@ function SearchResults({ results, onResultClick, setSearchResults }) { onClick={handleItemClick} sx={{ borderBottom: '1px solid #eee', + width: '100%', '&:last-child': { borderBottom: 'none', }, @@ -39,7 +40,7 @@ function SearchResults({ results, onResultClick, setSearchResults }) { }} > - + { error: false, }); + console.log(setStatus); + return status; }; diff --git a/src/pages/Checkout.jsx b/src/pages/Checkout.jsx index d7ccf4d..514f47f 100644 --- a/src/pages/Checkout.jsx +++ b/src/pages/Checkout.jsx @@ -1,6 +1,5 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import axios from 'axios'; import CheckoutForm from '../components/CheckoutForm'; import { Typography, CircularProgress } from '@mui/material'; @@ -15,6 +14,7 @@ function Checkout({ cartItems }) { try { // Simulate API call to create an order - This is a demo website so we do not have capacity to handle a real order yet + // Remember to import axios at the top of the file if you want to use it here // const response = await axios.post('http://localhost:5000/api/checkout/create-order', { // items: cartItems, // name: formData.name, diff --git a/src/pages/ForgotPassword.js b/src/pages/ForgotPassword.js new file mode 100644 index 0000000..4b77571 --- /dev/null +++ b/src/pages/ForgotPassword.js @@ -0,0 +1,68 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Box, Container, TextField, Typography, Button, CircularProgress, Paper } from '@mui/material'; +import axios from 'axios'; + +function ForgotPassword() { + const [email, setEmail] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const navigate = useNavigate(); + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + setError(null); + + try { + // Verify email + await axios.post('/api/auth/verify-email', { email }); + // If successful, navigate to reset password page + navigate('/reset-password'); + } catch (err) { + setError(err.response?.data?.msg || 'Failed to verify email'); + } finally { + setLoading(false); + } + }; + + return ( + + + + Forgot Password + + + {error && ( + + {error} + + )} + +
+ setEmail(e.target.value)} + required + /> + + + {loading ? ( + + ) : ( + + )} + + +
+
+ ); +} + +export default ForgotPassword; diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx new file mode 100644 index 0000000..fbf592e --- /dev/null +++ b/src/pages/Login.jsx @@ -0,0 +1,88 @@ +import React, { useState } from 'react'; +import { Box, Container, TextField, Typography, Button, CircularProgress, Paper } from '@mui/material'; +import axios from 'axios'; + +function Login() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleLogin = async (e) => { + e.preventDefault(); + setLoading(true); + setError(null); + + try { + const response = await axios.post('https://mern-stack-ecommerce-app-h5wb.onrender.com/api/auth/login', { email, password }); + const token = response.data.token; + // Store token in localStorage or sessionStorage + localStorage.setItem('MERNEcommerceToken', token); + // Redirect to the homepage or dashboard + window.location.href = '/'; + } catch (err) { + setError(err.response?.data?.msg || 'Login failed'); + } finally { + setLoading(false); + } + }; + + return ( + + + + Login + + + {error && ( + + {error} + + )} + +
+ setEmail(e.target.value)} + required + /> + setPassword(e.target.value)} + required + /> + + + {loading ? ( + + ) : ( + + )} + + + + + + Forgot password? + + + Don't have an account? Register here + + +
+
+ ); +} + +export default Login; diff --git a/src/pages/Register.jsx b/src/pages/Register.jsx new file mode 100644 index 0000000..87690fc --- /dev/null +++ b/src/pages/Register.jsx @@ -0,0 +1,95 @@ +import React, { useState } from 'react'; +import { Box, Container, TextField, Typography, Button, CircularProgress, Paper } from '@mui/material'; +import axios from 'axios'; // For making API calls + +function Register() { + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleRegister = async (e) => { + e.preventDefault(); + setLoading(true); + setError(null); + + try { + const response = await axios.post('https://mern-stack-ecommerce-app-h5wb.onrender.com/api/auth/register', { name, email, password }); + const token = response.data.token; + // Store token in localStorage or sessionStorage + localStorage.setItem('token', token); + // Redirect to the homepage or dashboard + window.location.href = '/'; + } catch (err) { + setError(err.response?.data?.msg || 'Registration failed'); + } finally { + setLoading(false); + } + }; + + return ( + + + + Register + + + {error && ( + + {error} + + )} + +
+ setName(e.target.value)} + required + /> + setEmail(e.target.value)} + required + /> + setPassword(e.target.value)} + required + /> + + + {loading ? ( + + ) : ( + + )} + + + + + + Already have an account? Login here + + +
+
+ ); +} + +export default Register; diff --git a/src/pages/ResetPassword.js b/src/pages/ResetPassword.js new file mode 100644 index 0000000..f6272ad --- /dev/null +++ b/src/pages/ResetPassword.js @@ -0,0 +1,101 @@ +import React, { useState } from 'react'; +import { Box, Container, TextField, Typography, Button, CircularProgress, Paper } from '@mui/material'; +import axios from 'axios'; + +function ResetPassword() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(null); + const [error, setError] = useState(null); + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + setError(null); + + // Check if password and confirmPassword match + if (password !== confirmPassword) { + setError("Passwords do not match"); + setLoading(false); + return; + } + + try { + // Make request to reset password + await axios.post('/api/auth/reset-password', { email, password }); + setSuccess('Password successfully reset'); + } catch (err) { + setError(err.response?.data?.msg || 'Failed to reset password'); + } finally { + setLoading(false); + } + }; + + return ( + + + + Reset Password + + + {success && ( + + {success} + + )} + + {error && ( + + {error} + + )} + +
+ setEmail(e.target.value)} + required + /> + setPassword(e.target.value)} + required + /> + setConfirmPassword(e.target.value)} + required + /> + + + {loading ? ( + + ) : ( + + )} + + +
+
+ ); +} + +export default ResetPassword;