diff --git a/Sean/modules/otpUtils.js b/Sean/modules/otpUtils.js new file mode 100644 index 0000000..c824c5b --- /dev/null +++ b/Sean/modules/otpUtils.js @@ -0,0 +1,42 @@ +const nodemailer = require("nodemailer"); +const otpGenerator = require('otp-generator'); +const path = require('path') +require('dotenv').config({ path: path.resolve(__dirname, '../.env') }) + +const generateOTP = () => { + const otp = otpGenerator.generate(6, { upperCase: false, specialChars: false }); + const expirationTime = Date.now() + 5 * 60 * 1000; // 5 minutes expiration + return { otp, expirationTime }; + }; +const sendOTPByEmail = async (email, otp) => { + try { + const transporter = nodemailer.createTransport({ + service: 'gmail', + host: 'smtp.gmail.com', + port: 587, // use the appropriate port for your SMTP server + secure: false, // true for 465, false for other ports + auth: { + user: process.env.euser, // replace with your email + pass: process.env.epass // replace with your email password + } + }); + + const mailOptions = { + from: process.env.euser, + to: email, + subject: 'Login OTP', + text: `Your OTP for login is: ${otp}` + }; + + await transporter.sendMail(mailOptions); + console.log('OTP sent successfully to', email); + } catch (error) { + console.error('Error sending OTP:', error); + throw error; + } + }; + + module.exports = { + generateOTP, + sendOTPByEmail + }; \ No newline at end of file diff --git a/Sean/modules/rateLimitMiddleware.js b/Sean/modules/rateLimitMiddleware.js new file mode 100644 index 0000000..1f0c2b9 --- /dev/null +++ b/Sean/modules/rateLimitMiddleware.js @@ -0,0 +1,9 @@ +const rateLimit = require('express-rate-limit'); + +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 5, // limit each IP to 5 requests per windowMs + message: 'Too many login attempts from this IP, please try again later.', +}); + +module.exports = limiter; diff --git a/Sean/modules/validationMiddleware.js b/Sean/modules/validationMiddleware.js new file mode 100644 index 0000000..3551e12 --- /dev/null +++ b/Sean/modules/validationMiddleware.js @@ -0,0 +1,77 @@ +const { body } = require('express-validator'); + +const locationValidation = [ + body('name').trim().isLength({ min: 1 }).withMessage('Name must not be empty').escape(), + body('added_by').trim().isLength({ min: 1 }).withMessage('Added by must not be empty').escape(), + body('description').trim().escape(), +]; + +const locationValidationUpdate = [ + body('id').trim().escape(), + body('name').trim().isLength({ min: 1 }).withMessage('Name must not be empty').escape(), + body('added_by').trim().isLength({ min: 1 }).withMessage('Added by must not be empty').escape(), + body('description').trim().escape(), + ]; + +const locationdeleteValidation = [ + body('id').trim().escape() + ]; + +const sensorValidation = [ + body('sensorname').trim().isLength({ min: 1 }).withMessage('Sensor Name must not be empty').escape(), + body('added_by').trim().isLength({ min: 1 }).withMessage('Added by must not be empty').escape(), + body('macAddress').custom(value => { + const macAddressRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/; + if (!macAddressRegex.test(value)) { + throw new Error('Invalid MAC address format'); + } + return true; + }).withMessage('Invalid MAC address format').escape(), + body('description').trim().escape(), + body('location').trim().escape() + ]; + +const sensorupdateValidation = [ + body('id').trim().escape(), + body('sensorname').trim().isLength({ min: 1 }).withMessage('Sensor Name must not be empty').escape(), + body('added_by').trim().isLength({ min: 1 }).withMessage('Added by must not be empty').escape(), + body('macAddress').custom(value => { + const macAddressRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/; + if (!macAddressRegex.test(value)) { + throw new Error('Invalid MAC address format'); + } + return true; + }).withMessage('Invalid MAC address format').escape(), + body('description').trim().escape(), + body('location').trim().escape() + ]; + +const sensordeleteValidation = [ + body('id').trim().escape() + ]; + +const loginValidation = [ + body('username').escape().trim().isLength({ min: 1 }).withMessage('Username must not be empty'), + body('password').escape().trim().isLength({ min: 1 }).withMessage('Password must not be empty'), + ]; + +const otpValidation = [ + body('otp').escape().trim().isLength({ min: 1 }).withMessage('OTP must not be empty'), + ]; + +const createValidation = [ + body('name').trim().isLength({ min: 1 }).withMessage('Name must not be empty').escape(), + body('username').trim().isLength({ min: 1 }).withMessage('Username must not be empty').escape(), + body('email').isEmail().withMessage('Invalid email address').normalizeEmail(), + body('password').custom((value) => { + if (!isStrongPassword(value)) { throw new Error('Password does not meet complexity requirements'); } return true; + }), + body('jobTitle').trim().isLength({ min: 1 }).withMessage('Job title must not be empty').escape(), + ]; + + +module.exports = { + locationValidation,locationValidationUpdate,locationdeleteValidation + ,sensorValidation,sensorupdateValidation,sensordeleteValidation,loginValidation,otpValidation + ,createValidation +}; diff --git a/Sean/server.js b/Sean/server.js index fe49ff8..d1d1bdb 100644 --- a/Sean/server.js +++ b/Sean/server.js @@ -1,21 +1,23 @@ const express = require("express"); const session = require("express-session"); -const rateLimit = require('express-rate-limit'); const cookieParser = require('cookie-parser'); const bodyParser = require("body-parser"); const bcrypt = require("bcrypt"); const crypto = require("crypto"); -const nodemailer = require("nodemailer"); -const otpGenerator = require('otp-generator'); -const { body, validationResult } = require('express-validator'); const validator = require('validator'); const axios = require('axios'); +const {validationResult } = require('express-validator'); +const {locationValidation, locationValidationUpdate, locationdeleteValidation +,sensorValidation, sensorupdateValidation, sensordeleteValidation, loginValidation +,otpValidation, createValidation} = require('./modules/validationMiddleware'); +const rateLimit = require('./modules/rateLimitMiddleware'); +const { generateOTP, sendOTPByEmail } = require('./modules/otpUtils'); const { format } = require('date-fns'); const { Sequelize } = require('sequelize'); const { transporter } = require("./modules/nodeMailer"); const { sequelize, User } = require("./modules/mysql"); -const userLogs= require('./models/userLogs')(sequelize); // Adjust the path based on your project structure +const userLogs= require('./models/userLogs')(sequelize); const app = express(); app.use(bodyParser.urlencoded({ extended: true })); @@ -28,7 +30,6 @@ app.use(bodyParser.urlencoded({ extended: true })); app.set("view engine", "ejs"); - app.use(session({ secret: process.env.key, resave: false, @@ -46,76 +47,27 @@ function isAuthenticated(req, res, next) { res.redirect("/login"); } } -const generateOTP = () => { - const otp = otpGenerator.generate(6, { upperCase: false, specialChars: false }); - const expirationTime = Date.now() + 5 * 60 * 1000; // 5 minutes expiration - return { otp, expirationTime }; - }; - -const sendOTPByEmail = async (email, otp) => { - try { - const transporter = nodemailer.createTransport({ - service: 'gmail', - auth: { - user: process.env.euser, // replace with your email - pass: process.env.epass // replace with your email password - } - }); - - const mailOptions = { - from: process.env.euser, - to: email, - subject: 'Login OTP', - text: `Your OTP for login is: ${otp}` - }; - - await transporter.sendMail(mailOptions); - console.log('OTP sent successfully to', email); - } catch (error) { - console.error('Error sending OTP:', error); - throw error; - } - }; - app.get("/login", (req, res) => { - res.render("login", { error: null }); }); - const limiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 5, // limit each IP to 3 requests per windowMs - message: 'Too many login attempts from this IP, please try again later.', - }); - -app.use('/login', limiter); +app.use('/login', rateLimit); - -app.post('/login', [ - body('username').escape().trim().isLength({ min: 1 }).withMessage('Username must not be empty'), - body('password').escape().trim().isLength({ min: 1 }).withMessage('Password must not be empty'), -], -async (req, res) => { - try { - const errors = validationResult(req); - +app.post('/login', loginValidation, async (req, res) => { + try {const errors = validationResult(req); if (!errors.isEmpty()) { return res.render('login', { error: 'Invalid input. Please check your credentials.', csrfToken: req.session.csrfToken }); } - let { username, password } = req.body; username = username.trim(); - const user = await User.findOne({ where: { username } }); - if (user) { const isLoginSuccessful = await bcrypt.compare(password, user.password); if (isLoginSuccessful) { await userLogs.create({ username, success: true, activity: "Credentials entered correctly" }); - const { otp, expirationTime } = generateOTP(); req.session.otp = otp; @@ -161,17 +113,12 @@ async (req, res) => { // OTP verification route -app.post("/verify-otp", [ - body('otp').escape().trim().isLength({ min: 1 }).withMessage('OTP must not be empty'), - ], - async (req, res) => { +app.post("/verify-otp", otpValidation ,async (req, res) => { try { const errors = validationResult(req); - if (!errors.isEmpty()) { return res.render('otp', { error: 'Invalid OTP. Please try again.'}); } - const enteredOTP = req.body.otp; if (!req.session) { @@ -209,17 +156,10 @@ app.post("/verify-otp", [ req.session.authenticated = true; req.session.username = req.body.username; req.session.sessionToken = sessionToken; - - csrfTokenSession = crypto.randomBytes(32).toString('hex'); - // Log anti-CSRF token - console.log(`Generated Anti-CSRF Token: ${csrfTokenSession}`); - res.cookie('sessionToken', sessionToken, { secure: true, httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000) }); // Expires in 1 day - console.log(`Generated Session Token: ${sessionToken}`); - res.redirect("/home"); } else { if (req.body.username) { @@ -262,17 +202,14 @@ app.post("/verify-otp", [ } }); - - - app.get("/home", isAuthenticated, (req, res) => { - // Render the home page with sensor data - res.render("home", { - username: req.session.username, - }); + app.get("/home", isAuthenticated, async (req, res) => { + + const response = await axios.get(process.env.API_ALLLOCATION); + const valueData = response.data; + console.log = (valueData); + res.render("home", { username: req.session.username, valueData}); }); - - app.get("/inusers", isAuthenticated, async (req, res) => { try { // Fetch all user data from the database using Sequelize @@ -318,20 +255,8 @@ function isStrongPassword(password) { return true; } - - app.post( - '/createUser', - [ - body('name').trim().isLength({ min: 1 }).withMessage('Name must not be empty').escape(), - body('username').trim().isLength({ min: 1 }).withMessage('Username must not be empty').escape(), - body('email').isEmail().withMessage('Invalid email address').normalizeEmail(), - body('password').custom((value) => { - if (!isStrongPassword(value)) { throw new Error('Password does not meet complexity requirements'); } return true; - }), - body('jobTitle').trim().isLength({ min: 1 }).withMessage('Job title must not be empty').escape(), - ], - async (req, res) => { + '/createUser', createValidation, async (req, res) => { try { const errors = validationResult(req); @@ -463,24 +388,13 @@ app.post("/forgot-password", async (req, res) => { const error = "Username or email not found."; return res.render("forgot-password", { error, success: null }); } - // Generate reset token and update the user const reset_token = crypto.randomBytes(20).toString("hex"); const reset_token_expiry = new Date(Date.now() + 3600000); // Token expires in 1 hour - // Update the user with the reset token and expiry - await User.update( - { - reset_token, - reset_token_expiry, - }, - { - where: { - id: user.id, // Replace 'id' with the actual primary key field of your User model - }, - } + await User.update({reset_token,reset_token_expiry,}, + {where: {id: user.id},} ); - // Send email with reset link const resetLink = `http://localhost:3000/reset-password/${reset_token}`; const mailOptions = { @@ -488,12 +402,9 @@ app.post("/forgot-password", async (req, res) => { subject: "Password Reset", text: `Click on the following link to reset your password: ${resetLink}`, }; - await transporter.sendMail(mailOptions); - const success = "Password reset email sent successfully. Check your inbox."; res.render("forgot-password", { error: null, success }); - // Log the successful sending of the reset link in the database await userLogs.create({ username: user.username, @@ -509,29 +420,22 @@ app.post("/forgot-password", async (req, res) => { console.error("Error during password reset:", error); const errorMessage = "An error occurred during the password reset process."; res.render("forgot-password", { error: errorMessage, success: null }); - } - }); + }}); - - app.post("/reset-password/:token", async (req, res) => { try { const { token } = req.params; const { password, confirmPassword } = req.body; - // Sanitize the inputs const sanitizedToken = validator.escape(token); const sanitizedPassword = validator.escape(password); const sanitizedConfirmPassword = validator.escape(confirmPassword); - // Find user with matching reset token and not expired const user = await User.findOne({ - where: { - reset_token: sanitizedToken, - reset_token_expiry: { [Sequelize.Op.gt]: new Date() }, + where: {reset_token: sanitizedToken, + reset_token_expiry: { [Sequelize.Op.gt]: new Date() }, }, }); - if (!user) { // Pass the error to the template when rendering the reset-password page return res.render("reset-password", { @@ -539,7 +443,6 @@ app.post("/forgot-password", async (req, res) => { resetError: "Invalid or expired reset token", }); } - // Check if passwords match if (sanitizedPassword !== sanitizedConfirmPassword) { // Pass the error to the template when rendering the reset-password page @@ -548,31 +451,24 @@ app.post("/forgot-password", async (req, res) => { resetError: "Passwords do not match", }); } - // Check if the new password meets complexity requirements if (!isStrongPassword(sanitizedPassword)) { // Pass the error to the template when rendering the reset-password page return res.render("reset-password", { - token, - resetError: + token, resetError: "Password does not meet complexity requirements. It must be at least 10 characters long and include at least one uppercase letter, one lowercase letter, one digit, and one symbol.", }); } - // Hash the new password const saltRounds = 10; const hashedPassword = await bcrypt.hash(sanitizedPassword, saltRounds); - // Update user's password and clear reset token const updateQuery = { password: hashedPassword, reset_token: null, reset_token_expiry: null, }; - const whereCondition = { - reset_token: sanitizedToken, - }; - + const whereCondition = {reset_token: sanitizedToken,}; await User.update(updateQuery, { where: whereCondition, }); @@ -619,10 +515,8 @@ app.post("/reset-password", async (req, res) => { return res.status(403).json({ error: 'CSRF token mismatch' }); } const sessionTokencookie = req.cookies['sessionToken']; - // Verify sessionToken with the one stored in the database const user = await User.findOne({ where: { sessionid: sessionTokencookie } }); - if (!user) { return res.status(403).json({ error: 'Invalid sessionToken' }); } @@ -630,12 +524,10 @@ app.post("/reset-password", async (req, res) => { const sanitizedUsername = validator.escape(username); const sanitizedPassword = validator.escape(password); const sanitizedConfirmPassword = validator.escape(confirmPassword); - // Check if passwords match if (sanitizedPassword !== sanitizedConfirmPassword) { return res.status(400).json({ error: "Passwords do not match" }); } - // Check if the new password meets complexity requirements if (!isStrongPassword(sanitizedPassword)) { return res.status(400).json({ @@ -643,31 +535,25 @@ app.post("/reset-password", async (req, res) => { "Password does not meet complexity requirements. It must be at least 10 characters long and include at least one uppercase letter, one lowercase letter, one digit, and one symbol.", }); } - try { // Find the user in the database const user = await User.findOne({ where: { username: sanitizedUsername } }); - if (!user) { return res.status(404).json({ error: "User does not exist" }); } - // Generate a random salt and hash the new password const saltRounds = 10; const hashedPassword = await bcrypt.hash(sanitizedPassword, saltRounds); - // Update user's password await User.update( { password: hashedPassword }, { where: { username: sanitizedUsername } } ); - // Log password reset activity await userLogs.create({ username: creatorUsername, activity: `Password has been reset for ${sanitizedUsername}`, }); - // Password update successful return res.status(200).json({ success: "Password updated successfully" }); } catch (error) { @@ -679,10 +565,8 @@ app.post("/reset-password", async (req, res) => { app.get('/searchUser', async (req, res) => { const { username } = req.query; - // Sanitize the input const sanitizedUsername = validator.escape(username); - try { // Find the user in the database const user = await User.findOne({ where: { username: sanitizedUsername } }); @@ -690,11 +574,7 @@ app.get('/searchUser', async (req, res) => { if (!user) { // No user found with the given username res.status(404).json({ success: false, error: 'User not found' }); - } else { - // User found, return user data - res.json(user); - - } + } else {res.json(user)} } catch (error) { console.error('Sequelize query error:', error); res.status(500).json({ success: false, error: 'Internal Server Error' }); @@ -705,7 +585,6 @@ app.get('/api/users', async (req, res) => { try { // Find all users in the database const users = await User.findAll(); - // Return the users in the response res.json(users); } catch (error) { @@ -773,9 +652,6 @@ app.delete('/api/deleteUser/:username', async (req, res) => { res.status(500).json({ success: false, error: 'Internal Server Error', details: error.message }); } }); - - - app.get('/api/getLogs', async (req, res) => { try { @@ -814,11 +690,6 @@ app.get("/locations", isAuthenticated, async (req, res) => { } }); - const locationValidation = [ - body('name').trim().isLength({ min: 1 }).withMessage('Name must not be empty').escape(), - body('added_by').trim().isLength({ min: 1 }).withMessage('Added by must not be empty').escape(), - body('description').trim().escape(), - ]; app.post('/location/new', locationValidation, async (req, res) => { try { const errors = validationResult(req); @@ -846,12 +717,6 @@ app.get("/locations", isAuthenticated, async (req, res) => { } }); - const locationValidationUpdate = [ - body('id').trim().escape(), - body('name').trim().isLength({ min: 1 }).withMessage('Name must not be empty').escape(), - body('added_by').trim().isLength({ min: 1 }).withMessage('Added by must not be empty').escape(), - body('description').trim().escape(), - ]; app.post('/location/update', locationValidationUpdate, async (req, res) => { try { const errors = validationResult(req); @@ -879,6 +744,33 @@ app.get("/locations", isAuthenticated, async (req, res) => { } }); + + app.post('location/delete',locationdeleteValidation, async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + const sessionTokencookie = req.cookies['sessionToken']; + const user = await User.findOne({ where: { sessionid: sessionTokencookie } }); + if (!user) { + return res.status(403).json({ error: 'Invalid sessionToken' }); + } + const submittedCSRFToken = req.body.csrf_token; + if (!csrfTokenSession || submittedCSRFToken !== csrfTokenSession) { + return res.status(403).json({ error: 'CSRF token mismatch' }); + } + const {id} = req.body; + const preparedData = {id}; + // Make a POST request with the sanitized data using Axios + const axiosResponse = await axios.post(process.env.API_DELLOCATION, preparedData); + // Send the Axios response back to the client + res.status(axiosResponse.status).json(axiosResponse.data); + } catch (error) { + console.error('Error handling new sensor submission:', error); + res.status(500).json({ message: 'Internal Server Error' }); + } + }); app.get("/sensors", isAuthenticated, async (req, res) => { try { @@ -894,21 +786,34 @@ app.get("/sensors", isAuthenticated, async (req, res) => { } }); -const sensorValidation = [ - body('id').trim().escape(), - body('sensorname').trim().isLength({ min: 1 }).withMessage('Sensor Name must not be empty').escape(), - body('added_by').trim().isLength({ min: 1 }).withMessage('Added by must not be empty').escape(), - body('macAddress').custom(value => { - const macAddressRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/; - if (!macAddressRegex.test(value)) { - throw new Error('Invalid MAC address format'); - } - return true; - }).withMessage('Invalid MAC address format').escape(), - body('description').trim().escape(), - body('location').trim().escape() - ]; app.post('sensor/new',sensorValidation, async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + const sessionTokencookie = req.cookies['sessionToken']; + const user = await User.findOne({ where: { sessionid: sessionTokencookie } }); + if (!user) { + return res.status(403).json({ error: 'Invalid sessionToken' }); + } + const submittedCSRFToken = req.body.csrf_token; + if (!csrfTokenSession || submittedCSRFToken !== csrfTokenSession) { + return res.status(403).json({ error: 'CSRF token mismatch' }); + } + const { sensorname, added_by, macAddress, description, location} = req.body; + const preparedData = {sensorname, added_by, macAddress, description, location}; + // Make a POST request with the sanitized data using Axios + const axiosResponse = await axios.post(process.env.API_NEWSENSOR, preparedData); + // Send the Axios response back to the client + res.status(axiosResponse.status).json(axiosResponse.data); + } catch (error) { + console.error('Error handling new sensor submission:', error); + res.status(500).json({ message: 'Internal Server Error' }); + } + }); + + app.post('sensor/update',sensorupdateValidation, async (req, res) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { @@ -935,6 +840,33 @@ const sensorValidation = [ } }); + app.post('sensor/delete',sensordeleteValidation, async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + const sessionTokencookie = req.cookies['sessionToken']; + const user = await User.findOne({ where: { sessionid: sessionTokencookie } }); + if (!user) { + return res.status(403).json({ error: 'Invalid sessionToken' }); + } + const submittedCSRFToken = req.body.csrf_token; + if (!csrfTokenSession || submittedCSRFToken !== csrfTokenSession) { + return res.status(403).json({ error: 'CSRF token mismatch' }); + } + const {id} = req.body; + const preparedData = {id}; + // Make a POST request with the sanitized data using Axios + const axiosResponse = await axios.post(process.env.API_DELSENSOR, preparedData); + // Send the Axios response back to the client + res.status(axiosResponse.status).json(axiosResponse.data); + } catch (error) { + console.error('Error handling new sensor submission:', error); + res.status(500).json({ message: 'Internal Server Error' }); + } + }); + app.use(express.static("views")); app.listen(PORT, () => { diff --git a/Sean/views/location.js b/Sean/views/location.js index adcffc2..f31ae8a 100644 --- a/Sean/views/location.js +++ b/Sean/views/location.js @@ -3,19 +3,26 @@ $(document).ready(function () { $('#locationContainer').show(); $('#createLocationForm').hide(); $('#updateLocationForm').hide(); + $('#deleteLocationForm').hide(); }); $('#addLocationLink').on('click', function () { $('#locationContainer').hide(); $('#createLocationForm').show(); $('#updateLocationForm').hide(); + $('#deleteLocationForm').hide(); }); $('#updateLocationLink').on('click', function () { $('#locationContainer').hide(); $('#createLocationForm').hide(); $('#updateLocationForm').show(); - populateLocationDropdown(); + $('#deleteLocationForm').hide(); }); - + $('#deleteLocationLink').on('click', function () { + $('#locationContainer').hide(); + $('#createLocationForm').hide(); + $('#updateLocationForm').show(); + $('#deleteLocationForm').show(); + }); }); let locationArray = []; @@ -42,8 +49,7 @@ $(document).ready(function () { }); } populateTableAndArray(locationsData); - console.log(locationArray); - + populateLocationDropdown(); $('#locationForm').on('submit', function (e) { e.preventDefault(); @@ -136,3 +142,39 @@ $('#locationForm').on('submit', function (e) { // Handle error as needed }); }); + + $('#deleteForm').on('submit', function (e) { + e.preventDefault(); + const selectedLocationId = $('#locationDropdown').val(); + const csrf_token = $('#userForm input[name="csrf_token"]').val(); + + fetch('/location/delete', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + id:selectedLocationId, + csrf_token: csrf_token + }), + }) + .then(response => { + if (response.ok) { + // Status 201 indicates successful creation + return response.json(); + } else { + return response.json().then(data => { + throw new Error(data.message || `HTTP error! Status: ${response.status}`); + }); + } +}) +.then(data => { + console.log(`Location deleted successfully. Message: ${data.message}`); + alert('Location deleted successfully!'); + resetFormFields(); +}) +.catch(error => { + console.error('Location not deleted successfully', error); + // Handle error as needed +}); + }); \ No newline at end of file diff --git a/Sean/views/locations.ejs b/Sean/views/locations.ejs index b0aaa5e..5608e91 100644 --- a/Sean/views/locations.ejs +++ b/Sean/views/locations.ejs @@ -62,9 +62,9 @@ + + + diff --git a/package-lock.json b/package-lock.json index baea8ef..f891995 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1626,6 +1626,11 @@ "formdata-polyfill": "^4.0.10" } }, + "nodemailer": { + "version": "6.9.8", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.8.tgz", + "integrity": "sha512-cfrYUk16e67Ks051i4CntM9kshRYei1/o/Gi8K1d+R34OIs21xdFnW7Pt7EucmVKA0LKtqUGNcjMZ7ehjl49mQ==" + }, "nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",