const express = require("express"); const session = require("express-session"); const rateLimit = require('express-rate-limit'); const mysql2 = require('mysql2'); 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 { format } = require('date-fns'); const { Sequelize } = require('sequelize'); const { transporter } = require("./modules/nodeMailer"); const { connection } = require("./modules/mysql"); const { sequelize, User } = require("./modules/mysql"); const userLogs= require('./models/userLogs')(sequelize); // Adjust the path based on your project structure const app = express(); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); const PORT = process.env.PORT || 3000; require("dotenv").config(); app.use(bodyParser.urlencoded({ extended: true })); app.set("view engine", "ejs"); app.use(session({ secret: process.env.key, resave: false, saveUninitialized: true, cookie: { secure: false, // Make sure to set this to true in a production environment with HTTPS httpOnly: true, maxAge: 24 * 60 * 60 * 1000, // Session duration in milliseconds (here set to 1 day) }, })); function isAuthenticated(req, res, next) { if (req.session && req.session.authenticated) { return next(); } else { 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.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); 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; req.session.otpExpiration = expirationTime; req.session.save(); try { await sendOTPByEmail(user.email, otp); await userLogs.create({ username: username, activity: "OTP successfully sent to user", }); } catch (sendOTPError) { await userLogs.create({ username: username, activity: "OTP failed to send to user", }); console.error("Error sending OTP:", sendOTPError); return res.status(500).send("Internal Server Error"); } res.render("otp", { error: null, username: user.username, csrfToken: req.session.csrfToken }); } else { await userLogs.create({ username, success: false, activity: "Incorrect password" }); res.render("login", { error: "Invalid username or password", csrfToken: req.session.csrfToken }); } } else { await userLogs.create({ username: username, activity: "User not found", }); res.render("login", { error: "Invalid username or password", csrfToken: req.session.csrfToken }); } } catch (error) { console.error("Error in login route:", error); res.status(500).send("Internal Server Error"); } }); // OTP verification route app.post("/verify-otp", [ body('otp').escape().trim().isLength({ min: 1 }).withMessage('OTP must not be empty'), ], async (req, res) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { return res.render('otp', { error: 'Invalid OTP. Please try again.', username: req.body.username, csrfToken: req.session.csrfToken }); } const enteredOTP = req.body.otp; if (!req.session) { console.error("Session is not defined."); return res.status(500).send("Internal Server Error"); } const user = await User.findOne({ where: { username: req.body.username } }); if (!user) { console.error("User not found."); return res.status(500).send("Internal Server Error"); } if (enteredOTP === req.session.otp) { if (req.body.username) { await userLogs.create({ username: req.body.username, activity: "OTP entered correctly" }); } const sessionToken = crypto.randomBytes(32).toString('hex'); 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}`); // Set CSRF token as a cookie 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) { await userLogs.create({ username: req.body.username, activity: "Incorrect OTP entered" }); } res.render("login", { error: "Incorrect OTP. Please try again."}); } } catch (error) { console.error("Error in OTP verification route:", error); res.status(500).send("Internal Server Error"); } }); app.get("/logout", async (req, res) => { try { const username = req.session.username || "Unknown User"; // Log the logout activity using Sequelize await userLogs.create({ username, activity: "User logged out. Session destroyed." }); // Log the user out by clearing the session req.session.destroy(async (err) => { if (err) { console.error("Error destroying session:", err); // Log the logout activity using Sequelize await userLogs.create({ username, activity: "User logged out unsuccessfully. Session not destroyed." }); } else { console.log("Session destroyed."); // Clear the session token cookie res.clearCookie('sessionToken'); } // Redirect to the login page after logout res.redirect("/login"); }); } catch (error) { console.error("Error in logout route:", error); res.status(500).send("Internal Server Error"); } }); app.get("/home", isAuthenticated, (req, res) => { // Render the home page with sensor data res.render("home", { username: req.session.username, }); }); app.get("/inusers", isAuthenticated, async (req, res) => { try { // Fetch all user data from the database using Sequelize const allUsers = await User.findAll({ attributes: ['name', 'username', 'email', 'jobTitle'], }); const currentUsername = req.session.username; // Render the inusers page with JSON data res.render("inusers", { allUsers, csrfToken: csrfTokenSession, currentUsername }); } catch (error) { console.error("Error fetching all users:", error); res.status(500).send("Internal Server Error"); } }); function isStrongPassword(password) { // Password must be at least 10 characters long if (password.length < 10) { return false; } // Password must contain at least one uppercase letter if (!/[A-Z]/.test(password)) { return false; } // Password must contain at least one lowercase letter if (!/[a-z]/.test(password)) { return false; } // Password must contain at least one digit if (!/\d/.test(password)) { return false; } // Password must contain at least one symbol if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { return false; } 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) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } // Validate the anti-CSRF token const submittedCSRFToken = req.body.csrf_token; if (!csrfTokenSession || submittedCSRFToken !== csrfTokenSession) { return res.status(403).json({ error: 'CSRF token mismatch' }); } // Extract user input const { name, username, email, password, jobTitle } = req.body; console.log(submittedCSRFToken); // Extract the username of the user creating a new user const creatorUsername = req.session.username; // Adjust this based on how you store the creator's username in your session // Additional password complexity check if (!isStrongPassword(password)) { return res.status(400).json({ error: "Password does not meet complexity requirements" }); } // Check if the username is already taken const existingUser = await User.findOne({ where: { username } }); if (existingUser) { // Log unsuccessful user creation due to username taken await userLogs.create({ username: creatorUsername, activity: "username taken" }); return res.status(400).json({ error: "Username is already taken", message: "Username is already taken. Please choose a different username." }); } // Check if the email is already taken const existingEmailUser = await User.findOne({ where: { email } }); if (existingEmailUser) { // Log unsuccessful user creation due to email taken await userLogs.create({ username: creatorUsername, activity: "email taken" }); return res.status(400).json({ error: "Email is already in use", message: "Email is already in use. Please choose another email." }); } // Hash the password const hashedPassword = await bcrypt.hash(password, 10); // Start a transaction const t = await sequelize.transaction(); try { // Create the user const newUser = await User.create({ name, username, email, password: hashedPassword, lastLogin: null, jobTitle, }, { transaction: t }); // Commit the transaction await t.commit(); // Log successful user creation await userLogs.create({ username: creatorUsername, activity: "user created successfully" }); return res.status(200).json({ message: "User created successfully" }); } catch (createUserError) { // Rollback the transaction in case of an error await t.rollback(); console.error("Error creating user:", createUserError); // Log unsuccessful user creation due to an error await userLogs.create({ username: creatorUsername, activity: "internal error" }); return res.status(500).json({ error: "Internal Server Error" }); } } catch (error) { console.error("Error creating user:", error); return res.status(500).json({ error: "Internal Server Error" }); } } ); app.get("/forgot-password", (req, res) => { res.render("forgot-password", { error: null, success: null }); }); app.post("/forgot-password", async (req, res) => { let user; // Declare the 'user' variable outside the try-catch block try { const { usernameOrEmail } = req.body; // Sanitize the input const sanitizedUsernameOrEmail = validator.escape(usernameOrEmail); // Find the user by username or email user = await User.findOne({ where: { [Sequelize.Op.or]: [ { username: sanitizedUsernameOrEmail }, { email: sanitizedUsernameOrEmail }, ], }, }); if (!user) { 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 }, } ); // Send email with reset link const resetLink = `http://localhost:3000/reset-password/${reset_token}`; const mailOptions = { to: user.email, 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, activity: "Password reset link sent successfully", }); } catch (error) { if (user) { await userLogs.create({ username: user.username, activity: "Password reset link unsuccessfully sent. Please check with the administrator.", }); } 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() }, }, }); if (!user) { // Pass the error to the template when rendering the reset-password page return res.render("reset-password", { token, 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 return res.render("reset-password", { token, 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: "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 hashedPassword = await bcrypt.hash(sanitizedPassword, 10); // Update user's password and clear reset token const updateQuery = { password: hashedPassword, reset_token: null, reset_token_expiry: null, }; const whereCondition = { reset_token: sanitizedToken, }; await User.update(updateQuery, { where: whereCondition, }); // Log password reset activity await userLogs.create({ username: user.username, activity: "Password reset successfully", }); // Redirect to the success page upon successful password reset res.redirect("/success"); } catch (error) { console.error("Error during password reset:", error); // Pass the error to the template when rendering the reset-password page res.render("reset-password", { token: req.params.token, resetError: "Error during password reset", }); } }); app.get("/success", (req, res) => { res.render("success"); }); app.get("/reset-password/:token", (req, res) => { const { token } = req.params; const error = req.query.error || null; // Get error from query parameter // Assuming you have this line in your server code where you render the reset-password view res.render("reset-password", { token, passwordValidationError: null, resetError: null, success: null, }); }); app.post("/reset-password", async (req, res) => { const { username, password, confirmPassword, csrf_token } = req.body; const creatorUsername = req.session.username; const submittedCSRFToken = req.body.csrf_token; if (!csrfTokenSession || submittedCSRFToken !== csrfTokenSession) { return res.status(403).json({ error: 'CSRF token mismatch' }); } // Sanitize the inputs 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({ error: "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) { console.error("Error updating password:", error); return res.status(500).json({ error: "Error updating password" }); } }); 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 } }); console.log(user); 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); } } catch (error) { console.error('Sequelize query error:', error); res.status(500).json({ success: false, error: 'Internal Server Error' }); } }); 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) { console.error('Sequelize query error:', error); res.status(500).json({ success: false, error: 'Internal Server Error' }); } }); app.get('/api/searchUser', async (req, res) => { const { username } = req.query; console.log(username); try { // Find the user in the database by username const user = await User.findOne({ where: { username } }); 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); } } catch (error) { console.error('Sequelize query error:', error); res.status(500).json({ success: false, error: 'Internal Server Error' }); } }); app.delete('/api/deleteUser/:username', async (req, res) => { const { username } = req.params; const creatorUsername = req.session.username; try { // Extract CSRF token from the request body const { csrfToken } = req.body; // Compare CSRF token with the one stored in the session if (csrfToken !== csrfTokenSession) { return res.status(403).json({ success: false, error: 'CSRF token mismatch' }); } // Log deletion activity to UserLogs model const deletionActivity = `User ${username} has been successfully deleted`; await userLogs.create({ username: creatorUsername, activity: deletionActivity }); // Perform user deletion using the User model const deletedUser = await User.destroy({ where: { username } }); if (!deletedUser) { res.status(404).json({ success: false, error: 'User not found' }); } else { res.json({ success: true, message: 'User deleted successfully' }); } } catch (error) { console.error('Sequelize query error:', error); res.status(500).json({ success: false, error: 'Internal Server Error', details: error.message }); } }); app.get('/api/getLogs', async (req, res) => { try { // Query the database to fetch logs using Sequelize model const logs = await userLogs.findAll({ attributes: ['id', 'username', 'activity', 'timestamp'], }); // Format timestamps to a more readable format with timezone conversion const formattedLogs = logs.map((log) => ({ id: log.id, username: log.username, activity: log.activity, timestamp: format(new Date(log.timestamp), 'yyyy-MM-dd HH:mm:ssXXX', { timeZone: 'Asia/Singapore' }), })); // Send the formatted logs as a JSON response res.json(formattedLogs); } catch (error) { console.error('Sequelize query error:', error); res.status(500).json({ error: 'Error fetching logs from Sequelize' }); } }); app.use(express.static("views")); app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); });