From 994aebf71d357b26168b2b8e825b117013ac7282 Mon Sep 17 00:00:00 2001
From: BIG2EYEZ
Date: Sun, 21 Jan 2024 16:07:27 +0800
Subject: [PATCH] CSP WIP add sessionid as well
---
Sean/inusers.js | 136 ---------------------------
Sean/models/User.js | 3 +
Sean/server.js | 72 +++++++++++++--
Sean/views/LOGO.PNG | Bin 10490 -> 0 bytes
Sean/views/forgot-password.css | 58 ++++++++++++
Sean/views/forgot-password.ejs | 61 +------------
Sean/views/home.js | 107 ----------------------
Sean/views/index.js | 64 -------------
Sean/views/inusers.ejs | 22 +++--
Sean/views/inusers.js | 8 +-
Sean/views/login.css | 67 ++++++++++++++
Sean/views/login.ejs | 70 +-------------
Sean/views/loginstyle.css | 146 -----------------------------
Sean/views/otp.css | 50 ++++++++++
Sean/views/otp.ejs | 53 +----------
Sean/views/responsive.css | 162 ---------------------------------
Sean/views/setup-mfa.ejs | 10 --
package-lock.json | 5 +
18 files changed, 264 insertions(+), 830 deletions(-)
delete mode 100644 Sean/inusers.js
delete mode 100644 Sean/views/LOGO.PNG
create mode 100644 Sean/views/forgot-password.css
delete mode 100644 Sean/views/home.js
delete mode 100644 Sean/views/index.js
create mode 100644 Sean/views/login.css
delete mode 100644 Sean/views/loginstyle.css
create mode 100644 Sean/views/otp.css
delete mode 100644 Sean/views/responsive.css
delete mode 100644 Sean/views/setup-mfa.ejs
diff --git a/Sean/inusers.js b/Sean/inusers.js
deleted file mode 100644
index e29bcd2..0000000
--- a/Sean/inusers.js
+++ /dev/null
@@ -1,136 +0,0 @@
-const allUsers = <%- JSON.stringify(allUsers) %>;
-
-document.getElementById('downloadButton').addEventListener('click', function () {
- console.log('Download button clicked');
- downloadExcel(allUsers);
-});
-
-document.getElementById('addUserLink').addEventListener('click', function () {
- document.getElementById('downloadButtonContainer').style.display = 'none';
- document.getElementById('userDataContainer').style.display = 'none';
- document.getElementById('createUserForm').style.display = 'block';
-});
-
-document.getElementById('userDataLink').addEventListener('click', function () {
- document.getElementById('downloadButtonContainer').style.display = 'block';
- document.getElementById('userDataContainer').style.display = 'block';
- document.getElementById('createUserForm').style.display = 'none';
-});
-
-document.getElementById('userForm').addEventListener('submit', function (event) {
- event.preventDefault();
-
- // Use FormData directly
- const formData = new FormData(document.getElementById('userForm'));
-
- // Check password complexity
- const password = formData.get('password');
- const confirmPassword = formData.get('confirmPassword');
-
- if (!isStrongPassword(password)) {
- alert('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.');
- return;
- }
-
- // Check if passwords match
- if (password !== confirmPassword) {
- alert('Passwords do not match. Please enter the same password in both fields.');
- return;
- }
-
- // Make a fetch request
- fetch('/createUser', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- name: formData.get('name'),
- username: formData.get('username'),
- email: formData.get('email'),
- password: password, // Use the validated password
- jobTitle: formData.get('jobTitle'),
- }),
- })
- .then(response => {
- if (!response.ok) {
- throw new Error(`HTTP error! Status: ${response.status}`);
- }
- return response.json();
- })
- .then(data => {
- console.log('Success:', data);
-
- // Show an alert with the received data
- alert(`User Registered!`);
-
- // Optionally, you can clear the form or take other actions after registration
- document.getElementById('userForm').reset();
- })
- .catch(error => {
- console.error('Fetch Error:', error);
- });
- });
-
- // Function to validate password complexity
- 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;
- }
-
-
- function downloadExcel(allUsers) {
- if (allUsers && allUsers.length > 0) {
- const workbook = new ExcelJS.Workbook();
- const worksheet = workbook.addWorksheet('All Users');
- const headers = ['Name', 'Username', 'Email', 'Password', 'Last Login', 'Job Title'];
- worksheet.addRow(headers);
- allUsers.forEach(user => {
- const rowData = [
- user.name || '',
- user.username || '',
- user.email || '',
- user.password || '',
- user.lastLogin ? new Date(user.lastLogin).toLocaleString('en-US', { timeZone: 'Asia/Singapore' }) : '',
- user.jobTitle || ''
- ];
- worksheet.addRow(rowData);
- });
- workbook.xlsx.writeBuffer().then(buffer => {
- const currentDate = new Date();
- const formattedDate = currentDate.toISOString().split('T')[0];
- const formattedTime = currentDate.toTimeString().split(' ')[0].replace(/:/g, '-');
- const fileName = `user_data_${formattedDate}_${formattedTime}.xlsx`;
- const blob = new Blob([buffer], {
- type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
- });
- saveAs(blob, fileName);
- });
- } else {
- console.error('No data available for download.');
- }
- }
diff --git a/Sean/models/User.js b/Sean/models/User.js
index 8cda275..d4c831b 100644
--- a/Sean/models/User.js
+++ b/Sean/models/User.js
@@ -37,6 +37,9 @@ module.exports = (sequelize) => {
reset_token_expiry: {
type: DataTypes.DATE,
},
+ sessionid: {
+ type: DataTypes.STRING,
+ },
}, {
hooks: {
beforeCreate: async (user) => {
diff --git a/Sean/server.js b/Sean/server.js
index ea3ead6..278a374 100644
--- a/Sean/server.js
+++ b/Sean/server.js
@@ -1,7 +1,7 @@
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");
@@ -10,21 +10,37 @@ const otpGenerator = require('otp-generator');
const { body, validationResult } = require('express-validator');
const validator = require('validator');
const { format } = require('date-fns');
-
+const helmet = require('helmet');
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 app = express();
+
+const nonce = crypto.randomBytes(16).toString('base64');
+
+console.log('Nonce:', nonce);
+
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
+app.use(cookieParser());
const PORT = process.env.PORT || 3000;
require("dotenv").config();
app.use(bodyParser.urlencoded({ extended: true }));
app.set("view engine", "ejs");
+app.use(
+ helmet.contentSecurityPolicy({
+ directives: {
+ defaultSrc: ["'self'",`'nonce-${nonce}'`],
+ scriptSrc: ["'self'",`'nonce-${nonce}'`,"'strict-dynamic'", 'cdn.jsdelivr.net', 'fonts.googleapis.com', 'stackpath.bootstrapcdn.com', 'code.jquery.com', 'cdnjs.cloudflare.com'],
+ styleSrc: ["'self'",`'nonce-${nonce}'`, 'cdn.jsdelivr.net', 'fonts.googleapis.com'],
+ imgSrc: ["'self'"],
+ fontSrc: ["'self'", 'fonts.gstatic.com'],
+ },
+ })
+ );
app.use(session({
secret: process.env.key,
@@ -189,7 +205,20 @@ app.post("/verify-otp", [
}
const sessionToken = crypto.randomBytes(32).toString('hex');
-
+ const username = req.body.username; // Replace with the actual username
+
+ User.update({ sessionid: sessionToken }, { where: { username } })
+ .then(([rowsUpdated]) => {
+ if (rowsUpdated > 0) {
+ console.log(`SessionId updated for user: ${username}`);
+ } else {
+ console.error('User not found.');
+ }
+ })
+ .catch(error => {
+ console.error('Error updating sessionId:', error);
+ });
+
req.session.authenticated = true;
req.session.username = req.body.username;
req.session.sessionToken = sessionToken;
@@ -200,7 +229,6 @@ app.post("/verify-otp", [
// 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}`);
@@ -232,6 +260,7 @@ app.post("/verify-otp", [
} else {
console.log("Session destroyed.");
// Log the logout activity using Sequelize
+ await User.update({ sessionid: null }, { where: { username } })
await userLogs.create({ username, activity: "User logged out. Session destroyed." });
// Clear the session token cookie
res.clearCookie('sessionToken');
@@ -267,7 +296,7 @@ app.post("/verify-otp", [
const currentUsername = req.session.username;
// Render the inusers page with JSON data
- res.render("inusers", { allUsers, csrfToken: csrfTokenSession, currentUsername });
+ res.render("inusers", { nonce: nonce, allUsers, csrfToken: csrfTokenSession, currentUsername });
} catch (error) {
console.error("Error fetching all users:", error);
res.status(500).send("Internal Server Error");
@@ -324,6 +353,14 @@ app.post(
return res.status(400).json({ errors: errors.array() });
}
+ 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' });
+ }
// Validate the anti-CSRF token
const submittedCSRFToken = req.body.csrf_token;
@@ -595,7 +632,14 @@ app.post("/reset-password", async (req, res) => {
if (!csrfTokenSession || submittedCSRFToken !== csrfTokenSession) {
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' });
+ }
// Sanitize the inputs
const sanitizedUsername = validator.escape(username);
const sanitizedPassword = validator.escape(password);
@@ -686,7 +730,6 @@ app.get('/api/users', async (req, res) => {
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 } });
@@ -709,13 +752,22 @@ 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
+ try {
+ // Retrieve sessionToken from cookies
+ const sessionTokencoookie = req.cookies['sessionToken'];
+ // Retrieve CSRF token from the request body
const { csrfToken } = req.body;
-
+ console.log(csrfToken);
// Compare CSRF token with the one stored in the session
if (csrfToken !== csrfTokenSession) {
return res.status(403).json({ success: false, error: 'CSRF token mismatch' });
+ }
+
+ // Verify sessionToken with the one stored in the database
+ const user = await User.findOne({ where: { sessionid: sessionTokencoookie } });
+
+ if (!user) {
+ return res.status(403).json({ success: false, error: 'Invalid sessionToken or user not found' });
}
// Log deletion activity to UserLogs model
diff --git a/Sean/views/LOGO.PNG b/Sean/views/LOGO.PNG
deleted file mode 100644
index eeecb45a91bbe17407338a081413c8f28dd71f09..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 10490
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
diff --git a/Sean/views/forgot-password.css b/Sean/views/forgot-password.css
new file mode 100644
index 0000000..12183d1
--- /dev/null
+++ b/Sean/views/forgot-password.css
@@ -0,0 +1,58 @@
+body {
+ font-family: 'Arial', sans-serif;
+ background-color: #f4f4f4;
+ margin: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100vh;
+ }
+
+ h1 {
+ text-align: center;
+ color: #333;
+ }
+
+ form {
+ background-color: #fff;
+ padding: 20px;
+ border-radius: 8px;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ width: 300px;
+ }
+
+ label {
+ display: block;
+ margin-bottom: 8px;
+ color: #555;
+ }
+
+ input {
+ width: 100%;
+ padding: 8px;
+ margin-bottom: 16px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ }
+
+ button {
+ background-color: #4caf50;
+ color: #fff;
+ padding: 10px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ width: 100%;
+ }
+
+ button:hover {
+ background-color: #45a049;
+ }
+ .error-message {
+ color: red;
+ margin-top: 10px;
+ }
+ .success-message {
+ color: green;
+ margin-top: 10px;
+ }
\ No newline at end of file
diff --git a/Sean/views/forgot-password.ejs b/Sean/views/forgot-password.ejs
index af82f99..b13f3bb 100644
--- a/Sean/views/forgot-password.ejs
+++ b/Sean/views/forgot-password.ejs
@@ -5,66 +5,7 @@
Forgot Password - Your Website
-
+
diff --git a/Sean/views/home.js b/Sean/views/home.js
deleted file mode 100644
index 883f205..0000000
--- a/Sean/views/home.js
+++ /dev/null
@@ -1,107 +0,0 @@
-document.addEventListener("DOMContentLoaded", async function () {
- console.log("DOM Loaded");
-
- // Extract data from sensorData
- const sensorData = JSON.parse('<%- JSON.stringify(sensorData) %>');
- console.log("Sensor Data:", sensorData);
-
- // Fetch location names from the server
- const locationNames = await fetch('/api/locations') // Adjust the API endpoint
- .then(response => response.json())
- .then(data => data.map(location => ({ id: location.id, name: location.name })))
- .catch(error => console.error('Error fetching location names:', error));
-
-
-
- // Group sensorData by locationid
- const groupedData = groupBy(sensorData, 'locationid');
-
- // Get the content div
- const contentDiv = document.getElementById('content');
-
- // Create a chart for each location
- Object.keys(groupedData).forEach(locationId => {
- const locationData = groupedData[locationId];
-
- // Find the corresponding location name
- const locationName = locationNames.find(location => location.id === parseInt(locationId, 10))?.name || `Unknown Location ${locationId}`;
-
- // Create a container for the chart
- const container = document.createElement('div');
- container.className = 'chart-container';
-
- // Create a title for the container with location name
- const title = document.createElement('h4');
- title.textContent = `Location: ${locationName}`;
- container.appendChild(title);
-
- // Get labels (Location IDs)
- const labels = locationData.map(data => new Date(data.createdAt).toLocaleString('en-US', { timeZone: 'Asia/Singapore' }));
-
- // Create datasets for each measurement
- const datasets = [
- {
- label: 'CO',
- data: locationData.map(data => data.measurement.co),
- backgroundColor: 'rgba(255, 99, 132, 0.5)', // Red color
- },
- {
- label: 'O3',
- data: locationData.map(data => data.measurement.o3),
- backgroundColor: 'rgba(54, 162, 235, 0.5)', // Blue color
- },
- {
- label: 'NO2',
- data: locationData.map(data => data.measurement.no2),
- backgroundColor: 'rgba(255, 206, 86, 0.5)', // Yellow color
- },
- {
- label: 'SO2',
- data: locationData.map(data => data.measurement.so2),
- backgroundColor: 'rgba(75, 192, 192, 0.5)', // Green color
- },
- ];
-
- // Create a canvas element for each location
- const canvas = document.createElement('canvas');
- canvas.width = 400;
- canvas.height = 200;
-
- // Append canvas to the container
- container.appendChild(canvas);
-
- // Append container to the content div
- contentDiv.appendChild(container);
-
- // Create a bar chart for each location
- const ctx = canvas.getContext('2d');
- new Chart(ctx, {
- type: 'bar',
- data: {
- labels: labels,
- datasets: datasets,
- },
- options: {
- scales: {
- x: {
- beginAtZero: true,
- },
- y: {
- beginAtZero: true,
- },
- },
- },
- });
- });
-
- // Helper function to group data by a specified key
- function groupBy(arr, key) {
- return arr.reduce((acc, obj) => {
- const groupKey = obj[key];
- acc[groupKey] = acc[groupKey] || [];
- acc[groupKey].push(obj);
- return acc;
- }, {});
- }
- });
-
\ No newline at end of file
diff --git a/Sean/views/index.js b/Sean/views/index.js
deleted file mode 100644
index 5ee6953..0000000
--- a/Sean/views/index.js
+++ /dev/null
@@ -1,64 +0,0 @@
-const express = require('express');
-const router = express.Router();
-const mysql = require('mysql');
-
-// Replace with your MySQL connection details
-const mysqlConfig = {
- host: process.env.host,
- user: process.env.user,
- password: process.env.password,
- database: process.env.database,
- timezone: 'Z', // Set the timezone to UTC
-};
-
-const mysqlConnection = mysql.createConnection(mysqlConfig);
-
-// Middleware to check if the user is authenticated
-function isAuthenticated(req, res, next) {
- if (req.session && req.session.authenticated) {
- return next();
- } else {
- res.redirect('/login');
- }
-}
-
-// InUsers route (renders the InUsers tab)
-router.get('/', isAuthenticated, (req, res) => {
- // Fetch all user data from the database
- const userDataQuery = 'SELECT * FROM users';
-
- mysqlConnection.query(userDataQuery, (error, userData) => {
- if (error) {
- console.error('Error fetching user data:', error);
- res.status(500).send('Internal Server Error');
- return;
- }
-
- // Render the inusers page with user data
- res.render('inusers', { userData: userData });
- });
-});
-
-// User Data route
-router.get('/userdata', isAuthenticated, (req, res) => {
- // Fetch all user data from the database
- const userDataQuery = 'SELECT * FROM users';
-
- mysqlConnection.query(userDataQuery, (error, userData) => {
- if (error) {
- console.error('Error fetching user data:', error);
- res.status(500).send('Internal Server Error');
- return;
- }
-
- // Render the user-data page with user data
- res.render('user-data', { userData: userData });
- });
-});
-
-// Edit User Data route
-router.get('/edituserdata', isAuthenticated, (req, res) => {
- res.render('edit-user-data');
-});
-
-module.exports = router;
diff --git a/Sean/views/inusers.ejs b/Sean/views/inusers.ejs
index 10ade93..1975b70 100644
--- a/Sean/views/inusers.ejs
+++ b/Sean/views/inusers.ejs
@@ -7,7 +7,7 @@
In-House Users
-
+
@@ -173,19 +173,21 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/Sean/views/loginstyle.css b/Sean/views/loginstyle.css
deleted file mode 100644
index ce9377d..0000000
--- a/Sean/views/loginstyle.css
+++ /dev/null
@@ -1,146 +0,0 @@
-@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;500;600;700&display=swap');
-*{
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- font-family: 'Poppins',sans-serif;
-}
-body{
- height: 100vh;
- display: flex;
- justify-content: center;
- align-items: center;
- padding: 10px;
- background: linear-gradient(135deg, #71b7e6, #9b59b6);
-}
-.container{
- max-width: 700px;
- width: 100%;
- background-color: #fff;
- padding: 25px 30px;
- border-radius: 5px;
- box-shadow: 0 5px 10px rgba(0,0,0,0.15);
-}
-.container .title{
- font-size: 25px;
- font-weight: 500;
- position: relative;
-}
-.container .title::before{
- content: "";
- position: absolute;
- left: 0;
- bottom: 0;
- height: 3px;
- width: 30px;
- border-radius: 5px;
- background: linear-gradient(135deg, #71b7e6, #9b59b6);
-}
-.content form .user-details{
- display: flex;
- flex-wrap: wrap;
- justify-content: space-between;
- margin: 20px 0 12px 0;
-}
-form .user-details .input-box{
- margin-bottom: 15px;
- width: calc(100% / 2 - 20px);
-}
-form .input-box span.details{
- display: block;
- font-weight: 500;
- margin-bottom: 5px;
-}
-.user-details .input-box input{
- height: 45px;
- width: 100%;
- outline: none;
- font-size: 16px;
- border-radius: 5px;
- padding-left: 15px;
- border: 1px solid #ccc;
- border-bottom-width: 2px;
- transition: all 0.3s ease;
-}
-.user-details .input-box input:focus,
-.user-details .input-box input:valid{
- border-color: #9b59b6;
-}
- form .gender-details .gender-title{
- font-size: 20px;
- font-weight: 500;
- }
- form .category{
- display: flex;
- width: 80%;
- margin: 14px 0 ;
- justify-content: space-between;
- }
- form .category label{
- display: flex;
- align-items: center;
- cursor: pointer;
- }
- form .category label .dot{
- height: 18px;
- width: 18px;
- border-radius: 50%;
- margin-right: 10px;
- background: #d9d9d9;
- border: 5px solid transparent;
- transition: all 0.3s ease;
-}
- #dot-1:checked ~ .category label .one,
- #dot-2:checked ~ .category label .two,
- #dot-3:checked ~ .category label .three{
- background: #9b59b6;
- border-color: #d9d9d9;
- }
- form input[type="radio"]{
- display: none;
- }
- form .button{
- height: 45px;
- margin: 35px 0
- }
- form .button input{
- height: 100%;
- width: 100%;
- border-radius: 5px;
- border: none;
- color: #fff;
- font-size: 18px;
- font-weight: 500;
- letter-spacing: 1px;
- cursor: pointer;
- transition: all 0.3s ease;
- background: linear-gradient(135deg, #71b7e6, #9b59b6);
- }
- form .button input:hover{
- /* transform: scale(0.99); */
- background: linear-gradient(-135deg, #71b7e6, #9b59b6);
- }
- @media(max-width: 584px){
- .container{
- max-width: 100%;
-}
-form .user-details .input-box{
- margin-bottom: 15px;
- width: 100%;
- }
- form .category{
- width: 100%;
- }
- .content form .user-details{
- max-height: 300px;
- overflow-y: scroll;
- }
- .user-details::-webkit-scrollbar{
- width: 5px;
- }
- }
- @media(max-width: 459px){
- .container .content .category{
- flex-direction: column;
- }
-}
\ No newline at end of file
diff --git a/Sean/views/otp.css b/Sean/views/otp.css
new file mode 100644
index 0000000..bfa99d8
--- /dev/null
+++ b/Sean/views/otp.css
@@ -0,0 +1,50 @@
+body {
+ font-family: Arial, sans-serif;
+ background-color: #f4f4f4;
+ text-align: center;
+ margin: 50px;
+}
+
+h2 {
+ color: #333;
+}
+
+form {
+ max-width: 300px;
+ margin: 20px auto;
+ padding: 20px;
+ background-color: #fff;
+ border-radius: 8px;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+}
+
+label {
+ display: block;
+ margin-bottom: 8px;
+ color: #333;
+}
+
+input {
+ width: 100%;
+ padding: 10px;
+ margin-bottom: 20px;
+ box-sizing: border-box;
+}
+
+button {
+ background-color: #4caf50;
+ color: #fff;
+ padding: 10px 15px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+button:hover {
+ background-color: #45a049;
+}
+
+.error {
+ color: red;
+ margin-top: 10px;
+}
\ No newline at end of file
diff --git a/Sean/views/otp.ejs b/Sean/views/otp.ejs
index 1966179..f9c5d0d 100644
--- a/Sean/views/otp.ejs
+++ b/Sean/views/otp.ejs
@@ -4,58 +4,7 @@
Enter OTP
-
+
Enter OTP
diff --git a/Sean/views/responsive.css b/Sean/views/responsive.css
deleted file mode 100644
index a16942e..0000000
--- a/Sean/views/responsive.css
+++ /dev/null
@@ -1,162 +0,0 @@
-/* Responsive CSS Here */
-@media screen and (max-width: 950px) {
- .nav-img {
- height: 25px;
- }
- .nav-option {
- gap: 30px;
- }
- .nav-option h3 {
- font-size: 15px;
- }
- .report-topic-heading,
- .item1,
- .items {
- width: 800px;
- }
- }
-
- @media screen and (max-width: 850px) {
- .nav-img {
- height: 30px;
- }
- .nav-option {
- gap: 30px;
- }
- .nav-option h3 {
- font-size: 20px;
- }
- .report-topic-heading,
- .item1,
- .items {
- width: 700px;
- }
- .navcontainer {
- width: 100vw;
- position: absolute;
- transition: all 0.6s ease-in-out;
- top: 0;
- left: -100vw;
- }
- .nav {
- width: 100%;
- position: absolute;
- }
- .navclose {
- left: 00px;
- }
- .searchbar {
- display: none;
- }
- .main {
- padding: 40px 30px 30px 30px;
- }
- .searchbar2 {
- width: 100%;
- display: flex;
- margin: 0 0 40px 0;
- justify-content: center;
- }
- .searchbar2 input {
- width: 250px;
- height: 42px;
- border-radius: 50px 0 0 50px;
- background-color: var(--background-color3);
- padding: 0 20px;
- font-size: 15px;
- border: 2px solid var(--secondary-color);
- }
- }
-
- @media screen and (max-width: 490px) {
- .message {
- display: none;
- }
- .logosec {
- width: 100%;
- justify-content: space-between;
- }
- .logo {
- font-size: 20px;
- }
- .menuicn {
- height: 25px;
- }
- .nav-img {
- height: 25px;
- }
- .nav-option {
- gap: 25px;
- }
- .nav-option h3 {
- font-size: 12px;
- }
- .nav-upper-options {
- gap: 15px;
- }
- .recent-Articles {
- font-size: 20px;
- }
- .report-topic-heading,
- .item1,
- .items {
- width: 550px;
- }
- }
-
- @media screen and (max-width: 400px) {
- .recent-Articles {
- font-size: 17px;
- }
- .view {
- width: 60px;
- font-size: 10px;
- height: 27px;
- }
- .report-header {
- height: 60px;
- padding: 10px 10px 5px 10px;
- }
- .searchbtn img {
- height: 20px;
- }
- }
-
- @media screen and (max-width: 320px) {
- .recent-Articles {
- font-size: 12px;
- }
- .view {
- width: 50px;
- font-size: 8px;
- height: 27px;
- }
- .report-header {
- height: 60px;
- padding: 10px 5px 5px 5px;
- }
- .t-op {
- font-size: 12px;
- }
- .t-op-nextlvl {
- font-size: 10px;
- }
- .report-topic-heading,
- .item1,
- .items {
- width: 300px;
- }
- .report-body {
- padding: 10px;
- }
- .label-tag {
- width: 70px;
- }
- .searchbtn {
- width: 40px;
- }
- .searchbar2 input {
- width: 180px;
- }
- }
-
\ No newline at end of file
diff --git a/Sean/views/setup-mfa.ejs b/Sean/views/setup-mfa.ejs
deleted file mode 100644
index a0ddd23..0000000
--- a/Sean/views/setup-mfa.ejs
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
Setup Multi-Factor Authentication
-
Scan the QR code below with your authenticator app:
-

-
-
diff --git a/package-lock.json b/package-lock.json
index 08346ff..8bf6c65 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1156,6 +1156,11 @@
"function-bind": "^1.1.2"
}
},
+ "helmet": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.1.0.tgz",
+ "integrity": "sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg=="
+ },
"htmlparser2": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",