From 3b9703f2c27704f728f31b6be890bcf338b806d8 Mon Sep 17 00:00:00 2001 From: wyattmog <145526766+wyattmog@users.noreply.github.com> Date: Thu, 7 Nov 2024 00:55:57 -0600 Subject: [PATCH 1/2] Bug fixes --- .gitignore | 6 +- Backend/server.js | 199 ++++++++++++++++++++++++++++------------------ 2 files changed, 122 insertions(+), 83 deletions(-) diff --git a/.gitignore b/.gitignore index 95681c2..b555e78 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -.env -.env.development -.env.production -Backend/uploads/ +Backend/.env.development +Backend/.env.production Backend/node_modules/ diff --git a/Backend/server.js b/Backend/server.js index e7d92a2..dda1b74 100644 --- a/Backend/server.js +++ b/Backend/server.js @@ -1,15 +1,15 @@ const express = require('express') const multer = require('multer') const jsdom = require('jsdom') -const fs = require('fs') +const yauzl = require('yauzl'); const { JSDOM } = jsdom; +const https = require('https') +const fs = require('fs') const bcrypt = require('bcryptjs') const dotenv = require('dotenv') const jwt = require('jsonwebtoken') +const cors = require('cors') const cookieParser = require('cookie-parser') -<<<<<<< Updated upstream -dotenv.config() -======= const result = dotenv.config({ path: `.env.${process.env.NODE_ENV || 'development'}` }); @@ -18,50 +18,43 @@ if (result.error) { } const isProduction = process.env.NODE_ENV === 'production'; -const logoutCookieOptions = isProduction ? { - httpOnly: true, // Ensures the cookie is not accessible via JavaScript - secure: isProduction, // Ensures the cookie is only sent over HTTPS - sameSite: 'None', // Ensures the cookie is sent with cross-site requests - domain: '.instagram-tool.duckdns.org', // Make sure the domain has a dot in front - path: '/', // Ensure this matches the path where the cookie was set -} : { - httpOnly: true, - sameSite: 'Strict', - secure: isProduction, // Set secure cookies only in production -} const allowedOrigins = isProduction ? ['https://wyattmog.github.io'] : ['http://localhost:5500']; ->>>>>>> Stashed changes const mysql = require('mysql2') const app = express() const port = 8383 - - -app.use(express.static('/Users/wyattmogelson/Coding/InstagramTool/Frontend')) +// const host = process.env.HOST; app.use(express.json()) app.use(cookieParser()) - +// console.log(result) +app.use(cors({ + origin: allowedOrigins, // Adjust as needed + credentials: true +})) const storage = multer.memoryStorage({ storage: multer.memoryStorage() , - limits: { fileSize: 5 * 1024 * 1024 } + limits: { fileSize: 10 * 1024 * 1024 } }) // Use enviornment variables because it prevents hard coding // authentication, and allows user to change the value without -// coding it in javascript. Sensitive info like password also -// shouldn't be hardcoded. +// coding it in javascript. + + + // Now you can access your environment variables + const pool = mysql.createPool({ host: process.env.DB1_HOST, user: process.env.DB1_USER, password: process.env.DB1_PASSWORD, database: process.env.DB1_DATABASE + }).promise() const uploads = multer({storage: storage}) -// This communicates from backend to front end -// Will use to display information after processing in sql +// Get request that first authenticates user, then parses and sends the request information to client browser. app.get('/data', authenticateToken, async (req,res) => { const result = await parse(req.user) if (result[0].length === 0) { @@ -71,40 +64,81 @@ app.get('/data', authenticateToken, async (req,res) => { res.json(result) } }) +// Error handler for multer in order to catch and send information back to client browser app.use((err, req, res, next) => { if (err instanceof multer.MulterError) { // A Multer error occurred when uploading. - return res.status(400).send('An error occurred during the file upload.'); // Send a generic error message + return res.status(400).send('An error occurred during the file upload.'); } else if (err) { // An unknown error occurred when uploading. - return res.status(400).send('An error occurred during the request.'); // Send a generic error message + return res.status(400).send('An error occurred during the request.'); } next(); }); + +// Extracts html files from the zip file +// Puts target files in array to return +async function extractHtml(zipBuffer, targetFiles) { + return new Promise((resolve, reject) => { + const extractedFiles = []; + yauzl.fromBuffer(zipBuffer, { lazyEntries: true }, (err, zipfile) => { + if (err) return reject(err); + zipfile.readEntry(); + zipfile.on("entry", (entry) => { + if (targetFiles.includes(entry.fileName)) { + // Read the entry into memory + zipfile.openReadStream(entry, (err, readStream) => { + if (err) return reject(err); + + let buffers = []; + readStream.on('data', (chunk) => { + buffers.push(chunk); // Collect chunks of data + }); + readStream.on('end', () => { + const htmlBuffer = Buffer.concat(buffers); // Concatenate the collected buffers + extractedFiles.push({ + originalname: entry.fileName, + buffer: htmlBuffer // Store the HTML file contents in memory + }); + zipfile.readEntry(); // Read the next entry + }); + }); + } else { + zipfile.readEntry(); + } + }); + zipfile.on('end', () => { + resolve(extractedFiles); // Resolve the promise with the collected HTML files + }); + }); + }) +} // Recieves post request from frontend // Puts followers file in /followers directory // Puts following file in /following directory // Uses JSDOM to parse and query and get all hyperlink content // For both following and follower links, puts // both into respective arrays. -app.post('/uploads', authenticateToken, uploads.array('files'), async (req, res) => { - if (!req.files || req.files.length === 0) { - return res.status(500).send('No files uploaded or invalid file type.'); - } +app.post('/uploads', authenticateToken, uploads.single('zipfile'), async (req, res) => { + const zipBuffer = req.file.buffer; + const targetFiles = ['connections/followers_and_following/followers_1.html', 'connections/followers_and_following/following.html']; try { - const followersFile = req.files.find(file => (file.originalname === 'followers_1.html') || (file.originalname === 'followers.html')); - const followingFile = req.files.find(file => file.originalname === 'following.html'); + const htmlFiles = await extractHtml(zipBuffer, targetFiles); + // Checks if needed files were found in zip file + const followersFile = htmlFiles.find(file => (file.originalname === 'connections/followers_and_following/followers_1.html')); + const followingFile = htmlFiles.find(file => (file.originalname === 'connections/followers_and_following/following.html')); if (!followersFile || !followingFile) { return res.status(500).send('Required files are missing.'); } await deleteColumns(req.user) + // Promise that is used to inject user information information into sql database await new Promise( async (resolve, reject) => { try { const data = followersFile.buffer.toString("utf-8") const dom = new JSDOM(data); const links = dom.window.document.querySelectorAll("a"); for (let i = 0; i < links.length; i++) { - await insertFollowers(links[i].textContent, req.user) + await insertFollowers(links[i].textContent,links[i].href, req.user) } resolve() } @@ -112,13 +146,14 @@ app.post('/uploads', authenticateToken, uploads.array('files'), async (req, res) reject(err) } }) + // Promise that is used to inject user information information into sql database await new Promise( async (resolve, reject) => { try { const data = followingFile.buffer.toString("utf-8") const dom = new JSDOM(data); const links = dom.window.document.querySelectorAll("a"); for (let i = 0; i < links.length; i++) { - await insertFollowing(links[i].textContent, req.user) + await insertFollowing(links[i].textContent, links[i].href, req.user) } resolve() } @@ -130,29 +165,37 @@ app.post('/uploads', authenticateToken, uploads.array('files'), async (req, res) // const result = await parse() res.json(result) } + // Various error handling catch(err) { + console.log(err) return res.status(500).send('Upload error'); }}, (err, req, res, next) => { if (err instanceof multer.MulterError) { + console.log(err) // A Multer error occurred when uploading. - return res.status(500).send('An error occurred during the file upload.'); // Send a generic error message + return res.status(500).send('An error occurred during the file upload.'); } else if (err) { // An unknown error occurred when uploading. - return res.status(500).send('An error occurred during the request.'); // Send a generic error message + return res.status(500).send('An error occurred during the request.'); } else { // Handle other errors return res.status(500).json({ error: 'Internal server error' }); } - }// res.json({status: 'form data recieved' }) + } ) +// Handles user logout app.post('/logout', (req, res) => { -<<<<<<< Updated upstream - res.clearCookie('auth_token'); // Clear the auth_token cookie -======= - res.clearCookie('auth_token', logoutCookieOptions); ->>>>>>> Stashed changes + res.clearCookie('auth_token', { + httpOnly: true, // Ensures the cookie is not accessible via JavaScript + secure: isProduction, // Ensures the cookie is only sent over HTTPS + sameSite: 'None', // Ensures the cookie is sent with cross-site requests + domain: '.instagram-tool.duckdns.org', // Make sure the domain has a dot in front + path: '/', // Ensure this matches the path where the cookie was set + }); res.json({ message: 'Logout successful' }); }); +// Handles user registration +// Stores information in database table for easy and secure lookup app.post('/register', uploads.none(), async (req, res) => { try { const { username, password } = req.body; @@ -160,6 +203,7 @@ app.post('/register', uploads.none(), async (req, res) => { if (result[0].length > 0) { return res.status(400).send('Username already taken') } + // Use bcrypt in order to store hashed version of password for increased security const hashedPassword = await bcrypt.hash(password, 10) await pool.query('INSERT INTO users (username, password) VALUES (?, ?)', [username, hashedPassword]) res.json('User registered successfully') @@ -168,6 +212,7 @@ app.post('/register', uploads.none(), async (req, res) => { return res.status(500).send('Database error'); } }) +// Checks if user is authenticated app.get('/protected', authenticateToken, (req, res) => { res.sendStatus(200); }); @@ -176,13 +221,11 @@ app.post('/login', uploads.none(), async (req, res) => { const username = req.body.username; const password = req.body.password; const remember = req.body.remember; - // Find the user by username const result = await pool.query('SELECT * FROM users WHERE username = ?', [username]); if (result[0].length === 0) { return res.status(400).send('User not found'); } - // Compare passwords const user = result[0]; const isMatch = await bcrypt.compare(password, user[0].password); @@ -190,57 +233,38 @@ app.post('/login', uploads.none(), async (req, res) => { return res.status(400).send('Invalid password') } const accessToken = jwt.sign(username, process.env.ACCESS_TOKEN_SECRET) + // Sets expiration to 1 week and one hour respectively const maxAges = String(remember) == "true" ? 604800000 : 3600000; -<<<<<<< Updated upstream res.cookie('auth_token', accessToken, { - httpOnly: true, // Prevents JavaScript from accessing the cookie (helps prevent XSS) // Cookie is only sent over HTTPS (use `false` during local development) - sameSite: 'Strict', // Prevents the cookie from being sent with cross-site requests (helps prevent CSRF) - maxAge: maxAges // Sets cookie expiration time - }) - res.json( {message: "login sucess"}) -======= - res.cookie('auth_token', accessToken, isProduction ? { httpOnly: true, sameSite: 'None', maxAge: maxAges, // Example maxAge domain: 'instagram-tool.duckdns.org', secure: isProduction, // Set secure cookies only in production - } : { - httpOnly: true, - sameSite: 'Strict', - maxAge: maxAges, // Example maxAge - secure: isProduction, // Set secure cookies only in production }); res.json( {message: "login success"}) ->>>>>>> Stashed changes } catch (err) { - console.error('Error during login:', err); // Log the error for debugging return res.status(500).send('Database error'); } }); -app.listen(port, () => console.log(`server has started on port: ${port}`)) - -function insertFollowing(name, user){ +// Inserts user information into database +function insertFollowing(name, user, link){ return pool.query(` - INSERT INTO following (user_id, following_id) - VALUES (?, ?) - `, [user, name]) + INSERT INTO following (user_id, following_link, following_id) + VALUES (?, ?, ?) + `, [link, user, name]) } - +// Parses user information from database and stores in dictionary to be returned to client browser function parse(userId){ - return pool.query(`SELECT following_id + return pool.query(`SELECT following_id, following_link FROM following WHERE user_id = ? AND following_id NOT IN (SELECT follower_id FROM followers WHERE user_id = ?); `, [userId, userId]) } - +// Authentices user information function authenticateToken(req, res, next) { const token = req.cookies.auth_token -<<<<<<< Updated upstream - // const authHeader = req.headers['authorization'] - // const token = authHeader && authHeader.split(' ')[1] -======= ->>>>>>> Stashed changes + console.log(req.cookies.auth_token) if (token == null) { return res.sendStatus(401) } @@ -252,6 +276,7 @@ function authenticateToken(req, res, next) { next() }) } +// Deletes user information upon new upload in order to save space function deleteColumns(user) { return pool.query(` DELETE FROM following WHERE user_id = ? @@ -262,9 +287,25 @@ function deleteColumns(user) { `, [user]); }); } -function insertFollowers(name, user){ +// Inserts user information into database +function insertFollowers(name, link, user){ return pool.query(` - INSERT INTO followers (user_id, follower_id) - VALUES (?, ?) - `, [user, name]) -} \ No newline at end of file + INSERT INTO followers (user_id, follower_link, follower_id) + VALUES (?, ?, ?) + `, [user, link, name]) +} + +if (isProduction) { + const options = { + key: fs.readFileSync('/etc/letsencrypt/live/instagram-tool.duckdns.org/privkey.pem'), + cert: fs.readFileSync('/etc/letsencrypt/live/instagram-tool.duckdns.org/fullchain.pem') + }; + + https.createServer(options, app).listen(port, '0.0.0.0', () => { + console.log('Server is running on https://yourdomain.duckdns.org:8383'); + }); +} else { + app.listen(port, '0.0.0.0', () => { + console.log('Server is running on http://localhost:8383'); + }); +} From c982af000cdcabb531fbacac1cc7122caf582a9f Mon Sep 17 00:00:00 2001 From: Wyatt Mogelson <145526766+wyattmog@users.noreply.github.com> Date: Thu, 7 Nov 2024 00:56:30 -0600 Subject: [PATCH 2/2] Delete Backend/.env.production --- Backend/.env.production | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 Backend/.env.production diff --git a/Backend/.env.production b/Backend/.env.production deleted file mode 100644 index c50c464..0000000 --- a/Backend/.env.production +++ /dev/null @@ -1,8 +0,0 @@ -DB1_HOST = '10.0.1.184' -DB1_USER = 'admin' -DB1_PASSWORD = 'LinusandTheo2021!' -DB1_DATABASE = 'instagram_tool' -ROUTE = '/home/opc/InstagramTool/Frontend' - -ACCESS_TOKEN_SECRET = '331ea4ef72ccd393a9edf3bf6ac0df75affb7de5ff2a8462cf3681baadd1c1c1a2c1a64d278c35f41baeaa6a14167fc6f1c439791d97ea122f5ac0631f5407e7' -REFRESH_TOKEN_SECRET = '8d0aa255929ad2437319f1ca5ae388d0f703881d5894ea934466ffd3a9bb7008bebc7ca5b2b6d9da44b5845eb7b6938a55cc52ea933838522fdbf47afcd11808' \ No newline at end of file