mp/Sean/server.js
2024-01-18 18:34:09 +08:00

774 lines
24 KiB
JavaScript

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}`);
});