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 (
-
@@ -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}
+
+ )}
+
+
+
+
+ );
+}
+
+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}
+
+ )}
+
+
+
+
+
+ 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}
+
+ )}
+
+
+
+
+
+ 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}
+
+ )}
+
+
+
+
+ );
+}
+
+export default ResetPassword;