diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 202146e..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/proiect-licenta.iml b/.idea/proiect-licenta.iml deleted file mode 100644 index 24643cc..0000000 --- a/.idea/proiect-licenta.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index a9b0bd6..0000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - 1711481695345 - - - - \ No newline at end of file diff --git a/CEO/.env b/CEO/.env new file mode 100644 index 0000000..c411426 --- /dev/null +++ b/CEO/.env @@ -0,0 +1,4 @@ +UDP_PORT=41234 +TCP_PORT=41233 + +HOST=0.0.0.0 \ No newline at end of file diff --git a/CEO/.idea/.name b/CEO/.idea/.name index 12fdd11..d8fcf6c 100644 --- a/CEO/.idea/.name +++ b/CEO/.idea/.name @@ -1 +1 @@ -sending_file_confirmation.js \ No newline at end of file +reset_password.js \ No newline at end of file diff --git a/CEO/.idea/CEO.iml b/CEO/.idea/CEO.iml deleted file mode 100644 index 24643cc..0000000 --- a/CEO/.idea/CEO.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/CEO/.idea/inspectionProfiles/Project_Default.xml b/CEO/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 80292f2..0000000 --- a/CEO/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/CEO/.idea/modules.xml b/CEO/.idea/modules.xml index 24cc0a4..202146e 100644 --- a/CEO/.idea/modules.xml +++ b/CEO/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/CEO/.idea/vcs.xml b/CEO/.idea/vcs.xml index 6c0b863..62bd7a0 100644 --- a/CEO/.idea/vcs.xml +++ b/CEO/.idea/vcs.xml @@ -1,6 +1,7 @@ + \ No newline at end of file diff --git a/CEO/backupSchemes.json b/CEO/backupSchemes.json deleted file mode 100644 index b9cbe93..0000000 --- a/CEO/backupSchemes.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "7c0ef9d5-23ed-47a6-bfb4-fdcfce436904": { - "ip": "192.168.169.19", - "directoryStructure": "{\"backup_dir\":{\"files\":[],\"fisiere\":{\"files\":[\"New Microsoft Excel Worksheet.xlsx\",\"New Microsoft Word Document.docx\"]}}}", - "totalSize": 19363 - }, - "59c712d9-e6dd-438f-aec9-31ca2ae750e6": { - "ip": "192.168.169.53", - "directoryStructure": "{\"backup_dir\":{\"files\":[],\"fisiere\":{\"files\":[\"New Microsoft Excel Worksheet.xlsx\",\"New Microsoft Word Document.docx\"]}}}", - "totalSize": 19363 - } -} \ No newline at end of file diff --git a/CEO/dirBackup.json b/CEO/dirBackup.json deleted file mode 100644 index a1c8356..0000000 --- a/CEO/dirBackup.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "path": "/home/andrei/Desktop/proiect-licenta/dirs/ceo/backup_dir", - "structure": { - "backup_dir": { - "files": [], - "fisiere": { - "files": [ - "New Microsoft Excel Worksheet.xlsx", - "New Microsoft Word Document.docx" - ] - } - } - }, - "size": 19363 -} \ No newline at end of file diff --git a/CEO/dirShare.json b/CEO/dirShare.json deleted file mode 100644 index f07ccf8..0000000 --- a/CEO/dirShare.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "path": "/home/andrei/Desktop/proiect-licenta/dirs/ceo/share_dir" -} \ No newline at end of file diff --git a/CEO/filesReceived.json b/CEO/filesReceived.json deleted file mode 100644 index 45ae4b1..0000000 --- a/CEO/filesReceived.json +++ /dev/null @@ -1 +0,0 @@ -ri6ۑѻ2d{%`nT`s \ No newline at end of file diff --git a/CEO/helpers/http_status.js b/CEO/helpers/http_status.js deleted file mode 100644 index b03796e..0000000 --- a/CEO/helpers/http_status.js +++ /dev/null @@ -1,36 +0,0 @@ -const httpStatus = { - // Informational - CONTINUE: 100, - SWITCHING_PROTOCOLS: 101, - PROCESSING: 102, - - // Success - OK: 200, - CREATED: 201, - ACCEPTED: 202, - NO_CONTENT: 204, - - // Redirection - MOVED_PERMANENTLY: 301, - FOUND: 302, - SEE_OTHER: 303, - NOT_MODIFIED: 304, - TEMPORARY_REDIRECT: 307, - - // Client Error - BAD_REQUEST: 400, - UNAUTHORIZED: 401, - FORBIDDEN: 403, - NOT_FOUND: 404, - METHOD_NOT_ALLOWED: 405, - CONFLICT: 409, - GONE: 410, - UNSUPPORTED_MEDIA_TYPE: 415, - - // Server Error - INTERNAL_SERVER_ERROR: 500, - NOT_IMPLEMENTED: 501, - SERVICE_UNAVAILABLE: 503 -}; - -module.exports = { httpStatus }; diff --git a/CEO/helpers/lock_mechanism.js b/CEO/helpers/lock_mechanism.js deleted file mode 100644 index 2d55fc5..0000000 --- a/CEO/helpers/lock_mechanism.js +++ /dev/null @@ -1,33 +0,0 @@ -const lockfile = require('proper-lockfile'); - -async function lockFile(filePath) { - try { - await lockfile.lock(filePath, { - realpath: false, - retries: { - retries: 10, // Number of retries - factor: 2, // The exponential factor - minTimeout: 1000, // The number of milliseconds before starting the first retry - maxTimeout: 5000, // The maximum number of milliseconds between two retries - randomize: true, // Randomizes the timeouts by multiplying with a factor between 1 to 2 - } - }); - console.log(`File locked: ${filePath}`); - } catch (error) { - console.error(`Error locking file ${filePath}: ${error.message}`); - throw error; // Propagate the error if unable to lock after retries - } -} - -async function unlockFile(filePath) { - try { - await lockfile.unlock(filePath); - console.log(`File unlocked: ${filePath}`); - } catch (error) { - console.error(`Error unlocking file ${filePath}: ${error.message}`); - // Decide whether to throw the error or not, based on your error handling strategy - throw error; - } -} - -module.exports = {lockFile, unlockFile} \ No newline at end of file diff --git a/CEO/ipConfig.json b/CEO/ipConfig.json deleted file mode 100644 index adbb4e0..0000000 --- a/CEO/ipConfig.json +++ /dev/null @@ -1 +0,0 @@ -/ =9 uDH[ \ No newline at end of file diff --git a/CEO/iv.key b/CEO/iv.key deleted file mode 100644 index a8859e3..0000000 --- a/CEO/iv.key +++ /dev/null @@ -1 +0,0 @@ -DZb /@ \ No newline at end of file diff --git a/CEO/jobs/backup.js b/CEO/jobs/backup.js deleted file mode 100644 index 16cedcf..0000000 --- a/CEO/jobs/backup.js +++ /dev/null @@ -1,364 +0,0 @@ -const fsPromises = require('fs').promises; // Ensure you use the promise-based API -const fs = require('fs'); -const path = require('path'); -const { decryptFileInPlace, encryptFileInPlace, encryptFileWithKey, decryptFileWithKey} = require("../src/main/aes_encrypt"); -const {lockFile, unlockFile} = require("../helpers/lock_mechanism"); - - -async function decryptAndGetServerIP(filePath) { - try { - await lockFile(filePath); - await decryptFileInPlace(filePath); - - const fileContent = await fs.promises.readFile(filePath, 'utf8'); - const jsonData = JSON.parse(fileContent); - const serverIP = jsonData.ip; - - await encryptFileInPlace(filePath) - await unlockFile(filePath); - return serverIP; - } catch (error) { - console.error('An error occurred:', error); - return null; - } -} - -function extractFilePaths(directoryStructure) { - let paths = []; - const traverse = (dir, currentPath) => { - Object.keys(dir).forEach(key => { - if (key === 'files') { - dir[key].forEach(file => paths.push(path.join(currentPath, file))); - } else { - traverse(dir[key], path.join(currentPath, key)); - } - }); - }; - traverse(directoryStructure, ''); - return paths; -} - -async function fetchFiles(ip, filePaths) { - let results = {}; - - for (const filePath of filePaths) { - const url = `http://${ip}:3000/file_path`; - try { - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ filePath }) - }); - - if (!response.ok) throw new Error(`HTTP status ${response.status}`); - - const arrayBuffer = await response.arrayBuffer(); // Fetch the response as an ArrayBuffer - results[filePath] = Buffer.from(arrayBuffer); - } catch (error) { - console.error(`Error fetching ${filePath}:`, error); - results[filePath] = null; - } - } - - return results; -} - -async function removeDirectory(directoryPath) { - try { - // Check if the directory exists - const stats = await fsPromises.stat(directoryPath); - if (!stats.isDirectory()) { - console.log('The specified path is not a directory.'); - return; - } - - // Read all the contents of the directory - const files = await fsPromises.readdir(directoryPath); - - // Loop through each file/directory and delete them - for (const file of files) { - const currentPath = path.join(directoryPath, file); - const currentStats = await fsPromises.stat(currentPath); - - if (currentStats.isDirectory()) { - // Recursive call for directories - await removeDirectory(currentPath); - } else { - await lockFile(currentPath); - await unlockFile(currentPath); - await fsPromises.unlink(currentPath); - } - } - - // Finally, delete the directory itself - await fsPromises.rmdir(directoryPath); - //console.log(`Directory removed: ${directoryPath}`); - } catch (error) { - console.error(`Error removing directory: ${error.message}`); - } -} - -async function decryptUserSystemConfig() { - const configPath = path.join(__dirname, '..', 'usersInSystem.json'); - await lockFile(configPath); - await decryptFileInPlace(configPath); -} - -async function encryptUserSystemConfig(){ - const configPath = path.join(__dirname, '..', 'usersInSystem.json'); - await encryptFileInPlace(configPath); - await unlockFile(configPath); -} - -async function processBackup() { - const usersConfigPath = path.join(__dirname, '..', 'usersInSystem.json'); - const backupConfigPath = path.join(__dirname, '..', 'backupSchemes.json'); - const baseBackupDirPath = path.join(__dirname, '..', 'backup_directories'); - if(!fs.existsSync(backupConfigPath) || !fs.existsSync(usersConfigPath)){ - console.log('Can\'t start backup system: necessary files missing!'); - return; - } - - await decryptUserSystemConfig(); - - try { - // Read and parse the users configuration - const usersData = await fsPromises.readFile(usersConfigPath, 'utf8'); - const usersConfig = JSON.parse(usersData); - - // Read and parse the backup configuration - const backupData = await fsPromises.readFile(backupConfigPath, 'utf8'); - const jsonData = JSON.parse(backupData); - - for (const key in jsonData) { - const userBackupDir = path.join(baseBackupDirPath, key); - await removeDirectory(userBackupDir); - - const node = jsonData[key]; - const directoryStructure = JSON.parse(node.directoryStructure); - const filePaths = extractFilePaths(directoryStructure); - const files = await fetchFiles(node.ip, filePaths); - - await fsPromises.mkdir(userBackupDir, { recursive: true }); - - // Find the encryption key for the user's department - let encryptionKey = ''; - for (const dept in usersConfig) { - if (usersConfig[dept].users.includes(key)) { - encryptionKey = usersConfig[dept].key; - break; - } - } - - for (const file in files) { - if (files[file]) { - const fullPath = path.join(userBackupDir, file); - await fsPromises.mkdir(path.dirname(fullPath), { recursive: true }); - await fsPromises.writeFile(fullPath, files[file]); // Write the binary data - - // Encrypt the file in place after writing it - if (encryptionKey) { - await encryptFileWithKey(fullPath, encryptionKey); - } - } - } - } - } catch (error) { - console.error('Failed to read or process the JSON file:', error); - } - - await encryptUserSystemConfig(); -} - -async function getAllFilePaths(dirPath) { - let filePaths = []; - async function recurse(currentPath) { - const entries = await fsPromises.readdir(currentPath, { withFileTypes: true }); - // Create promises for each entry and process them in parallel - const entryPromises = entries.map(async (entry) => { - const resolvedPath = path.join(currentPath, entry.name); - if (entry.isDirectory()) { - // If it's a directory, recurse into it - await recurse(resolvedPath); - } else { - // If it's a file, add it to the file paths array - filePaths.push(resolvedPath); - } - }); - // Wait for all promises to complete - await Promise.all(entryPromises); - } - await recurse(dirPath); - return filePaths; -} - -async function decryptBackupFilesToDirectory(destinationDir) { - await decryptUserSystemConfig(); - const usersConfigPath = path.join(__dirname, '..', 'usersInSystem.json'); - - if(!fs.existsSync(usersConfigPath)){ - return false; - } - - const usersConfig = JSON.parse(await fsPromises.readFile(usersConfigPath, 'utf8')); - const sourceDir = path.join(__dirname, '..', 'backup_directories'); - - try { - for (const department in usersConfig) { - const { key, users } = usersConfig[department]; - for (const userId of users) { - const userDir = path.join(sourceDir, userId); - try { - const files = await getAllFilePaths(userDir); - for (const filePath of files) { - const stats = await fsPromises.stat(filePath); - if (stats.isFile()) { - const relativePath = path.relative(userDir, filePath); - - // Construct the destination file path - const destinationFilePath = path.join(destinationDir, userId, relativePath); - await fsPromises.mkdir(path.dirname(destinationFilePath), { recursive: true }); - await lockFile(filePath); - - // Decrypt the file in its original location - await decryptFileWithKey(filePath, key); - - // Copy the decrypted file to the destination directory - await fsPromises.copyFile(filePath, destinationFilePath); - console.log(`Decrypted file copied to: ${destinationFilePath}`); - - // Re-encrypt the file in its original location (optional) - await encryptFileWithKey(filePath, key); - await unlockFile(filePath); - } - } - } catch (error) { - console.error(`Error processing files for user ${userId}: ${error.message}`); - await encryptUserSystemConfig(); - return false; - } - } - } - } catch (error) { - console.error(`Error processing decryption: ${error.message}`); - await encryptUserSystemConfig(); - return false; - } - - await encryptUserSystemConfig(); - return true; -} - -async function decryptDepartmentFilesToDirectory() { - const dirDepartmentPath = path.join(__dirname, '..', 'dirDepartment.json'); - const loginDataPath = path.join(__dirname, '..', 'loginData.json'); - const usersConfigPath = path.join(__dirname, '..', 'usersInSystem.json'); - const sourceDir = path.join(__dirname, '..', 'backup_directories'); - - if (!fs.existsSync(usersConfigPath) || !fs.existsSync(loginDataPath) || !fs.existsSync(dirDepartmentPath)) { - console.error("Missing one or more essential configuration files."); - return false; - } - - await lockFile(loginDataPath); - await lockFile(usersConfigPath); - - await decryptFileInPlace(loginDataPath); - await decryptFileInPlace(usersConfigPath); - - const { path: destinationDir } = JSON.parse(await fsPromises.readFile(dirDepartmentPath, 'utf8')); - const { department } = JSON.parse(await fsPromises.readFile(loginDataPath, 'utf8')); - const usersConfig = JSON.parse(await fsPromises.readFile(usersConfigPath, 'utf8')); - - // Check if department exists in users configuration - if (!usersConfig[department]) { - console.error(`No configuration found for department: ${department}`); - return false; - } - - const { key, users } = usersConfig[department]; - - await encryptFileInPlace(loginDataPath); - await encryptFileInPlace(usersConfigPath); - - await unlockFile(loginDataPath); - await unlockFile(usersConfigPath); - - await removeDirectory(destinationDir); - - for (const userId of users) { - const userDir = path.join(sourceDir, userId); - - try { - const files = await getAllFilePaths(userDir); - console.log(`Processing files for user ${userId}: ${files}`); - - for (const filePath of files) { - const stats = await fsPromises.stat(filePath); - if (stats.isFile()) { - const relativePath = path.relative(userDir, filePath); - - // Construct the destination file path - const destinationFilePath = path.join(destinationDir, userId, relativePath); - await fsPromises.mkdir(path.dirname(destinationFilePath), { recursive: true }); - - // Decrypt the file in its original location - await decryptFileWithKey(filePath, key); - - // Copy the decrypted file to the destination directory - await fsPromises.copyFile(filePath, destinationFilePath); - console.log(`Decrypted file copied to: ${destinationFilePath}`); - - // Re-encrypt the file in its original location (optional) - await encryptFileWithKey(filePath, key); - } - } - } catch (error) { - console.error(`Error processing files for user ${userId}: ${error.message}`); - return false; - } - } - - console.log("All files processed successfully."); - return true; -} - -let serverIp = null; -decryptAndGetServerIP(path.join(__dirname, '..', 'ipConfig.json')).then(ip => { - serverIp = ip; -}); - -const timeoutInterval = 7 * 60 * 1000 //minutes * seconds * miliseconds - -setInterval(async () => { - await decryptDepartmentFilesToDirectory(); -}, timeoutInterval); - -console.log('backup.js process started!'); -process.on('SIGINT', async () => { - console.log('Shutdown signal received in \'backup.js\'. Cleaning up...'); - - try { - console.log('Performing cleanup tasks...'); - console.log('Cleanup completed successfully.'); - } catch (error) { - console.error('An error occurred during cleanup:', error); - } finally { - console.log('Process terminated'); - process.exit(0); - } -}); - -process.on('message', async (message) => { - if (message.type === 'startBackup') { - console.log('BackupProcess: Received command to start backup.'); - await processBackup() - } - if(message.type === 'decryptBackup'){ - console.log(`BackupProcess: Received command to decrypt backup to ${message.decryptDestPath}`); - console.log(message.decryptDestPath); - await decryptBackupFilesToDirectory(message.decryptDestPath); - } -}); \ No newline at end of file diff --git a/CEO/jobs/clean.js b/CEO/jobs/clean.js deleted file mode 100644 index 236f48a..0000000 --- a/CEO/jobs/clean.js +++ /dev/null @@ -1,28 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -// List of files you want to check and delete -const filesToDelete = [ - path.join(__dirname, '..', 'backupSchemes.json'), - path.join(__dirname, '..', 'dirBackup.json'), - path.join(__dirname, '..', 'dirShare.json'), - path.join(__dirname, '..', 'usersInSystem.json'), - path.join(__dirname, '..', 'loginData.json'), - path.join(__dirname, '..', 'ipConfig.json') -]; - -filesToDelete.forEach(file => { - fs.access(file, fs.constants.F_OK, (err) => { - if (!err) { - fs.unlink(file, err => { - if (err) { - console.error(`Failed to delete ${file}: ${err}`); - } else { - console.log(`${file} was deleted successfully.`); - } - }); - } else { - console.log(`${file} does not exist.`); - } - }); -}); diff --git a/CEO/jobs/external_endpoints.js b/CEO/jobs/external_endpoints.js deleted file mode 100644 index 73d3dc4..0000000 --- a/CEO/jobs/external_endpoints.js +++ /dev/null @@ -1,192 +0,0 @@ -const express = require('express'); -const fs = require('fs'); -const fsExtra = require('fs-extra'); -const path = require('path'); -const Joi = require('joi'); -const { httpStatus } = require('../helpers/http_status'); -const {lockFile, unlockFile} = require("../helpers/lock_mechanism"); -const {decryptFileInPlace, encryptFileInPlace} = require("../src/main/aes_encrypt"); - -const app = express(); -app.use(express.json()); - - -const server = require('http').createServer(app); -const connections = []; - -// Store active connections to close them on shutdown -server.on('connection', (conn) => { - connections.push(conn); - conn.on('close', () => { - connections.splice(connections.indexOf(conn), 1); - }); -}); - -const filePathSchema = Joi.object({ - filePath: Joi.string().required() -}); - -async function getBaseDirectory() { - const filePath = path.join(__dirname, '..', 'dirBackup.json'); - try { - const data = await fs.promises.readFile(filePath, 'utf8'); - const jsonData = JSON.parse(data); - return jsonData.path; - } catch (error) { - console.error('Error reading the base directory:', error); - throw error; - } -} - -app.post('/file_path', async (req, res) => { - const { error, value } = filePathSchema.validate(req.body); - if (error) { - return res.status(httpStatus.BAD_REQUEST).json({ - message: 'Invalid input', - data: error.details - }); - } - - try { - const basePath = await getBaseDirectory(); - let { filePath } = value; - const baseDirName = path.basename(basePath); - if (filePath.startsWith(baseDirName + path.sep)) { - filePath = filePath.slice(baseDirName.length + 1); - } - const fullPath = path.resolve(basePath, filePath); - - fs.stat(fullPath, (err, stats) => { - if (err) { - if (err.code === 'ENOENT') { - return res.status(httpStatus.NOT_FOUND).json({ - message: 'File not found' - }); - } - return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ - message: 'Error accessing the file' - }); - } - - if (!stats.isFile()) { - return res.status(httpStatus.BAD_REQUEST).json({ - message: 'Path is not a file' - }); - } - - // Stream the file as binary data - res.writeHead(httpStatus.OK, { - 'Content-Type': 'application/octet-stream', - 'Content-Disposition': `attachment; filename="${path.basename(fullPath)}"` - }); - const fileStream = fs.createReadStream(fullPath); - fileStream.pipe(res); - }); - } catch (error) { - return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ - message: 'Error processing request' - }); - } -}); - - - -async function addFilePathToJson(filePath) { - const jsonFilePath = path.join(__dirname, '..', 'filesReceived.json'); - let data = { receivedFiles: [] }; - - try { - // Check if the JSON file exists - const fileExists = await fsExtra.pathExists(jsonFilePath); - - if (fileExists) { - await lockFile(jsonFilePath); - await decryptFileInPlace(jsonFilePath); - data = await fsExtra.readJson(jsonFilePath); - } else { - await fsExtra.writeJson(jsonFilePath, data, { spaces: 2 }); - await lockFile(jsonFilePath); - } - - if (data.receivedFiles.findIndex(existingFilePath => existingFilePath === filePath) === -1) { - data.receivedFiles.push(filePath); - } - - // Write the updated data back to the file - await fsExtra.writeJson(jsonFilePath, data, { spaces: 2 }); - // Unlock the file after updating - await unlockFile(jsonFilePath); - await encryptFileInPlace(jsonFilePath); - console.log('File path added successfully.'); - } catch (error) { - console.error('Error updating JSON file:', error); - await encryptFileInPlace(jsonFilePath); - await unlockFile(jsonFilePath); - } -} - -app.use('/share_file', express.raw({ - type: 'application/octet-stream', - limit: '50mb' -})); - -async function getShareDir(filePath) { - try { - await lockFile(filePath); - - const fileContent = await fs.promises.readFile(filePath, 'utf8'); - const jsonData = JSON.parse(fileContent); - const path = jsonData.path; - - await unlockFile(filePath); - return path; - } catch (error) { - console.error('An error occurred:', error); - return null; - } -} - - -app.post('/share_file', async (req, res) => { - // Extract metadata from headers - const idUser = req.headers['x-iduser']; - const nameOfFile = req.headers['x-nameoffile']; - const sizeOfFile = req.headers['x-sizeoffile']; - - // You might want to validate the metadata here - if (!idUser || !nameOfFile || !sizeOfFile) { - return res.status(httpStatus.BAD_REQUEST).json({message: 'Missing metadata headers'}); - } - - // Construct the file path using the metadata - const shareDirPath = await getShareDir(path.join(__dirname, '..', 'dirShare.json')); - const baseDir = path.join(shareDirPath, idUser); - fsExtra.ensureDirSync(baseDir); - - const filePath = path.join(baseDir, nameOfFile); - - try { - fs.writeFileSync(filePath, req.body); - await addFilePathToJson(filePath); - res.status(httpStatus.OK).json({ message: 'Upload successful.' }); - } catch (error) { - console.error('Error during file upload:', error); - res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ message: 'Internal server error.' }); - } -}); - - -console.log('external_endpoints.json started.'); -const PORT = process.env.PORT || 3000; -server.listen(PORT, () => console.log(`Server running on port ${PORT}`)); - -process.on('SIGINT', () => { - console.log('SIGINT signal received. Shutting down gracefully.'); - server.close(() => { - console.log('Server closed.'); - // Ensure all connections are closed - connections.forEach(conn => conn.end()); - setTimeout(() => connections.forEach(conn => conn.destroy()), 5000); - process.exit(0); - }); -}); diff --git a/CEO/jobs/fetcher.js b/CEO/jobs/fetcher.js deleted file mode 100644 index 45bb116..0000000 --- a/CEO/jobs/fetcher.js +++ /dev/null @@ -1,234 +0,0 @@ -const fsPromises = require('fs').promises; -const fs = require('fs'); -const path = require('path'); -const ip = require('ip'); -const {encryptFileInPlace, decryptFileInPlace} = require('../src/main/aes_encrypt'); -const {lockFile, unlockFile} = require("../helpers/lock_mechanism"); - -async function fetchBackupSchemes(serverIp) { - // Fetching backup schemes from the server - const depResponse = await fetch(`http://${serverIp}/backup_schemes`, { - method: 'GET', - headers: {'x-api-key': 'uc_api'} - }); - const backupSchemesJson = await depResponse.json(); - const backupSchemes = backupSchemesJson['data']; - - // Specify the path where the backup schemes will be saved - const destPath = path.join(__dirname, '..', 'backupSchemes.json'); - - // Write the fetched backup schemes to the specified file - try { - await lockFile(destPath); - await fsPromises.writeFile(destPath, JSON.stringify(backupSchemes, null, 2)); // Use null, 2 for pretty formatting - await unlockFile(destPath); - console.log('Backup schemes saved successfully to', destPath); - } catch (error) { - console.error('Failed to save backup schemes:', error); - } -} - -async function fetchUsersAndDepartments(serverIp) { - try { - console.log('se asteapta depart'); - // Fetch departments - const depResponse = await fetch(`http://${serverIp}/users/departments`, { - method: 'GET', - headers: {'x-api-key': 'uc_api'} - }); - - const departmentsJson = await depResponse.json(); - const departments = departmentsJson['data']; - - console.log('se asteapta users') - // Fetch users - const userResponse = await fetch(`http://${serverIp}/users`, { - method: 'GET', - headers: {'x-api-key': 'uc_api'} - }); - const usersJson = await userResponse.json(); - const users = usersJson['data']; - - // Combine the JSON data - const combinedData = Object.values(departments).reduce((acc, {name, key}) => { - acc[name] = { - key, - users: users.filter(user => user.department === name).map(user => user.id) - }; - return acc; - }, {}); - - // Write combined data to a file - const filePath = path.join(__dirname, '..', 'usersInSystem.json'); - if(!fs.existsSync(filePath)){ - await fsPromises.writeFile(filePath, ''); - } - await lockFile(filePath); - await fsPromises.writeFile(filePath, JSON.stringify(combinedData, null, 2), 'utf8'); - - await encryptFileInPlace(filePath); - await unlockFile(filePath); - console.log('Data has been encrypted and saved.'); - } catch (error) { - console.error('Failed to fetch or process data:', error); - } -} - -async function decryptAndGetServerIP(filePath) { - try { - await lockFile(filePath); - await decryptFileInPlace(filePath); - - const fileContent = await fsPromises.readFile(filePath, 'utf8'); - const jsonData = JSON.parse(fileContent); - const serverIP = jsonData.ip; - - await encryptFileInPlace(filePath) - await unlockFile(filePath); - return serverIP; - } catch (error) { - console.error('An error occurred:', error); - return null; - } -} - -async function getDirectoryStructure(dirPath) { - const baseName = path.basename(dirPath); - const entries = await fsPromises.readdir(dirPath, {withFileTypes: true}); - const result = {}; - result[baseName] = {files: []}; - let totalSize = 0; - - for (let entry of entries) { - const entryPath = path.join(dirPath, entry.name); - if (entry.isDirectory()) { - // Recursively get structure for subdirectories - const {structure, size: subSize} = await getDirectoryStructure(entryPath); - result[baseName][entry.name] = structure[Object.keys(structure)[0]]; // Object.keys to fetch the first key name - totalSize += subSize; - } else { - // Add file name to the 'files' array and calculate total size - const stats = await fsPromises.stat(entryPath); - result[baseName].files.push(entry.name); - totalSize += stats.size; - } - } - return {structure: result, size: totalSize}; -} - -async function createBackupScheme(serverIp, structure, size) { - let filePath = path.join(__dirname, '..', 'loginData.json'); - await lockFile(filePath); - await decryptFileInPlace(filePath); - - const userContent = await fsPromises.readFile(filePath, 'utf8'); - const userJson = JSON.parse(userContent); - - await encryptFileInPlace(filePath) - await unlockFile(filePath); - - // Find the IP address of the machine - const machineIP = ip.address(); - - // Prepare the data to be sent - const dataToSend = { - id: userJson.id, - ip: machineIP, - directoryStructure: JSON.stringify(structure), - totalSize: size - }; - - const response = await fetch(`http://${serverIp}/backup_schemes`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': 'uc_api' - }, - body: JSON.stringify(dataToSend) - }); - - if (response.ok) { - console.log("Backup dir structure submitted.") - } else { - const data = await response.json() - console.log(data.message); - } -} - -async function watchDirectoryChanges(serverIp) { - async function readBackupConfig(filePath) { - await lockFile(filePath); - const fileContent = await fsPromises.readFile(filePath, 'utf8'); - await unlockFile(filePath); - return JSON.parse(fileContent); - } - - async function updateBackupConfig(filePath, structure, size) { - const data = { - path: filePath, - structure: structure, - size: size - }; - await lockFile(filePath); - await fsPromises.writeFile(path.join(__dirname, '..', 'dirBackup.json'), JSON.stringify(data, null, 2)); - await unlockFile(filePath); - } - - const configPath = path.join(__dirname, '..', 'dirBackup.json'); - if(!fs.existsSync(configPath)){ - console.log('backupDir file doesn\'t exists'); - return; - } - const backupConfig = await readBackupConfig(configPath); - const storedStructure = backupConfig.structure; - - const {structure: currentStructure, size: currentSize} = await getDirectoryStructure(backupConfig.path); - - if (JSON.stringify(storedStructure) !== JSON.stringify(currentStructure)) { - console.log("Directory structure has changed. Updating backup..."); - await createBackupScheme(serverIp, currentStructure, currentSize); - - await updateBackupConfig(backupConfig.path, currentStructure, currentSize); - console.log('FetcherProcess: start backup.'); - process.send({type: 'startBackup'}); - } else { - console.log("No changes detected in the directory structure."); - } -} - -let serverIp = null -decryptAndGetServerIP(path.join(__dirname, '..', 'ipConfig.json')).then(ip => { - serverIp = ip; -}); - -const timeoutInterval = 1 * 20 * 1000 //minutes * seconds * miliseconds - -setInterval(() => { - fetchUsersAndDepartments(serverIp); -}, timeoutInterval); - -setInterval(() => { - watchDirectoryChanges(serverIp); -}, timeoutInterval); - -setInterval(() => { - fetchBackupSchemes(serverIp); -}, timeoutInterval); - - -console.log("fetcher.js process started."); - -process.on('SIGINT', async () => { - console.log('Shutdown signal received in \'fetcher.js\'. Cleaning up...'); - - try { - console.log('Performing cleanup tasks...'); - console.log('Cleanup completed successfully.'); - } catch (error) { - console.error('An error occurred during cleanup:', error); - } finally { - console.log('Process terminated'); - process.exit(0); - } -}); - diff --git a/CEO/jobs/send_file.js b/CEO/jobs/send_file.js deleted file mode 100644 index b37520f..0000000 --- a/CEO/jobs/send_file.js +++ /dev/null @@ -1,90 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const axios = require('axios'); -const FormData = require('form-data'); -const {lockFile, unlockFile} = require("../helpers/lock_mechanism"); -const {decryptFileInPlace, encryptFileInPlace} = require("../src/main/aes_encrypt"); - -async function decryptAndGetServerIP(filePath) { - try { - await lockFile(filePath); - await decryptFileInPlace(filePath); - - const fileContent = await fs.promises.readFile(filePath, 'utf8'); - const jsonData = JSON.parse(fileContent); - const serverIP = jsonData.ip; - - await encryptFileInPlace(filePath) - await unlockFile(filePath); - return serverIP; - } catch (error) { - console.error('An error occurred:', error); - return null; - } -} - -async function extractId() { - try { - // Path to the JSON file - const jsonFilePath = path.join(__dirname, '..', 'loginData.json'); - - // Read and parse the JSON file - const jsonData = await fs.promises.readFile(jsonFilePath, 'utf-8'); - const { id } = JSON.parse(jsonData); - - return id; - } catch (error) { - console.error('Failed to read file or parse data:', error); - throw error; - } -} - -async function uploadFile(filePath, serverIp) { - try { - const idUser = await extractId(); // Fetch ID from JSON - const nameOfFile = path.basename(filePath); // Extract the file name from the path - - // Ensure the file exists and get its size - if (!fs.existsSync(filePath)) { - console.error("File does not exist: " + filePath); - process.exit(1); - } - const fileSizeInBytes = fs.statSync(filePath).size; - - const form = new FormData(); - form.append('idUser', idUser); - form.append('nameOfFile', nameOfFile); - form.append('sizeOfFile', fileSizeInBytes); - form.append('file', fs.createReadStream(filePath)); - - const response = await axios.post(`http://${serverIp}/upload`, form, { - headers: { - ...form.getHeaders(), - }, - }); - - console.log('File uploaded successfully:', response.data); - process.exit(0); // Exit with status 0 (success) - } catch (error) { - console.error('Error uploading file:', error.message); - process.exit(1); // Exit with status 1 (error) - } -} - -// Command-line argument for file path -if (process.argv.length < 3) { - console.log('Usage: node + Welcome + + +
+
+

BACKUP FETCHER

+
+
+
+

Backup is not searched, please wait...

+
+
+ + diff --git a/CEO/render/html/departments_management.html b/CEO/render/html/departments_management.html new file mode 100644 index 0000000..5d801cb --- /dev/null +++ b/CEO/render/html/departments_management.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + Manage Departments + + +
+
+

Departments

+
+ +
+ +
+
+

Create New Department

+
+ + + +
+
+
+ + + + + diff --git a/CEO/render/html/login.html b/CEO/render/html/login.html new file mode 100644 index 0000000..32b4986 --- /dev/null +++ b/CEO/render/html/login.html @@ -0,0 +1,37 @@ + + + + + + + + + + Login + + +
+
+

DO WE KNOW

+

EACH OTHER?

+
+
+ + + +
+
+ + + + + diff --git a/CEO/render/html/main_menu.html b/CEO/render/html/main_menu.html new file mode 100644 index 0000000..c6f91f5 --- /dev/null +++ b/CEO/render/html/main_menu.html @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + Main Page + + + +
+
+

Are you sure?

+

This operation cannot be reverted. Do you want to proceed with resetting the database?

+
+ + +
+
+
+
+
+
+
+

WELCOME BACK,

+

+

Hope you have a productive day!

+
+ +
+
+
+ + +
+
+ + + +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+
+

NOTIFICATIONS

+
+
+
+ +
+
+
+ + diff --git a/User/src/renderer/html/profile.html b/CEO/render/html/profile.html similarity index 95% rename from User/src/renderer/html/profile.html rename to CEO/render/html/profile.html index 860ae39..c5db853 100644 --- a/User/src/renderer/html/profile.html +++ b/CEO/render/html/profile.html @@ -9,7 +9,7 @@ - + Profile diff --git a/CEO/render/html/reset_password.html b/CEO/render/html/reset_password.html new file mode 100644 index 0000000..8bf382e --- /dev/null +++ b/CEO/render/html/reset_password.html @@ -0,0 +1,36 @@ + + + + + + + + + Reset Password + + +
+
+

RESET

+

YOUR PASSWORD

+
+
+ + + +
+
+ + + + + diff --git a/User/src/renderer/html/share_file.html b/CEO/render/html/share_file.html similarity index 97% rename from User/src/renderer/html/share_file.html rename to CEO/render/html/share_file.html index f578673..6163a09 100644 --- a/User/src/renderer/html/share_file.html +++ b/CEO/render/html/share_file.html @@ -9,7 +9,7 @@ - + Share File diff --git a/CEO/render/html/sign_up.html b/CEO/render/html/sign_up.html new file mode 100644 index 0000000..4a1bdcc --- /dev/null +++ b/CEO/render/html/sign_up.html @@ -0,0 +1,63 @@ + + + + + + + + + Signup Process + + + +
+ +
+
+

LET US MEET

+

EACH OTHER

+
+ +
+ + + +
+ + + + + diff --git a/User/src/renderer/html/sign_up_confirmation.html b/CEO/render/html/uc_not_found.html similarity index 53% rename from User/src/renderer/html/sign_up_confirmation.html rename to CEO/render/html/uc_not_found.html index ac3c893..fb3ce49 100644 --- a/User/src/renderer/html/sign_up_confirmation.html +++ b/CEO/render/html/uc_not_found.html @@ -5,20 +5,22 @@ - + - - + - Setup Completion + UC Not Found
-

ALL THE SETUP IS DONE!

-

LET’S PROCEED TO THE

-

LOGIN PAGE

+

SEARCHING FOR THE

+

SERVER...

+
+
+
+

Please wait while we try to connect to the server.

diff --git a/CEO/render/html/users_management.html b/CEO/render/html/users_management.html new file mode 100644 index 0000000..209e056 --- /dev/null +++ b/CEO/render/html/users_management.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + Manage Users + + +
+
+

Manage Users

+
+ +
+ +
+
+ + diff --git a/CEO/render/html/welcome.html b/CEO/render/html/welcome.html new file mode 100644 index 0000000..ffcc348 --- /dev/null +++ b/CEO/render/html/welcome.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + Welcome + + +
+
+

WELCOME TO THE APP

+
+
+
+

Initializing the application, please wait...

+
+
+ + diff --git a/CEO/render/js/backup_retreive.html b/CEO/render/js/backup_retreive.html new file mode 100644 index 0000000..ce01128 --- /dev/null +++ b/CEO/render/js/backup_retreive.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + Welcome + + +
+
+

BACKUP FETCHER

+
+
+
+

Backup is not searched, please wait...

+
+
+ + diff --git a/CEO/render/js/departments_management.js b/CEO/render/js/departments_management.js new file mode 100644 index 0000000..b357b0d --- /dev/null +++ b/CEO/render/js/departments_management.js @@ -0,0 +1,216 @@ +document.addEventListener('DOMContentLoaded', async () => { + const backButton = document.getElementById('backButton'); + const form = document.getElementById('new-department-form'); + const modifyModal = document.getElementById('modify-department-modal'); + const closeModal = document.getElementById('close-modal'); + const confirmModifyButton = document.getElementById('confirm-modify'); + let currentDepartmentId = null; + + // Retrieve operation codes via IPC + const operationCodes = await window.electronAPI.getOperationsCodes(); + if (!operationCodes) { + await window.electronAPI.showAlert('Internal error of the application.'); + return; + } + + // Open the TCP socket right after fetching the operation codes + if (!await window.electronAPI.openUcSocket()) { + await window.electronAPI.showAlert('Failed to open socket. Internal error of the application.'); + return; + } + + let codeGetDepartments = operationCodes.GET_DEPARTMENTS; + let codeCreateDepartment = operationCodes.CREATE_DEPARTMENT; + let codeModifyDepartment = operationCodes.MODIFY_DEPARTMENT; + let codeDeleteDepartment = operationCodes.DELETE_DEPARTMENT; + let codeOk = operationCodes.OK; + + // Set up the back button - close the socket before going back + backButton.addEventListener('click', async () => { + await window.electronAPI.closeUcSocket(); // Close the socket when going back + fadeOut('main_menu'); // Navigate to the main menu + }); + + // Set up the form for creating new departments + form.addEventListener('submit', async function (event) { + event.preventDefault(); // Prevent the form from refreshing the page + const departmentName = document.getElementById('departmentName').value; + + if (departmentName.trim()) { + await createDepartment(departmentName); // Call createDepartment function when the form is submitted + } + }); + + // Fetch the departments on page load + await fetchDepartments(); + + // Fetch departments and render them in the left block + async function fetchDepartments() { + const departmentsContainer = document.getElementById('departments-container'); + + // Send the request to get the departments + if (!await window.electronAPI.sendUcMessage(codeGetDepartments)) { + await window.electronAPI.showAlert("Failed to send request."); + await redirectToMainMenu(); + return; + } + + const response = await waitForResponse(); + if (!response || response.operationCode !== codeOk) { + await window.electronAPI.showAlert("Unable to fetch departments."); + await redirectToMainMenu(); + return; + } + + departmentsContainer.innerHTML = ''; // Clear any existing data + + const departments = response.metaInfo.departments; + + // Filter out CEO department + const filteredDepartments = departments.filter(department => department.name.toLowerCase() !== 'ceo'); + + // Check if there are no departments after filtering + if (!filteredDepartments.length) { + const noDepartmentsMessage = document.createElement('p'); + noDepartmentsMessage.textContent = 'No departments exist.'; + noDepartmentsMessage.classList.add('no-departments-message'); // Add a class for styling + departmentsContainer.appendChild(noDepartmentsMessage); + return; + } + + // Iterate over the remaining departments and render them + filteredDepartments.forEach(department => { + const departmentDiv = document.createElement('div'); + departmentDiv.classList.add('department-card'); + + // Create department name div + const nameDiv = document.createElement('div'); + nameDiv.classList.add('department-name'); + nameDiv.textContent = department.name; + + // Create modify button + const modifyButton = document.createElement('button'); + modifyButton.textContent = 'Modify'; + modifyButton.classList.add('modify-button'); + modifyButton.dataset.departmentId = department.id; + + // Create delete button + const deleteButton = document.createElement('button'); + deleteButton.textContent = 'Delete'; + deleteButton.classList.add('delete-button'); + deleteButton.dataset.departmentId = department.id; + + // Append department info and buttons to the department div + departmentDiv.appendChild(nameDiv); + departmentDiv.appendChild(modifyButton); + departmentDiv.appendChild(deleteButton); + + // Append department div to the container + departmentsContainer.appendChild(departmentDiv); + }); + + // Add click event listeners for all dynamically created buttons + addEventListenersToButtons(); + } + + // Add event listeners to dynamically created buttons + function addEventListenersToButtons() { + const modifyButtons = document.querySelectorAll('.modify-button'); + const deleteButtons = document.querySelectorAll('.delete-button'); + + modifyButtons.forEach(button => { + button.addEventListener('click', () => { + currentDepartmentId = button.dataset.departmentId; + openModifyModal(); // Open the modal when modify button is clicked + }); + }); + + deleteButtons.forEach(button => { + button.addEventListener('click', () => { + const departmentId = button.dataset.departmentId; + deleteDepartment(departmentId); + }); + }); + } + + // Open the modify modal + function openModifyModal() { + modifyModal.style.display = 'block'; + } + + // Close the modify modal + function closeModifyModal() { + modifyModal.style.display = 'none'; + currentDepartmentId = null; + } + + // Handle modify modal close event + closeModal.addEventListener('click', closeModifyModal); + + // Handle department modification + confirmModifyButton.addEventListener('click', async () => { + const newDepartmentName = document.getElementById('new-department-name').value; + if (newDepartmentName.trim() && currentDepartmentId) { + await modifyDepartment(currentDepartmentId, newDepartmentName); + closeModifyModal(); // Close the modal after modification + } + }); + + async function modifyDepartment(departmentId, newName) { + if (!await window.electronAPI.sendUcMessage(codeModifyDepartment, { + departmentId: departmentId, + newDepartmentName: newName + })) { + await window.electronAPI.showAlert('Failed to send modification request.'); + return; + } + + const response = await waitForResponse(); + if (response && response.operationCode === codeOk) { + await window.electronAPI.showAlert(`Department modified: ${newName}`); + await fetchDepartments(); // Refresh the department list after modification + } else { + await window.electronAPI.showAlert('Failed to modify the department.'); + } + } + + async function deleteDepartment(departmentId) { + const confirmDelete = confirm("Are you sure you want to delete this department?"); + if (confirmDelete) { + if (!await window.electronAPI.sendUcMessage(codeDeleteDepartment, { departmentId })) { + await window.electronAPI.showAlert('Failed to send delete request.'); + return; + } + + const response = await waitForResponse(); + if (response && response.operationCode === codeOk) { + await window.electronAPI.showAlert("Department deleted successfully."); + await fetchDepartments(); // Refresh the department list after deletion + } else { + await window.electronAPI.showAlert("Failed to delete the department."); + } + } + } + + // Function to create a new department + async function createDepartment(departmentName) { + if (!await window.electronAPI.sendUcMessage(codeCreateDepartment, { departmentName })) { + await window.electronAPI.showAlert('Failed to send create request.'); + return; + } + + const response = await waitForResponse(); + if (response && response.operationCode === codeOk) { + await window.electronAPI.showAlert(`New department created: ${departmentName}`); + await fetchDepartments(); // Refresh the department list + } else { + await window.electronAPI.showAlert("Failed to create department."); + } + } + + // Redirect to main menu + async function redirectToMainMenu() { + await window.electronAPI.closeUcSocket(); + fadeOut('main_menu'); + } +}); diff --git a/CEO/render/js/helpers.js b/CEO/render/js/helpers.js new file mode 100644 index 0000000..78cbf06 --- /dev/null +++ b/CEO/render/js/helpers.js @@ -0,0 +1,35 @@ +function fadeIn() { + document.querySelector('.container').classList.remove('fade-out'); + document.querySelector('.container').classList.add('fade-in'); +} + +function fadeOut(destination) { + const container = document.querySelector('.container'); + container.classList.remove('fade-in'); + container.classList.add('fade-out'); + + console.log(destination); + setTimeout(() => {}, 5000); + + container.addEventListener('animationend', async () => { + try { + await window.electronAPI.changeContent(destination); + console.log('Navigated to', destination); + } catch (error) { + console.error('Error navigating:', error); + } + }); +} + +async function waitForResponse() { + return new Promise((resolve) => { + const idResponseCheck = setInterval(async () => { + const status = await window.electronAPI.hasResponseArrived(); + if (status) { + clearInterval(idResponseCheck); + const response = await window.electronAPI.getLastUcResult(); + resolve(response || null); // Resolve the response or null if not available + } + }, 100); // Check every 100 milliseconds if the response has arrived + }); +} diff --git a/CEO/render/js/login.js b/CEO/render/js/login.js new file mode 100644 index 0000000..3262e93 --- /dev/null +++ b/CEO/render/js/login.js @@ -0,0 +1,144 @@ +document.addEventListener('DOMContentLoaded', async function () { + const submitButton = document.getElementById('submit'); + const resetPasswordButton = document.getElementById('resetPassword'); + + // Retrieve the operation codes via IPC + const operationCodes = await window.electronAPI.getOperationsCodes(); + if (!operationCodes) { + await window.electronAPI.showAlert('Internal error of the application. Failed to retrieve operation codes.'); + return; + } + + const codeLogin = operationCodes.LOGIN; + const codeFindByEmail = operationCodes.FIND_BY_EMAIL; + const codeFindKeyByUser = operationCodes.FIND_KEY_BY_USER_ID; + const codeOk = operationCodes.OK; + + resetPasswordButton.addEventListener('click', function (e) { + e.preventDefault(); + window.electronAPI.changeContent('reset_password'); + }); + + // Submit button logic (handle login) + submitButton.addEventListener('click', async function (e) { + e.preventDefault(); + console.log('Submit button clicked'); + + const form = document.getElementById('loginForm'); + const formData = new FormData(form); + + const email = formData.get('email'); + const password = formData.get('password'); + + // Open a TCP socket to the stored IP + if (!await window.electronAPI.openUcSocket()) { + await window.electronAPI.showAlert('Internal error of the application. Unable to open socket.'); + return; + } + + // Attempt login + if (!await attemptLogin(email, password)) { + await window.electronAPI.closeUcSocket(); + return; + } + + // Fetch and store user info + if (!await fetchAndStoreUserInfo(email)) { + await window.electronAPI.closeUcSocket(); + return; + } + + // Fetch user info from local storage + const userInfo = await window.electronAPI.readUserConfig('user_info'); + if (!userInfo) { + await window.electronAPI.closeUcSocket(); + await window.electronAPI.showAlert('Failed to fetch user info.'); + return; + } + + // Fetch and store encryption key + if (!await fetchAndStoreEncryptionKey(userInfo.id)) { + await window.electronAPI.closeUcSocket(); + return; + } + + // Close the socket and navigate to main menu after success + await window.electronAPI.closeUcSocket(); + await window.electronAPI.changeContent('main_menu'); + }); + + async function attemptLogin(email, password) { + const app_type = await window.electronAPI.readUserConfig('app_type'); + const messageData = { email, password, app_type }; + + // Send the login message to the server + if (!await window.electronAPI.sendUcMessage(codeLogin, messageData)) { + await window.electronAPI.showAlert('Failed to send login request.'); + return false; + } + + // Wait for the response using waitForResponse + const response = await waitForResponse(); + if (response && response.operationCode === codeOk) { + await window.electronAPI.resetApplicationInfo(); + await window.electronAPI.resetUserConfig(); + await window.electronAPI.writeUserConfig('app_type', app_type); + await window.electronAPI.writeUserConfig('user_info', { email, password }); + return true; + }else if(response && respone.operationCode !== codeOk){ + await window.electronAPI.showAlert(response.metaInfo.message); + return false; + } + + await window.electronAPI.showAlert('Invalid login response received.'); + return false; + } + + async function fetchAndStoreUserInfo(userEmail) { + if (!await window.electronAPI.sendUcMessage(codeFindByEmail, { email: userEmail })) { + await window.electronAPI.showAlert('Failed to send request to fetch user info.'); + return false; + } + + // Wait for the response using waitForResponse + const response = await waitForResponse(); + if (response && response.operationCode === codeOk) { + let userInfo = await window.electronAPI.readUserConfig('user_info'); + if (!userInfo) { + userInfo = {}; + } + + userInfo.id = response.metaInfo.id; + userInfo.departmentId = response.metaInfo.departmentId; + userInfo.name = response.metaInfo.name || 'User'; + + await window.electronAPI.writeUserConfig('user_info', userInfo); + return true; + } + + await window.electronAPI.showAlert('Failed to fetch user info from server.'); + return false; + } + + async function fetchAndStoreEncryptionKey(userId) { + if (!await window.electronAPI.sendUcMessage(codeFindKeyByUser, { userId })) { + await window.electronAPI.showAlert('Failed to send request to fetch encryption key.'); + return false; + } + + // Wait for the response using waitForResponse + const response = await waitForResponse(); + if (response && response.operationCode === codeOk) { + const encryptionKey = { + key: response.metaInfo.key.key, + iv: response.metaInfo.key.iv, + }; + + await window.electronAPI.writeUserConfig('encryption_key', encryptionKey); + return true; + } + + await window.electronAPI.showAlert('Failed to fetch encryption key from server.'); + return false; + } +}); diff --git a/CEO/render/js/main_menu.js b/CEO/render/js/main_menu.js new file mode 100644 index 0000000..a2418b7 --- /dev/null +++ b/CEO/render/js/main_menu.js @@ -0,0 +1,233 @@ +document.addEventListener('DOMContentLoaded', async function () { + setInterval(loadReceivedFiles, 3000); // Call loadReceivedFiles every 3000 ms (3 seconds) + + const backupButton = document.getElementById('backup-dir'); + const shareButton = document.getElementById('share-dir'); + const departmentButton = document.getElementById('department-dir'); + + const changeInfoButton = document.getElementById('change-info'); + const shareFileButton = document.getElementById('share-file'); + + const manageUsersButton = document.getElementById('manage-users'); + const manageDepartmentsButton = document.getElementById('manage-departments'); + + const resetDatabaseButton = document.getElementById('reset-database'); + const restoreBackupButton = document.getElementById('restore-backup'); + + const logoutButton = document.getElementById('logout'); + + const disclaimerModal = document.getElementById('disclaimer-modal'); + const cancelDisclaimerButton = document.getElementById('disclaimer-cancel'); + const submitDisclaimerButton = document.getElementById('disclaimer-submit'); + + await fetchUserInfo(); + await checkAndSetAllDirectories(); + + backupButton.addEventListener('click', async function () { + await setPath('backupDirectory'); + }); + + shareButton.addEventListener('click', async function () { + await setPath('shareDirectory'); + }); + + departmentButton.addEventListener('click', async function () { + await setPath('departmentDirectory'); + }); + + changeInfoButton.addEventListener('click', function () { + fadeOut('profile'); + }); + + shareFileButton.addEventListener('click', function () { + fadeOut('share_file'); + }); + + manageUsersButton.addEventListener('click', function () { + fadeOut('users_management'); + }); + + manageDepartmentsButton.addEventListener('click', function () { + fadeOut('departments_management' + + ''); + }); + + resetDatabaseButton.addEventListener('click', function () { + disclaimerModal.style.display = 'block'; // Show disclaimer modal + }); + + cancelDisclaimerButton.addEventListener('click', function () { + disclaimerModal.style.display = 'none'; // Hide modal if cancel is clicked + }); + + submitDisclaimerButton.addEventListener('click', async function () { + disclaimerModal.style.display = 'none'; // Hide modal + await resetDatabase(); // Perform database reset + fadeOut('login'); // Fade out and go back to login + }); + + logoutButton.addEventListener('click', async function () { + fadeOut('login'); + }); + + restoreBackupButton.addEventListener('click', async function () { + await restoreBackup(); + }); + + async function restoreBackup() { + const backupDirectory = await window.electronAPI.readApplicationInfo('backupDirectory'); + + if (backupDirectory && backupDirectory.path) { + await window.electronAPI.showAlert('You have already set a backup directory. You cannot restore again.'); + return; + } + + const destinationPath = await window.electronAPI.selectDirectory(); + if (!destinationPath) return; + + await window.electronAPI.startBackupRetrieval(destinationPath); + fadeOut('backup_retrieve'); + } + + async function checkAndSetAllDirectories() { + await attachNotificationButton('backupDirectory', 'Set your backup directory!', 'backup_alert', 'alert'); + await attachNotificationButton('shareDirectory', 'Set your share directory!', 'share_alert', 'alert'); + await attachNotificationButton('departmentDirectory', 'Set your department directory!', 'department_alert', 'alert'); + } + + async function checkPathExistence(pathKey) { + return await window.electronAPI.readApplicationInfo(pathKey); + } + + async function setPath(pathKey) { + const path = await window.electronAPI.selectDirectory(); + if (path === undefined) return; + + const id = await window.electronAPI.createMemoryEntry() + console.log({id, path}) + await window.electronAPI.writeApplicationInfo(pathKey, {id, path}); + } + + async function attachNotificationButton(pathKey, buttonText, buttonId, buttonName) { + const path = await checkPathExistence(pathKey); + if (!path) { + const notificationsDiv = document.getElementById('notifications'); + const button = document.createElement('button'); + button.id = buttonId; + button.name = buttonName; + button.textContent = buttonText; + button.addEventListener('click', async function () { + await setPath(pathKey); + button.remove(); + }); + notificationsDiv.appendChild(button); + } + } + + async function fetchUserInfo() { + const usernameField = document.getElementById('username-field'); + let userInfo = await window.electronAPI.readUserConfig('user_info'); + if (userInfo && userInfo.name) { + usernameField.textContent = userInfo.name; + } else { + console.error("Username field is not available in the DOM."); + } + } + + async function loadReceivedFiles() { + const shareDirectoryData = await window.electronAPI.readApplicationInfo('shareDirectory'); + if (!shareDirectoryData || !shareDirectoryData.id) { + console.log('No received files or directory ID found.'); + return; + } + + const directoryId = shareDirectoryData.id; + const directoryStructure = await window.electronAPI.readMemoryEntry(directoryId); + if (!directoryStructure || !directoryStructure.structure) { + console.log('No received files found in the directory structure.'); + return; + } + + const notificationsDiv = document.getElementById('notifications'); + const existingButtons = Array.from(notificationsDiv.children).map(button => button.getAttribute('data-filepath')); + + Object.keys(directoryStructure.structure).forEach(userName => { + const userFiles = directoryStructure.structure[userName]; + Object.keys(userFiles).forEach(fileName => { + const filePath = userFiles[fileName]; + if (!existingButtons.includes(filePath)) { + const button = document.createElement('button'); + button.setAttribute('data-filepath', filePath); + button.name = 'notification'; + button.textContent = `You received a file "${fileName}" from ${userName}`; + button.addEventListener('click', () => handleFileReceivedButtonPressed(filePath, button)); + notificationsDiv.appendChild(button); + } + }); + }); + } + + async function removeReceivedFile(filePath) { + const shareDirectoryData = await window.electronAPI.readApplicationInfo('shareDirectory'); + if (!shareDirectoryData || !shareDirectoryData.id) { + console.log('No directory ID found to update.'); + return; + } + + const directoryId = shareDirectoryData.id; + let directoryStructure = await window.electronAPI.readMemoryEntry(directoryId); + + if (directoryStructure && directoryStructure.structure) { + for (let userName in directoryStructure.structure) { + let userFiles = directoryStructure.structure[userName]; + if (userFiles[filePath]) { + delete userFiles[filePath]; + if (Object.keys(userFiles).length === 0) { + delete directoryStructure.structure[userName]; + } + await window.electronAPI.updateMemoryEntry(directoryId, directoryStructure); + console.log(`File ${filePath} removed from memory.`); + return; + } + } + } else { + console.log('No directory structure found in memory to update.'); + } + } + + async function handleFileReceivedButtonPressed(filePath, button) { + await window.electronAPI.showFileInExplorer(filePath) + .then(() => console.log('File explorer opened for: ' + filePath)) + .catch(error => console.error('Error opening file explorer:', error)); + + button.remove(); + await removeReceivedFile(filePath); + } + + async function resetDatabase() { + const operationCodes = await window.electronAPI.getOperationsCodes(); + if (!operationCodes) { + await window.electronAPI.showAlert('Internal error of the application. Failed to retrieve operation codes.'); + return; + } + + const codeOk = operationCodes.OK; + const codeResetDatabase = operationCodes.RESET_DATABASE; + + if(!await window.electronAPI.openUcSocket()){ + await window.electronAPI.showAlert('Internal application error.'); + return; + } + + // Send the request to reset the database + if (!await window.electronAPI.sendUcMessage(codeResetDatabase)) { + await window.electronAPI.showAlert('Failed to send reset database request.'); + return; + } + + // Wait for the response + await waitForResponse(); + await window.electronAPI.changeContent('login'); + } + +}); diff --git a/CEO/render/js/profile.js b/CEO/render/js/profile.js new file mode 100644 index 0000000..c942e50 --- /dev/null +++ b/CEO/render/js/profile.js @@ -0,0 +1,82 @@ +document.addEventListener('DOMContentLoaded', async function () { + const {email, password, name} = await window.electronAPI.readUserConfig('user_info'); + + const emailInput = document.querySelector('input[name="email"]'); + const usernameInput = document.querySelector('input[name="username"]'); + const passwordInput = document.querySelector('input[name="password"]'); + + // Pre-fill form fields with existing data + emailInput.value = email; + usernameInput.value = name; + passwordInput.value = password; + + const backButton = document.querySelector('button[name="login"]'); + const submitButton = document.querySelector('button[name="submit"]'); + + // Get operation codes from the backend + const operationCodes = await window.electronAPI.getOperationsCodes(); + if (!operationCodes) { + await window.electronAPI.showAlert('Internal error of the application.'); + return; + } + + const codeModifyUser = operationCodes.MODIFY_USER; + const codeOk = operationCodes.OK; + + // Handle the back button click + backButton.addEventListener('click', async function () { + console.log('Back button clicked!'); + fadeOut('main_menu'); + }); + + // Handle the submit button click + submitButton.addEventListener('click', async function (e) { + e.preventDefault(); + console.log('Submit button clicked!'); + + // Fetch form values + const email = emailInput.value; + const name = usernameInput.value; + const password = passwordInput.value; + + const { id, departmentId, app_type } = await window.electronAPI.readUserConfig('user_info'); // Fetch login data + + // Prepare the data to be sent via the UC socket + const messageData = { + id: id, + name: name, + email: email, + password: password, + departmentId: departmentId, + app_type: app_type // Include app_type in the payload + }; + + // Open UC socket + if (!await window.electronAPI.openUcSocket()) { + await window.electronAPI.showAlert('Failed to open socket. Internal error of the application.'); + return; + } + + // Send the message to the server using the UC socket + if (!await window.electronAPI.sendUcMessage(codeModifyUser, messageData)) { + await window.electronAPI.showAlert('Failed to send message.'); + return; + } + + // Wait for the response + const response = await waitForResponse(); + if (response && response.operationCode === codeOk) { + console.log('User update successful.'); + + // Save updated user info to the userConfig + await window.electronAPI.writeUserConfig('user_credentials', { email: email, password: password }); + await window.electronAPI.writeUserConfig('user_info', {id: id, departmentId: departmentId, name: name}); + + // Navigate back to the main menu + fadeOut('main_menu'); + } else { + console.log('Error updating user:', response?.metaInfo?.message || 'Unknown error'); + await window.electronAPI.showAlert(response?.metaInfo?.message || 'Unknown error occurred'); + } + }); +}); diff --git a/CEO/render/js/reset_password.js b/CEO/render/js/reset_password.js new file mode 100644 index 0000000..06a2f67 --- /dev/null +++ b/CEO/render/js/reset_password.js @@ -0,0 +1,63 @@ +document.addEventListener('DOMContentLoaded', async function () { + const backToLoginButton = document.getElementById('backToLogin'); + const resetPasswordButton = document.getElementById('resetPassword'); + + // Retrieve the operation codes via IPC + const operationCodes = await window.electronAPI.getOperationsCodes(); + if (!operationCodes) { + await window.electronAPI.showAlert('Internal error of the application.'); + return; + } + + const codeResetPassword = operationCodes.RESET_PASSWORD; + const codeOk = operationCodes.OK; + + // Back to login + backToLoginButton.addEventListener('click', function (e) { + e.preventDefault(); + window.electronAPI.changeContent('login'); + }); + + // Reset password logic + resetPasswordButton.addEventListener('click', async function (e) { + e.preventDefault(); + if (!await window.electronAPI.openUcSocket()) { + await window.electronAPI.showAlert('Internal error of the application.'); + return; + } + + const form = document.getElementById('resetPasswordForm'); + const formData = new FormData(form); + const email = formData.get('email'); + const newPassword = formData.get('newPassword'); + + // Ensure the email and new password are provided + if (!email || !newPassword) { + await window.electronAPI.showAlert('Please provide both email and new password.'); + return; + } + + if (!await attemptResetPassword(email, newPassword)) { + return; + } + + await window.electronAPI.closeUcSocket(); + await window.electronAPI.showAlert('Password reset successfully.'); + window.electronAPI.changeContent('login'); + }); + + async function attemptResetPassword(email, newPassword) { + const app_type = await window.electronAPI.readUserConfig('app_type'); + const messageData = { email, newPassword, app_type }; + + if (!await window.electronAPI.sendUcMessage(codeResetPassword, messageData)) return false; + + const response = await waitForResponse(); + if (response && response.operationCode === codeOk) { + return true; + } else { + await window.electronAPI.showAlert(response?.metaInfo?.message || 'Error resetting password.'); + return false; + } + } +}); diff --git a/CEO/render/js/share_file.js b/CEO/render/js/share_file.js new file mode 100644 index 0000000..2053302 --- /dev/null +++ b/CEO/render/js/share_file.js @@ -0,0 +1,118 @@ +document.addEventListener('DOMContentLoaded', async function () { + let user_info = {}; + let pathToFile = ''; + + user_info = await window.electronAPI.readUserConfig('user_info'); + if (!user_info) { + await window.electronAPI.showAlert('No user_info entry found.'); + return; + } + + // Function to update the file name display + function updateFileName() { + const fileNameElement = document.getElementById('fileName'); + if (!fileNameElement) { + return; + } + + if (pathToFile.trim() === '') { + fileNameElement.textContent = 'No file chosen'; + } else { + fileNameElement.textContent = pathToFile.split('\\').pop().split('/').pop(); + } + } + + updateFileName(); + + // Fetch user information from applicationInfo and create checkboxes for selecting users + async function fetchUsersAndCreateCheckboxes() { + const usersInfoId = await window.electronAPI.readApplicationInfo('active_users_info'); + if (!usersInfoId) { + await window.electronAPI.showAlert('Internal error.'); + return; + } + + const usersInfo = await window.electronAPI.readMemoryEntry(usersInfoId); + if (!usersInfo || usersInfo.length === 0) { + await window.electronAPI.showAlert('No active users found.'); + await window.electronAPI.changeContent('main_menu'); + return; + } + + const usersDiv = document.querySelector('.choose_user_form_content'); + usersDiv.innerHTML = ''; + usersInfo.forEach(user => { + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.name = 'users'; + checkbox.value = user.ip; // Store user's IP in the value + + const label = document.createElement('label'); + label.innerHTML = `${user.user_info.name}`; // Display user's name + label.insertBefore(checkbox, label.firstChild); + + usersDiv.appendChild(label); + }); + } + + // Event listener for selecting the file + document.getElementById('selectFile').addEventListener('click', async function () { + try { + pathToFile = await window.electronAPI.selectFile(); + updateFileName(); + } catch (error) { + console.error('Error opening file dialog:', error); + } + }); + + // Event listener for back button to navigate to main menu + document.getElementById('backButton').addEventListener('click', function () { + fadeOut('main_menu'); + }); + + // Submit button logic to queue file send tasks and then navigate to main_menu.html + document.getElementById('submitButton').addEventListener('click', async function (event) { + event.preventDefault(); + + // Check if a file was chosen + if (!pathToFile.trim()) { + await window.electronAPI.showAlert('File not chosen!'); + return; + } + + const form = document.getElementById('userDestForm'); + const checkboxes = form.querySelectorAll('input[name="users"]'); + const selectedUserIps = Array.from(checkboxes) + .filter(checkbox => checkbox.checked) + .map(checkbox => checkbox.value); // Get IP of the selected users + + if (!selectedUserIps.length) { + await window.electronAPI.showAlert('No user selected!'); + return; + } + + for (const selectedUserIp of selectedUserIps) { + try { + // Add task to send file to the queue + const task = { + ip: selectedUserIp, // Destination IP for the file + path: pathToFile, // File path + userName: user_info.name // Sender's username from userConfig + }; + await window.electronAPI.addTaskToSendFileQueue(task); + + console.log(`Task added to send file to IP ${selectedUserIp}`); + } catch (error) { + console.error(`Error processing IP ${selectedUserIp}:`, error); + } + } + + // Navigate back to the main menu after all tasks are queued + fadeOut('main_menu'); // Navigate to the main menu + }); + + // Fetch users and create checkboxes every 5 seconds (in case of updates) + setInterval(fetchUsersAndCreateCheckboxes, 5000); + + fetchUsersAndCreateCheckboxes().then(() => console.log('User checkboxes rendered')); +}); diff --git a/CEO/render/js/sign_up.js b/CEO/render/js/sign_up.js new file mode 100644 index 0000000..2ec0477 --- /dev/null +++ b/CEO/render/js/sign_up.js @@ -0,0 +1,134 @@ +document.addEventListener('DOMContentLoaded', async function () { + const nextToStep2Button = document.getElementById('nextToStep2'); + const backToStep1Button = document.getElementById('backToStep1'); + const submitSignupButton = document.getElementById('submitSignup'); + const backToLogin = document.getElementById('backToLogin'); + + const step1 = document.getElementById('step1'); + const step2 = document.getElementById('step2'); + + // This object will hold the user data + let userData = { + email: null, + name: null, + password: null, + app_type: null, + departmentId: null + }; + + if (!await window.electronAPI.openUcSocket()) { + alert('Internal error of the application.'); + return; + } + + const operationCodes = await window.electronAPI.getOperationsCodes(); + if (!operationCodes) { + await window.electronAPI.showAlert('Internal error of the application.'); + return; + } + + const codeGetDepartments = operationCodes.GET_DEPARTMENTS; + const codeSignUp = operationCodes.SIGN_UP; + const codeOk = operationCodes.OK; + + let departments = await getDepartments(); + if (departments === null) { + await window.electronAPI.showAlert('Failed to fetch departments.'); + return; + } + console.log(departments); + + // Handle form submission for Step 1 (Profile Information) + nextToStep2Button.addEventListener('click', async function (e) { + e.preventDefault(); // Prevent the default form submission + + // Collect data from Step 1 inputs + userData.email = document.querySelector('input[name="email"]').value; + userData.name = document.querySelector('input[name="name"]').value; + userData.password = document.querySelector('input[name="password"]').value; + userData.app_type = await window.electronAPI.readUserConfig('app_type'); + + // Move to Step 2 + const departmentList = document.getElementById('departmentList'); + departmentList.innerHTML = ''; // Clear existing departments + + // Populate department list + departments.forEach(department => { + if (department.name !== 'CEO' && department.name !== 'ADMIN') { + const label = document.createElement('label'); + label.innerHTML = ` + + ${department.name} + `; + departmentList.appendChild(label); + } + }); + + switchStep(step1, step2); + }); + + // Handle form submission for Step 2 (Department Selection) + submitSignupButton.addEventListener('click', async function (e) { + e.preventDefault(); // Prevent form from being submitted in the usual way + + // Get the selected department + userData.departmentId = document.querySelector('input[name="dept"]:checked')?.value; + if (!userData.departmentId) { + await window.electronAPI.showAlert('Please select a department.'); + return; + } + + if (!await attemptSignUp()) { + switchStep(step2, step1); + return; + } + + await window.electronAPI.showAlert('Signup successful!'); + await window.electronAPI.changeContent('login'); + }); + + // Back to Step 1 from Step 2 + backToStep1Button.addEventListener('click', function () { + switchStep(step2, step1); + }); + + // Back to login + backToLogin.addEventListener('click', async function (e) { + e.preventDefault(); + await window.electronAPI.closeUcSocket(); + await window.electronAPI.changeContent('login'); + }); + + async function getDepartments() { + if (!await window.electronAPI.sendUcMessage(codeGetDepartments)) return null; + + const response = await waitForResponse(); + if (response && response.operationCode === codeOk) { + return response.metaInfo.departments; + } + return null; + } + + async function attemptSignUp() { + if (!await window.electronAPI.sendUcMessage(codeSignUp, userData)) return false; + + const response = await waitForResponse(); + if (response && response.operationCode === codeOk) { + return true; + } + await window.electronAPI.showAlert(`Signup failed: ${response?.metaInfo?.message || 'Unknown error'}`); + return false; + } + + // Function to switch between steps + function switchStep(fromStep, toStep) { + fromStep.classList.remove('active'); // Fade out the current step + setTimeout(() => { + fromStep.style.display = 'none'; // Hide the current step after transition + toStep.style.display = 'flex'; // Ensure display is flex for the next step + setTimeout(() => { + toStep.classList.add('active'); // Fade in the next step + }, 20); // Small delay to allow display change before applying opacity + }, 500); // Transition duration (0.5s) + } +}); diff --git a/CEO/render/js/users_management.js b/CEO/render/js/users_management.js new file mode 100644 index 0000000..5444917 --- /dev/null +++ b/CEO/render/js/users_management.js @@ -0,0 +1,142 @@ +document.addEventListener('DOMContentLoaded', async () => { + const backButton = document.getElementById('backButton'); + backButton.addEventListener('click', async () => { + await window.electronAPI.closeUcSocket(); + fadeOut('main_menu'); + }); + + const operationCodes = await window.electronAPI.getOperationsCodes(); + if (!operationCodes) { + alert('Internal error of the application.'); + return; + } + + // Open the TCP socket right after fetching the operation codes + if (!await window.electronAPI.openUcSocket()) { + alert('Failed to open socket. Internal error of the application.'); + return; + } + + let codeGetDepartments = operationCodes.GET_DEPARTMENTS; + let codeGetUsers = operationCodes.GET_USERS; + let codeDeleteUser = operationCodes.DELETE_USER; + let codeOk = operationCodes.OK; + + // Fetch both users and departments, then display the users with their departments + async function fetchData() { + try { + // Fetch departments + const departmentsResponse = await fetchDepartments(); + if (!departmentsResponse || departmentsResponse.operationCode !== codeOk) { + alert('Failed to fetch departments. Redirecting to main menu...'); + await redirectToMainMenu(); + return; + } + + const departments = departmentsResponse.metaInfo.departments; + + // Fetch users + const usersResponse = await fetchUsers(); + if (!usersResponse || usersResponse.operationCode !== codeOk) { + alert('Failed to fetch users. Redirecting to main menu...'); + await redirectToMainMenu(); + return; + } + + const users = usersResponse.metaInfo.users; + + const usersContainer = document.getElementById('users-container'); + usersContainer.innerHTML = ''; // Clear the container + + // Filter out users in the CEO department + const ceoDepartment = departments.find(dept => dept.name.toLowerCase() === 'ceo'); + const filteredUsers = users.filter(user => user.departmentId !== ceoDepartment.id); + + // Check if there are no users after filtering + if (!filteredUsers.length) { + const noUsersMessage = document.createElement('p'); + noUsersMessage.textContent = 'No users exist.'; + noUsersMessage.classList.add('no-users-message'); // Add a class for styling + usersContainer.appendChild(noUsersMessage); + fadeIn(); // Fade in the container when the message is loaded + return; + } + + // Render users if there are any + filteredUsers.forEach(user => { + const userDiv = document.createElement('div'); + userDiv.classList.add('user-card'); + + // Create user name div + const nameDiv = document.createElement('div'); + nameDiv.classList.add('user-name'); + nameDiv.textContent = user.name; + + // Find the user's department name using departmentId + const department = departments.find(dept => dept.id === user.departmentId); + const departmentName = department ? department.name : 'Unknown'; + + // Create department div + const departmentDiv = document.createElement('div'); + departmentDiv.classList.add('user-department'); + departmentDiv.textContent = `Department: ${departmentName}`; + + // Create delete button + const deleteButton = document.createElement('button'); + deleteButton.textContent = 'Delete'; + deleteButton.classList.add('delete-button'); + deleteButton.onclick = () => deleteUser(user.id); + + // Append user info and delete button to the user div + userDiv.appendChild(nameDiv); + userDiv.appendChild(departmentDiv); + userDiv.appendChild(deleteButton); + + // Append user div to the container + usersContainer.appendChild(userDiv); + }); + + fadeIn(); // Fade in the container when the users are loaded + } catch (error) { + console.error('Error fetching data:', error); + alert('An unexpected error occurred. Redirecting to main menu...'); + await redirectToMainMenu(); + } + } + + async function fetchDepartments() { + // Send the request to get departments + if (!await window.electronAPI.sendUcMessage(codeGetDepartments)) { + return null; + } + return await waitForResponse(); + } + + async function fetchUsers() { + // Send the request to get users + if (!await window.electronAPI.sendUcMessage(codeGetUsers)) { + return null; + } + return await waitForResponse(); + } + + async function deleteUser(userId) { + // Find the index of the user in the dummy data + const userIndex = dummyUsers.findIndex(user => user.id === userId); + + // If user found, remove it from the array + if (userIndex !== -1) { + dummyUsers.splice(userIndex, 1); + alert(`User with ID ${userId} deleted.`); + await fetchData(); // Refresh the users after deleting + } + } + + async function redirectToMainMenu() { + await window.electronAPI.closeUcSocket(); + fadeOut('main_menu'); + } + + // Fetch data on page load + await fetchData(); +}); diff --git a/CEO/secret.key b/CEO/secret.key deleted file mode 100644 index e23d93c..0000000 Binary files a/CEO/secret.key and /dev/null differ diff --git a/CEO/src/helpers/backup_manager.ts b/CEO/src/helpers/backup_manager.ts new file mode 100644 index 0000000..38154dd --- /dev/null +++ b/CEO/src/helpers/backup_manager.ts @@ -0,0 +1,205 @@ +import fs from 'fs'; +import path from 'path'; +import { FileEncryptor } from './file_encryptor'; // Assume the class is in this file +import { MemoryManager } from './memory_manager'; // Assume this handles memory-based storage +import { JsonManager } from './json_manager'; // Manages JSON-based configurations +import { TcpClient } from '../network/tcp/tcp_client' // Import your TcpClient class +import { operationCodes } from '../network/operation_codes'; +import { parentPort } from 'worker_threads'; + +export class BackupManager { + private fileEncryptor: FileEncryptor | null = null; + private memoryManager: MemoryManager; + private applicationInfo: JsonManager; + private userConfig: JsonManager; + private readonly clientPort: number; + + constructor(userConfigPath: string, applicationInfoPath: string, memoryManagerPath: string, clientPort: number) { + this.applicationInfo = new JsonManager(applicationInfoPath); + this.userConfig = new JsonManager(userConfigPath); + this.memoryManager = new MemoryManager(memoryManagerPath); + this.clientPort = clientPort; + } + + async start(): Promise { + setInterval(async () => { + await this.initialize(); // Re-run every minute + }, 60000); // 1 minute interval + } + + // Initialize the backup process: fetch data from the app info and memory + private async initialize(): Promise { + // Initialize the encryption settings from UserConfig + const encryptionKeyData = await this.userConfig.readValue('encryption_key'); + if (!encryptionKeyData || !encryptionKeyData.key || !encryptionKeyData.iv) { + console.error('Encryption key data is missing in user configuration.'); + return; + } + + // Create a new FileEncryptor with the retrieved key and IV + this.fileEncryptor = new FileEncryptor(encryptionKeyData.key, encryptionKeyData.iv); + + // Get the name of the user + const userInfo = await this.userConfig.readValue('user_info'); + if (!userInfo || !userInfo.name) { + console.error('User information is missing in user configuration.'); + return; + } + + const userName = userInfo.name; + + // Get the backup directory information from ApplicationInfo + const backupDirectoryData = await this.applicationInfo.readValue('backupDirectory'); + if (!backupDirectoryData || !backupDirectoryData.id || !backupDirectoryData.path) { + console.error('Backup directory information is missing in application info.'); + return; + } + + const activeUsersIp = await this.applicationInfo.readValue('users_ip'); + if (!activeUsersIp || !activeUsersIp.length) { + console.error('No active users found.'); + return; + } + + const backupDirectoryId = backupDirectoryData.id; + const backupDirectoryPath = backupDirectoryData.path; + + // Get the file structure from MemoryManager + const directoryData = await this.memoryManager.retrieveMetaInformation(backupDirectoryId); + if (!directoryData || !directoryData.structure) { + console.error('Backup directory structure is missing in memory.'); + return; + } + + // Send files to the list of IPs + await this.sendFilesToUsers(directoryData.structure, userName, activeUsersIp, backupDirectoryPath); + } + + // Method to encrypt a file and return the base64 string + private encryptFile(filePath: string): string { + if (!this.fileEncryptor) { + return filePath; + } + + if (!fs.existsSync(filePath)) { + console.error(`File not found: ${filePath}`); + return ''; + } + + // Use FileEncryptor to encrypt the file and return the base64 string + return this.fileEncryptor.encryptFileToBase64(filePath); + } + + // Send files to the list of users and remove successfully sent files from the list + private async sendFilesToUsers(fileStructure: { [key: string]: string }, userName: string, usersIp: string[], backupDirectoryPath: string): Promise { + let unsentFiles = Object.keys(fileStructure); // Keep track of unsent files + + for (const fileName of unsentFiles) { + const filePath = fileStructure[fileName]; + const encryptedFileContent = this.encryptFile(filePath); + + if (!encryptedFileContent) { + console.error(`Failed to encrypt file: ${fileName}`); + continue; // Skip to the next file + } + + // Calculate relative file path + const relativeFilePath = path.relative(backupDirectoryPath, filePath); // Get the relative file path + + // Meta information to send + const metaInfo = { + userName, // Name of the user + relativeFilePath // Relative path to preserve the directory structure + }; + + for (const ip of usersIp) { + const tcpClient = new TcpClient(this.clientPort); + tcpClient.openSocket(ip); + + // Wait for AES key and send the file + try { + const sendSuccess = await this.waitForAesKeyAndSendFile(tcpClient, operationCodes.BACKUP_FILE, metaInfo, Buffer.from(encryptedFileContent, 'base64')); + if (sendSuccess) { + console.log(`Successfully sent file: ${fileName} to ${ip}`); + unsentFiles = unsentFiles.filter(f => f !== fileName); // Remove the file from the unsent list + tcpClient.closeSocket(); + break; // Move to the next file after successful send + } + } catch (error) { + console.error(`Failed to send file: ${fileName} to ${ip}. Error: ${error}`); + tcpClient.closeSocket(); + } + } + } + + // If there are any unsent files, notify the parent process + if (unsentFiles.length > 0) { + parentPort?.postMessage({ success: false, message: 'Backup could not be completed for all files', unsentFiles }); + } else { + parentPort?.postMessage({ success: true, message: 'Backup completed successfully' }); + } + } + + // Wait for AES key to be set, send the file, and wait for the response + private async waitForAesKeyAndSendFile(tcpClient: TcpClient, operationCode: string, metaInfo: { [key: string]: any }, fileContent: Buffer, checkInterval: number = 500, timeout: number = 10000): Promise { + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + const intervalId = setInterval(async () => { + const elapsedTime = Date.now() - startTime; + + // Check if AES key is set, if timeout occurs, reject + if (!tcpClient.isAesKeySet()) { + if (elapsedTime > timeout) { + clearInterval(intervalId); + console.error('Timeout waiting for AES key.'); + reject(new Error('Timeout waiting for AES key.')); + } + return; // Continue waiting for AES key + } + + // AES key is set, send the message + clearInterval(intervalId); + + try { + const success = await tcpClient.sendMessage(operationCode, metaInfo, fileContent); + if (!success) { + reject(new Error('Failed to send the file content.')); + return; + } + + // Wait for a response after sending the message + const responseReceived = await this.waitForMessageResponse(tcpClient, timeout); + if (!responseReceived) { + reject(new Error('Timeout waiting for the message response.')); + return; + } + + // Everything went fine + resolve(true); + } catch (error) { + reject(error); + } + }, checkInterval); // Check for AES key every `checkInterval` + }); + } + + // Method to wait for the response from the TCP client + private waitForMessageResponse(tcpClient: TcpClient, timeout: number = 10000): Promise { + return new Promise((resolve) => { + const startTime = Date.now(); + const intervalId = setInterval(() => { + const response = tcpClient.getLastResult(); + const elapsedTime = Date.now() - startTime; + + if (response) { + clearInterval(intervalId); + resolve(true); + } else if (elapsedTime > timeout) { + clearInterval(intervalId); + resolve(false); // No response after timeout + } + }, 100); + }); + } +} diff --git a/CEO/src/helpers/backup_retrieval.ts b/CEO/src/helpers/backup_retrieval.ts new file mode 100644 index 0000000..a21a122 --- /dev/null +++ b/CEO/src/helpers/backup_retrieval.ts @@ -0,0 +1,224 @@ +import { JsonManager } from './json_manager'; // Assuming this manages JSON configurations +import { TcpClient } from '../network/tcp/tcp_client'; // Assuming this handles TCP client connections +import { operationCodes } from '../network/operation_codes'; // Assuming this holds operation codes +import { parentPort } from 'worker_threads'; +import path from 'path'; +import fs from 'fs'; +import crypto from 'crypto'; // Import the crypto module for encryption and decryption + +export class BackupRetrievalWorker { + private userConfig: JsonManager; + private applicationInfo: JsonManager; + private readonly clientPort: number; + private readonly destinationPath: string; + private encryptionKey: Buffer | null = null; + private iv: Buffer | null = null; + + constructor(userConfigPath: string, applicationInfoPath: string, clientPort: number, destinationPath: string) { + this.userConfig = new JsonManager(userConfigPath); + this.applicationInfo = new JsonManager(applicationInfoPath); + this.clientPort = clientPort; + this.destinationPath = destinationPath; + } + + async start(): Promise { + // Retrieve the necessary data from userConfig and applicationInfo + const userInfo = await this.userConfig.readValue('user_info'); + if (!userInfo || !userInfo.name) { + console.error('User information or name is missing.'); + return; + } + const userName = userInfo.name; + + // Load encryption key and IV from userConfig + const encryptionData = await this.userConfig.readValue('encryption_key'); + if (!encryptionData || !encryptionData.key || !encryptionData.iv) { + console.error('Encryption key or IV is missing.'); + return; + } + + // Convert encryption key and IV from base64 to buffer + this.encryptionKey = Buffer.from(encryptionData.key, 'base64'); + this.iv = Buffer.from(encryptionData.iv, 'base64'); + + const activeUsersIp = await this.applicationInfo.readValue('users_ip'); + if (!activeUsersIp || !activeUsersIp.length) { + console.error('No active users found.'); + return; + } + + // Process each IP and request backup data + for (const ip of activeUsersIp) { + try { + const success = await this.processBackupForIp(ip, userName); + if (success) { + console.log(`Backup retrieved successfully from ${ip}`); + } else { + console.error(`Failed to retrieve backup from ${ip}`); + } + } catch (error) { + console.error(`Error processing backup from ${ip}: ${error}`); + } + } + + // Notify the parent that the worker is done + parentPort?.postMessage({ success: true, message: 'Backup retrieval completed.' }); + } + + private async processBackupForIp(ip: string, userName: string): Promise { + const tcpClient = new TcpClient(this.clientPort); + tcpClient.openSocket(ip); + + try { + // Wait for the AES key to be set before continuing + const aesSet = await this.waitForAesKey(tcpClient); + if (!aesSet) { + console.error(`Timeout waiting for AES key on IP ${ip}`); + return false; + } + + // Step 1: Check if a backup exists for the user on this IP + const backupExists = await this.checkIfBackupExists(tcpClient, userName); + if (!backupExists) { + console.log(`No backup found for user ${userName} on IP ${ip}`); + return false; + } + + // Step 2: Request the structure of the backup directory + const backupStructure = await this.requestBackupStructure(tcpClient, userName); + if (!backupStructure || Object.keys(backupStructure).length === 0) { + console.log(`No files found in backup structure for user ${userName} on IP ${ip}`); + return false; + } + + // Step 3: Request and retrieve each file from the backup + for (const relativeFilePath of Object.keys(backupStructure)) { + const fileRequestSuccess = await this.requestBackupFile(tcpClient, userName, relativeFilePath); + if (!fileRequestSuccess) { + console.error(`Failed to retrieve file ${relativeFilePath} from backup for user ${userName} on IP ${ip}`); + return false; // Stop if any file fails to be retrieved + } + } + + return true; // All files retrieved successfully + } catch (error) { + console.error(`Error during backup processing for IP ${ip}: ${error}`); + return false; + } finally { + tcpClient.closeSocket(); // Ensure socket is closed + } + } + + // Wait for AES key to be set + private async waitForAesKey(tcpClient: TcpClient, timeout: number = 10000): Promise { + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + const intervalId = setInterval(() => { + const elapsedTime = Date.now() - startTime; + + if (tcpClient.isAesKeySet()) { + clearInterval(intervalId); + resolve(true); // AES key is set, we can proceed + } else if (elapsedTime > timeout) { + clearInterval(intervalId); + resolve(false); // Timeout reached, AES key not set + } + }, 500); // Check every 500ms + }); + } + + // Wait for a message response with timeout + private async waitForMessageResponse(tcpClient: TcpClient, timeout: number = 10000): Promise { + const startTime = Date.now(); + + return new Promise((resolve) => { + const intervalId = setInterval(() => { + const elapsedTime = Date.now() - startTime; + const response = tcpClient.getLastResult(); + + if (response) { + clearInterval(intervalId); + resolve(response); // Return the response + } else if (elapsedTime > timeout) { + clearInterval(intervalId); + resolve(null); // Timeout, no response + } + }, 500); // Check every 500ms + }); + } + + // Check if the backup exists for the user on the remote IP + private async checkIfBackupExists(tcpClient: TcpClient, userName: string): Promise { + const metaInfo = { name: userName }; + await tcpClient.sendMessage(operationCodes.IS_BACKUP_CREATED, metaInfo); + + const response = await this.waitForMessageResponse(tcpClient); + return response?.metaInfo?.backupExists === true; + } + + // Request the backup structure from the remote IP + private async requestBackupStructure(tcpClient: TcpClient, userName: string): Promise { + const metaInfo = { name: userName }; + await tcpClient.sendMessage(operationCodes.GET_BACKUP_STRUCTURE, metaInfo); + + const response = await this.waitForMessageResponse(tcpClient); + return response?.metaInfo?.structure ? response.metaInfo.structure : null; + } + + // Request a file from the backup and wait for it to be decrypted and stored + private async requestBackupFile(tcpClient: TcpClient, userName: string, relativeFilePath: string): Promise { + const metaInfo = { name: userName, relativeFilePath }; + await tcpClient.sendMessage(operationCodes.REQ_FILE_FROM_BACKUP, metaInfo); + + const response = await this.waitForMessageResponse(tcpClient); + if (response?.operationCode === operationCodes.OK && response.metaInfo && response.fileContent) { + return this.saveFile(response.metaInfo.relativeFilePath, response.fileContent); + } + return false; + } + + // Decrypt the file content using AES-256-CBC and save the decrypted file + private saveFile(relativeFilePath: string, fileContent: string): boolean { + if (!this.encryptionKey || !this.iv) { + console.error('Encryption key or IV is not set.'); + return false; + } + + // Decode the base64-encoded file content into a buffer + let encryptedBuffer: Buffer; + try { + encryptedBuffer = Buffer.from(fileContent, 'base64'); + } catch (error) { + console.error('Error decoding base64 file content:', error); + return false; + } + + // Decrypt the file content + let decryptedContent: Buffer; + try { + const decipher = crypto.createDecipheriv('aes-256-cbc', this.encryptionKey, this.iv); + decryptedContent = Buffer.concat([decipher.update(encryptedBuffer), decipher.final()]); + } catch (error) { + console.error('Error decrypting file:', error); + return false; + } + + // Save the decrypted file content + const fullFilePath = path.join(this.destinationPath, relativeFilePath); + + try { + const dirPath = path.dirname(fullFilePath); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); // Ensure directory exists + } + + fs.writeFileSync(fullFilePath, decryptedContent); + console.log(`File saved successfully: ${fullFilePath}`); + return true; + } catch (error: any) { + console.error(`Error saving file ${relativeFilePath}: ${error.message}`); + return false; + } + } +} diff --git a/CEO/src/helpers/department_sharer.ts b/CEO/src/helpers/department_sharer.ts new file mode 100644 index 0000000..766095f --- /dev/null +++ b/CEO/src/helpers/department_sharer.ts @@ -0,0 +1,278 @@ +import fs from 'fs'; +import path from 'path'; +import { TcpClient } from '../network/tcp/tcp_client'; // Assuming this class exists +import { operationCodes } from '../network/operation_codes'; +import { JsonManager } from './json_manager'; // Manages JSON configurations +import { MemoryManager } from './memory_manager'; // Manages in-memory data structures + +export class DepartmentSharer { + private userConfig: JsonManager; + private applicationInfo: JsonManager; + private memoryManager: MemoryManager; // To read the department files + private departmentDirectory: string | null; + private readonly clientPort: number; + + constructor( + userConfigPath: string, + applicationInfoPath: string, + memoryManagerPath: string, + clientPort: number + ) { + this.userConfig = new JsonManager(userConfigPath); + this.applicationInfo = new JsonManager(applicationInfoPath); + this.memoryManager = new MemoryManager(memoryManagerPath); // To retrieve files + this.clientPort = clientPort; + this.departmentDirectory = null; + } + + // Start sharing files with the department every minute + async start(): Promise { + setInterval(async () => { + await this.shareFilesWithDepartment(); // Retry every minute + }, 10000); // 10-second interval for testing + } + + // Share files with users in the same department + private async shareFilesWithDepartment(): Promise { + console.log('\n\nStarting Department Share Process\n\n'); + + // Get the current user's department information + const userInfo = await this.userConfig.readValue('user_info'); + if (!userInfo || !userInfo.departmentId || !userInfo.name) { + console.error('User information or department ID is missing in the configuration.'); + return; + } + + const departmentId = userInfo.departmentId; + const userName = userInfo.name; + + // Get the list of active users from applicationInfo + const activeUsersId = await this.applicationInfo.readValue('active_users_info'); + if (!activeUsersId) { + console.error('No active users found.'); + return; + } + + const activeUsers = await this.memoryManager.retrieveMetaInformation(activeUsersId); + if (!activeUsers || activeUsers.length === 0) { + console.error('No active users found.'); + return; + } + + // Filter users who belong to the same department + const departmentUsers = activeUsers.filter((user: any) => user.user_info.departmentId === departmentId); + + if (departmentUsers.length === 0) { + console.log('No users found in the same department.'); + return; + } + + // Get department directory info + const departmentData = await this.applicationInfo.readValue('departmentDirectory'); + if (!departmentData || !departmentData.path || !departmentData.id) { + console.error('No department directory found.'); + return; + } + + this.departmentDirectory = departmentData.path; + + // Read files from the MemoryManager related to this department + const departmentFiles = await this.memoryManager.retrieveMetaInformation(departmentData.id); + if (!departmentFiles || !departmentFiles.structure) { + console.error('No files found for this department in the memory manager.'); + return; + } + + // Send each file to every department user + await this.shareFilesWithUsers(departmentUsers, departmentFiles.structure, userName); + + // After sending all files, clear the department directory + await this.clearDepartmentDirectory(departmentUsers); + } + + // Send the files to the users in the department + private async shareFilesWithUsers(users: any[], files: { [key: string]: string }, userName: string): Promise { + const unsentFiles = Object.keys(files); // Keep track of unsent files + + for (const fileName of unsentFiles) { + const filePath = files[fileName]; + + // Ensure the file exists before attempting to send + if (!fs.existsSync(filePath)) { + console.error(`File not found: ${filePath}`); + continue; + } + + // Read the file content + const fileContent = fs.readFileSync(filePath); + + // Get the relative path of the file (used in the meta info) + if (!this.departmentDirectory) return; + const relativeFilePath = path.relative(this.departmentDirectory, filePath); + + // Prepare the metaInfo (same structure as FileSharer) + const metaInfo = { + userName, // Sender's username + relativeFilePath // Use the relative path to preserve directory structure + }; + + // Send the file to each user in the same department + for (const user of users) { + const userIp = user.ip; + const tcpClient = new TcpClient(this.clientPort); + + // Open a connection to the user's IP + tcpClient.openSocket(userIp); + + try { + // Wait for the AES key and send the file + const success = await this.waitForAesKeyAndSendFile(tcpClient, operationCodes.DEPARTMENT_FILE, metaInfo, fileContent); + if (success) { + console.log(`File successfully sent: ${fileName} to user ${userIp}`); + unsentFiles.splice(unsentFiles.indexOf(fileName), 1); // Remove successfully sent file + tcpClient.closeSocket(); // Close the connection after sending + break; // Move to the next file after a successful send + } + } catch (error) { + console.error(`Failed to send file: ${fileName} to IP: ${userIp}. Error: ${error}`); + tcpClient.closeSocket(); // Ensure socket is closed on error + } + } + } + + if (unsentFiles.length > 0) { + console.log('Some files could not be sent, retrying later.'); + } else { + console.log('All files shared successfully.'); + } + } + + // Clear the department directory for all users after sharing + private async clearDepartmentDirectory(users: any[]): Promise { + for (const user of users) { + const userIp = user.ip; + const tcpClient = new TcpClient(this.clientPort); + + // Open a connection to the user's IP + tcpClient.openSocket(userIp); + + try { + const success = await this.waitForAesKeyAndSendClearDepartment(tcpClient, operationCodes.CLEAR_DEPARTMENT); + if (success) { + console.log(`Department directory cleared successfully for user ${userIp}`); + } else { + console.error(`Failed to clear department directory for user ${userIp}`); + } + } catch (error) { + console.error(`Error clearing department directory for user ${userIp}:`, error); + } finally { + tcpClient.closeSocket(); // Ensure the socket is closed + } + } + } + + // Wait for AES key to be set, send the file, and wait for the response + private async waitForAesKeyAndSendFile(tcpClient: TcpClient, operationCode: string, metaInfo: { [key: string]: any }, fileContent: Buffer, checkInterval: number = 500, timeout: number = 10000): Promise { + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + const intervalId = setInterval(async () => { + const elapsedTime = Date.now() - startTime; + + // Check if AES key is set, if timeout occurs, reject + if (!tcpClient.isAesKeySet()) { + if (elapsedTime > timeout) { + clearInterval(intervalId); + console.error('Timeout waiting for AES key.'); + reject(new Error('Timeout waiting for AES key.')); + } + return; // Continue waiting for AES key + } + + // AES key is set, send the file + clearInterval(intervalId); + + try { + const success = await tcpClient.sendMessage(operationCode, metaInfo, fileContent); + if (!success) { + reject(new Error('Failed to send the file content.')); + return; + } + + // Wait for a response after sending the message + const responseReceived = await this.waitForMessageResponse(tcpClient, timeout); + if (!responseReceived) { + reject(new Error('Timeout waiting for the message response.')); + return; + } + + resolve(true); // Everything went fine + } catch (error) { + reject(error); + } + }, checkInterval); // Check for AES key every `checkInterval` + }); + } + + // Wait for AES key to be set, send the clear department message, and wait for the response + private async waitForAesKeyAndSendClearDepartment(tcpClient: TcpClient, operationCode: string, checkInterval: number = 500, timeout: number = 10000): Promise { + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + const intervalId = setInterval(async () => { + const elapsedTime = Date.now() - startTime; + + // Check if AES key is set, if timeout occurs, reject + if (!tcpClient.isAesKeySet()) { + if (elapsedTime > timeout) { + clearInterval(intervalId); + console.error('Timeout waiting for AES key.'); + reject(new Error('Timeout waiting for AES key.')); + } + return; // Continue waiting for AES key + } + + // AES key is set, send the clear department message + clearInterval(intervalId); + + try { + const success = await tcpClient.sendMessage(operationCode, {}); + if (!success) { + reject(new Error('Failed to send the clear department operation.')); + return; + } + + // Wait for a response after sending the message + const responseReceived = await this.waitForMessageResponse(tcpClient, timeout); + if (!responseReceived) { + reject(new Error('Timeout waiting for the message response.')); + return; + } + + resolve(true); // Everything went fine + } catch (error) { + reject(error); + } + }, checkInterval); // Check for AES key every `checkInterval` + }); + } + + // Wait for the response from the TCP client + private waitForMessageResponse(tcpClient: TcpClient, timeout: number = 10000): Promise { + return new Promise((resolve) => { + const startTime = Date.now(); + const intervalId = setInterval(() => { + const response = tcpClient.getLastResult(); + const elapsedTime = Date.now() - startTime; + + if (response?.operationCode === operationCodes.OK) { + clearInterval(intervalId); + resolve(true); // Message successfully received + } else if (elapsedTime > timeout) { + clearInterval(intervalId); + resolve(false); // No response after timeout + } + }, 100); // Check every 100ms + }); + } +} diff --git a/CEO/src/helpers/directory_watcher.ts b/CEO/src/helpers/directory_watcher.ts new file mode 100644 index 0000000..b4404b9 --- /dev/null +++ b/CEO/src/helpers/directory_watcher.ts @@ -0,0 +1,129 @@ +import { promises as fs, watch, FSWatcher } from 'fs'; +import path from 'path'; +import { MemoryManager } from './memory_manager'; +import { JsonManager } from './json_manager'; + +export class DirectoryWatcher { + private directoryPath: string; + private directoryMemoryId: string; + private directoryScheme: any; + private applicationInfo: JsonManager; + private memoryManager: MemoryManager; + private readonly sourceKey: string; + private directoryWatcher: FSWatcher | null; // To store the watcher reference + private totalSize: number; // To store total directory size + + constructor(memoryManagerPath: string, applicationInfoPath: string, sourceKey: string) { + this.sourceKey = sourceKey; + this.applicationInfo = new JsonManager(applicationInfoPath); + this.memoryManager = new MemoryManager(memoryManagerPath); + this.directoryMemoryId = ''; + this.directoryPath = ''; + this.directoryWatcher = null; // Initialize with no watcher + this.totalSize = 0; // Initialize size with zero + } + + // Method to initialize and validate the backup directory + async initialize(): Promise { + const directoryData = await this.applicationInfo.readValue(this.sourceKey); + if (!directoryData) { + return false; + } + + this.directoryPath = directoryData.path; + this.directoryMemoryId = directoryData.id; + + if (!this.directoryMemoryId || !this.directoryPath) { + console.error('Components of entry in \'DirectoryWatcher\' not found.'); + await this.applicationInfo.removeValue(this.sourceKey); + return false; + } + + if (!this.directoryScheme || Object.keys(this.directoryScheme).length === 0) { + // No structure in memory, scan and save it + const result = await this.buildDirectoryScheme(this.directoryPath); + this.directoryScheme = result.structure; + this.totalSize = result.size; + + await this.memoryManager.updateMetaInformation(this.directoryMemoryId, { + structure: this.directoryScheme, + totalSize: this.totalSize, + }); + } + + // Start watching the directory (after stopping any existing watcher) + this.restartWatcher(); + return true; + } + + // Recursively build the directory structure and calculate the total size + private async buildDirectoryScheme(dirPath: string): Promise<{ structure: any, size: number }> { + const directoryScheme: any = {}; + let totalSize = 0; + + const items = await fs.readdir(dirPath, { withFileTypes: true }); + + for (const item of items) { + const fullPath = path.join(dirPath, item.name); + const stats = await fs.stat(fullPath); // Get stats for each item + + if (item.isDirectory()) { + // If it's a directory, recursively build its structure and accumulate size + const { structure, size } = await this.buildDirectoryScheme(fullPath); + directoryScheme[item.name] = structure; + totalSize += size; + } else if (item.isFile()) { + // If it's a file, store its full path and accumulate size + directoryScheme[item.name] = fullPath; + totalSize += stats.size; // Add file size + } + } + + return { structure: directoryScheme, size: totalSize }; + } + + // Restart the directory watcher, ensuring any previous watcher is closed + private restartWatcher(): void { + if (this.directoryWatcher) { + console.log('Stopping existing watcher...'); + this.directoryWatcher.close(); // Stop the existing watcher + } + + this.startDirectoryWatcher(); + } + + // Start watching the backup directory for changes + private startDirectoryWatcher(): void { + if (!this.directoryPath) { + throw new Error('Backup directory not set. Cannot start watcher.'); + } + + this.directoryWatcher = watch(this.directoryPath, { recursive: true }, async (eventType, filename) => { + if (filename) { + console.log(`File change detected: ${eventType} - ${filename}`); + // Rebuild the directory scheme and update memory + const result = await this.buildDirectoryScheme(this.directoryPath); + this.directoryScheme = result.structure; + this.totalSize = result.size; + + await this.memoryManager.updateMetaInformation(this.directoryMemoryId, { + structure: this.directoryScheme, + totalSize: this.totalSize, + }); + + console.log('Directory structure and size updated in memory.'); + } + }); + + console.log(`Watching for changes in: ${this.directoryPath}`); + } + + // Close the directory watcher + public closeWatcher(): void { + if (this.directoryWatcher) { + console.log(`Stopping watcher for ${this.directoryPath}`); + this.directoryWatcher.close(); + this.directoryWatcher = null; // Clear the reference after closing + } + } +} diff --git a/CEO/src/helpers/file_encryptor.ts b/CEO/src/helpers/file_encryptor.ts new file mode 100644 index 0000000..20bc391 --- /dev/null +++ b/CEO/src/helpers/file_encryptor.ts @@ -0,0 +1,55 @@ +import fs from 'fs'; +import crypto from 'crypto'; + +export class FileEncryptor { + private readonly encryptionKey: Buffer; + private readonly iv: Buffer; + + constructor(base64Key: string, base64Iv: string) { + // Decode the base64-encoded key and IV + this.encryptionKey = Buffer.from(base64Key, 'base64'); + this.iv = Buffer.from(base64Iv, 'base64'); + } + + // Method to read a file, encrypt it, and return the encrypted content as a base64 string + public encryptFileToBase64(filePath: string): string { + try { + // Read the file contents + const fileBuffer = fs.readFileSync(filePath); + + // Create the cipher using AES-256-CBC (or another algorithm you prefer) + const cipher = crypto.createCipheriv('aes-256-cbc', this.encryptionKey, this.iv); + + // Encrypt the file data + let encryptedData = cipher.update(fileBuffer); + encryptedData = Buffer.concat([encryptedData, cipher.final()]); + + // Return the encrypted data as a base64 string + return encryptedData.toString('base64'); + } catch (err) { + console.error(`Error encrypting file at path ${filePath}:`, err); + throw err; + } + } + + // Method to decrypt base64-encoded encrypted content and return the decrypted buffer + public decryptBase64(encryptedBase64: string): Buffer { + try { + // Decode the base64-encoded encrypted data + const encryptedData = Buffer.from(encryptedBase64, 'base64'); + + // Create the decipher using AES-256-CBC + const decipher = crypto.createDecipheriv('aes-256-cbc', this.encryptionKey, this.iv); + + // Decrypt the data + let decryptedData = decipher.update(encryptedData); + decryptedData = Buffer.concat([decryptedData, decipher.final()]); + + // Return the decrypted buffer + return decryptedData; + } catch (err) { + console.error('Error decrypting data:', err); + throw err; + } + } +} diff --git a/CEO/src/helpers/file_sharer.ts b/CEO/src/helpers/file_sharer.ts new file mode 100644 index 0000000..b6a6d7e --- /dev/null +++ b/CEO/src/helpers/file_sharer.ts @@ -0,0 +1,152 @@ +import { QueueManager } from './queue_manager'; // Assume the QueueManager is in this path +import { TcpClient } from '../network/tcp/tcp_client'; // Import your TcpClient class +import { operationCodes } from '../network/operation_codes'; +import fs from "fs"; +import path from "path"; +import {compareFnFileItemTask, FileItemTask} from "../interfaces/file_item_task"; + +interface FileSendTask { + ip: string; + path: string; + userName: string; +} + +export class FileSharer { + private queueManager: QueueManager; + private readonly clientPort: number; + + constructor(queueFilePath: string, clientPort: number) { + + + this.queueManager = new QueueManager(queueFilePath, compareFnFileItemTask); + this.clientPort = clientPort; + } + + // Start processing the file queue + async start(): Promise { + setInterval(async () => { + await this.processQueue(); // Process the queue at regular intervals + }, 10000); // 10 seconds interval + } + + // Method to process the queue + private async processQueue(): Promise { + while (!this.queueManager.isEmpty()) { + const task = this.queueManager.peek(); + + if (task) { + const success = await this.sendFile(task); + + if (!success) { + console.error(`Failed to send file: ${task.path} to IP: ${task.ip}. Re-adding to queue.`); + this.queueManager.dequeue(); + this.queueManager.enqueue(task); // Re-add to queue if failed + } + else{ + console.log(`File sent successfully: ${task.path} to IP: ${task.ip}.`); + this.queueManager.dequeue(); + } + } + } + } + + // Method to send the file to a specific IP using the TcpClient + private async sendFile(task: FileSendTask): Promise { + const { ip, path: filePath, userName } = task; + + // Ensure the file exists before attempting to send + if (!fs.existsSync(filePath)) { + console.error(`File not found: ${filePath}`); + return false; + } + + // Read the file contents + const fileContent = fs.readFileSync(filePath); + + // Extract the file name from the file path using path.basename (handles both Windows and Unix) + const fileName = path.basename(filePath); + + const metaInfo = { + userName, // Sender's username + relativeFilePath: fileName, // Use the file name instead of the full path + }; + + const tcpClient = new TcpClient(this.clientPort); + tcpClient.openSocket(ip); + + // Wait for the AES key and then send the file + try { + const sendSuccess = await this.waitForAesKeyAndSendFile(tcpClient, operationCodes.SHARE_FILE, metaInfo, fileContent, task); + tcpClient.closeSocket(); + + return sendSuccess; + } catch (error) { + console.error(`Error sending file: ${filePath} to IP: ${ip}. Error: ${error}`); + tcpClient.closeSocket(); + return false; + } + } + + // Wait for AES key to be set and send the file, handling the response + private async waitForAesKeyAndSendFile(tcpClient: TcpClient, operationCode: string, metaInfo: { [key: string]: any }, fileContent: Buffer, task: FileSendTask, checkInterval: number = 500, timeout: number = 10000): Promise { + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + const intervalId = setInterval(async () => { + const elapsedTime = Date.now() - startTime; + + // Check if AES key is set, if timeout occurs, reject + if (!tcpClient.isAesKeySet()) { + if (elapsedTime > timeout) { + clearInterval(intervalId); + console.error('Timeout waiting for AES key.'); + + reject(new Error('Timeout waiting for AES key.')); + } + return; // Continue waiting for AES key + } + + // AES key is set, send the message + clearInterval(intervalId); + + try { + const success = await tcpClient.sendMessage(operationCode, metaInfo, fileContent); + if (!success) { + reject(new Error('Failed to send the file content.')); + return; + } + + // Wait for a response after sending the message + const responseReceived = await this.waitForMessageResponse(tcpClient, timeout); + if (!responseReceived) { + reject(new Error('Timeout waiting for the message response.')); + return; + } + + resolve(true); // Everything went fine + } catch (error) { + reject(error); + } + }, checkInterval); // Check for AES key every `checkInterval` + }); + } + + // Method to wait for the response from the TCP client + private waitForMessageResponse(tcpClient: TcpClient, timeout: number = 10000): Promise { + return new Promise((resolve) => { + const startTime = Date.now(); + const intervalId = setInterval(() => { + const response = tcpClient.getLastResult(); + const elapsedTime = Date.now() - startTime; + + if (response?.operationCode === operationCodes.OK) { + clearInterval(intervalId); + resolve(true); // Message successfully received + } else if (elapsedTime > timeout) { + clearInterval(intervalId); + resolve(false); // No response after timeout + } + }, 100); // Check every 100ms + }); + } +} diff --git a/CEO/src/helpers/json_manager.ts b/CEO/src/helpers/json_manager.ts new file mode 100644 index 0000000..627acac --- /dev/null +++ b/CEO/src/helpers/json_manager.ts @@ -0,0 +1,119 @@ +import fs from 'fs'; +import path from 'path'; + +export class JsonManager { + private readonly filePath: string; + private readonly lockFilePath: string; + + constructor(filePath: string) { + const dir = path.dirname(filePath); + + // Check if the directory exists, throw error if it doesn't + if (!fs.existsSync(dir)) { + throw new Error(`The directory does not exist: ${dir}`); + } + + this.filePath = filePath; + this.lockFilePath = `${filePath}.lock`; // Define the lock file path + + // If the file doesn't exist, create it + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, JSON.stringify({}, null, 2), 'utf8'); + } + } + + // Method to acquire a lock (create .lock file) + private async acquireLock(): Promise { + while (fs.existsSync(this.lockFilePath)) { + // Wait until the lock file is released + await new Promise(resolve => setTimeout(resolve, 100)); // 100ms delay before retrying + } + // Create the lock file + fs.writeFileSync(this.lockFilePath, ''); + } + + // Method to release the lock (delete .lock file) + private releaseLock(): void { + if (fs.existsSync(this.lockFilePath)) { + fs.unlinkSync(this.lockFilePath); + } + } + + // Read a value by key from the JSON file with a lock + public async readValue(key: string): Promise { + await this.acquireLock(); // Acquire the lock + + try { + if (!fs.existsSync(this.filePath)) return null; + + const data = JSON.parse(fs.readFileSync(this.filePath, 'utf8')); + return data[key] !== undefined ? data[key] : null; + } catch (err: any) { + console.error(`Error reading from JSON file: ${err.message}`); + return null; + } finally { + this.releaseLock(); // Always release the lock after the operation + } + } + + // Write a key-value pair to the JSON file with a lock + public async writeValue(key: string, value: any): Promise { + await this.acquireLock(); // Acquire the lock + + try { + let data: { [key: string]: any } = {}; + + if (fs.existsSync(this.filePath)) { + data = JSON.parse(fs.readFileSync(this.filePath, 'utf8')); + } + + // Update the key with the new value + data[key] = value; + + fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2), 'utf8'); + return true; + } catch (err: any) { + console.error(`Error writing to JSON file: ${err.message}`); + return false; + } finally { + this.releaseLock(); // Always release the lock after the operation + } + } + + // Remove a key-value pair from the JSON file with a lock + public async removeValue(key: string): Promise { + await this.acquireLock(); // Acquire the lock + + try { + if (!fs.existsSync(this.filePath)) return false; + + const data = JSON.parse(fs.readFileSync(this.filePath, 'utf8')); + if (data[key] !== undefined) { + delete data[key]; + fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2), 'utf8'); + return true; + } + return false; + } catch (err: any) { + console.error(`Error removing key from JSON file: ${err.message}`); + return false; + } finally { + this.releaseLock(); // Always release the lock after the operation + } + } + + // Reset the JSON file by clearing all data with a lock + public async resetFile(): Promise { + await this.acquireLock(); // Acquire the lock + + try { + fs.writeFileSync(this.filePath, JSON.stringify({}, null, 2), 'utf8'); + return true; + } catch (err: any) { + console.error(`Error resetting JSON file: ${err.message}`); + return false; + } finally { + this.releaseLock(); // Always release the lock after the operation + } + } +} diff --git a/CEO/src/helpers/memory_manager.ts b/CEO/src/helpers/memory_manager.ts new file mode 100644 index 0000000..93e93b2 --- /dev/null +++ b/CEO/src/helpers/memory_manager.ts @@ -0,0 +1,47 @@ +import { v4 as uuidv4 } from 'uuid'; +import { JsonManager } from './json_manager'; + +export class MemoryManager extends JsonManager { + constructor(filePath: string) { + super(filePath); // Call the parent constructor to ensure file initialization + } + + // Generate a new unique GUID and ensure it doesn't already exist in the file + private generateUniqueGuid(): Promise { + const generate = async (): Promise => { + const guid = uuidv4(); + const value = await this.readValue(guid); + if (value === null) { + return guid; + } + return generate(); + }; + return generate(); + } + + // Store meta information with a unique GUID as the key + public async storeMetaInformation(metaInfo: any): Promise { + const guid = await this.generateUniqueGuid(); + const success = await this.writeValue(guid, metaInfo); + if (success) { + return guid; // Return the unique GUID for future reference + } else { + throw new Error('Failed to store meta information.'); + } + } + + // Retrieve meta information using the GUID + public retrieveMetaInformation(guid: string): Promise { + return this.readValue(guid); + } + + // Update meta information by merging new data into existing data + public async updateMetaInformation(guid: string, newMetaInfo: any): Promise { + return await this.writeValue(guid, newMetaInfo); + } + + // Remove meta information using the GUID + public removeMetaInformation(guid: string): Promise { + return this.removeValue(guid); + } +} diff --git a/CEO/src/helpers/queue_manager.ts b/CEO/src/helpers/queue_manager.ts new file mode 100644 index 0000000..cc3aeb0 --- /dev/null +++ b/CEO/src/helpers/queue_manager.ts @@ -0,0 +1,137 @@ +import fs from 'fs'; +import path from 'path'; + +export class QueueManager { + private readonly filePath: string; + private readonly lockFilePath: string; + private queue: T[]; + private readonly compareFn: (a: T, b: T) => boolean; // Comparison function + + constructor(filePath: string, compareFn: (a: T, b: T) => boolean) { + this.filePath = filePath; + this.lockFilePath = `${filePath}.lock`; // Define the lock file path + this.queue = []; + this.compareFn = compareFn; + + const dir = path.dirname(filePath); + + // Check if the directory exists, throw error if it doesn't + if (!fs.existsSync(dir)) { + throw new Error(`The directory does not exist: ${dir}`); + } + + // If the file doesn't exist, create it + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, JSON.stringify([], null, 2), 'utf8'); + } + } + + // Method to acquire a lock (create .lock file) + private acquireLock(): void { + while (fs.existsSync(this.lockFilePath)) { + // Wait until the lock file is released + this.sleepSync(100); // 100ms delay before retrying + } + // Create the lock file + fs.writeFileSync(this.lockFilePath, ''); + } + + // Sleep function to simulate delay for locking mechanism + private sleepSync(ms: number): void { + const start = Date.now(); + while (Date.now() - start < ms) { + // busy wait + } + } + + // Method to release the lock (delete .lock file) + private releaseLock(): void { + if (fs.existsSync(this.lockFilePath)) { + fs.unlinkSync(this.lockFilePath); + } + } + + // Load the queue from the JSON file + loadQueue(): void { + this.acquireLock(); // Acquire the lock + + try { + const fileData = fs.readFileSync(this.filePath, 'utf8'); + this.queue = JSON.parse(fileData) || []; + } catch (err) { + // If the file doesn't exist or is invalid, start with an empty queue + this.queue = []; + } finally { + this.releaseLock(); // Release the lock + } + } + + // Save the queue back to the JSON file + saveQueue(): void { + this.acquireLock(); // Acquire the lock + + try { + fs.writeFileSync(this.filePath, JSON.stringify(this.queue, null, 2), 'utf8'); + } finally { + this.releaseLock(); // Release the lock + } + } + + // Enqueue: Add an item to the end of the queue if it doesn't already exist + enqueue(item: T): void { + this.loadQueue(); // Ensure we load the latest queue + + // Check if the item already exists in the queue + const exists = this.queue.some(existingItem => this.compareFn(existingItem, item)); + + console.log(this.queue); + + if (!exists) { + this.queue.push(item); + this.saveQueue(); // Save the updated queue + } else { + console.log('Item already exists in the queue. Skipping enqueue.'); + } + } + + // Dequeue: Remove an item from the front of the queue + dequeue(): T | null { + this.loadQueue(); // Ensure we load the latest queue + if (this.queue.length === 0) { + return null; // Queue is empty + } + const item = this.queue.shift() as T; // Remove the first item + this.saveQueue(); // Save the updated queue + return item; + } + + // Peek: Get the item at the front of the queue without removing it + peek(): T | null { + this.loadQueue(); // Ensure we load the latest queue + return this.queue.length > 0 ? this.queue[0] : null; + } + + // Check if the queue is empty + isEmpty(): boolean { + this.loadQueue(); // Ensure we load the latest queue + return this.queue.length === 0; + } + + // Get the length of the queue + length(): number { + this.loadQueue(); // Ensure we load the latest queue + return this.queue.length; + } + + // Clear the entire queue + clearQueue(): void { + this.acquireLock(); // Acquire the lock + + try { + this.queue = []; // Clear the queue + fs.writeFileSync(this.filePath, JSON.stringify(this.queue, null, 2), 'utf8'); + } finally { + this.releaseLock(); // Release the lock + } + } +} diff --git a/CEO/src/helpers/task_scheduler.ts b/CEO/src/helpers/task_scheduler.ts new file mode 100644 index 0000000..e250c1d --- /dev/null +++ b/CEO/src/helpers/task_scheduler.ts @@ -0,0 +1,76 @@ +import { JsonManager } from './json_manager'; // Assuming you have this class for managing user/memory +import { WindowManager } from './window_manager'; // For managing app navigation +import { UdpClient } from '../network/udp/udp_client'; + +export class TaskScheduler { + private applicationInfo: JsonManager; + private windowManager: WindowManager; + private appStarted: boolean = false; + private intervalIds: NodeJS.Timeout[] = []; // Array to store interval IDs + + constructor(applicationInfo: JsonManager, windowManager: WindowManager) { + this.applicationInfo = applicationInfo; + this.windowManager = windowManager; + } + + // Function to schedule the UC check task with dynamic UDP client creation + public startUCCheck(udpPort: number, okPage: string, errorPage: string, interval: number = 30000) { + const intervalId = setInterval(async () => { + try { + const udpClient = new UdpClient(udpPort); // Create UdpClient with the provided port + const aliveClients = await udpClient.getAliveClients(); + const storedIp = await this.applicationInfo.readValue('serverIp'); + const foundClient = aliveClients.length > 0; + + if (foundClient) { + const ipAddress = aliveClients[0]; // Just using the first alive client + + if (!storedIp || storedIp !== ipAddress) { + await this.applicationInfo.writeValue('serverIp', ipAddress); + if (!this.appStarted) { + await this.windowManager.changeContent(okPage); + } + this.appStarted = true; + } else if (!this.appStarted) { + await this.windowManager.changeContent(okPage); + this.appStarted = true; + } + } else { + await this.windowManager.changeContent(errorPage); + } + } catch (err) { + console.error('Error checking UC:', err); + await this.windowManager.changeContent(errorPage); + } + }, interval); + + this.intervalIds.push(intervalId); + } + + // Function to schedule the IP lookup task, storing the active addresses in memory + public async startUserIPLookup(udpPort: number, interval: number = 30000) { + const serverIp = await this.applicationInfo.readValue('serverIp'); // Ensure it's awaited if it's an async function + const intervalId = setInterval(async () => { + try { + const udpClient = new UdpClient(udpPort); // Create a UDP client with the provided port + const activeIPs = await udpClient.getAliveClients(); // Get the list of active IPs + + // Filter out the serverIp from the list of active clients + const filteredIPs = activeIPs.filter(ip => ip !== serverIp); + + // Save the filtered IPs to 'users_ip' + await this.applicationInfo.writeValue('users_ip', filteredIPs); + } catch (err) { + console.error('Error during user IP lookup:', err); + } + }, interval); + + this.intervalIds.push(intervalId); // Store the interval ID + } + + // Function to stop all tasks (UC check, user worker, etc.) + public stopAllTasks() { + this.intervalIds.forEach(clearInterval); // Clear all intervals + this.intervalIds = []; // Reset the array + } +} diff --git a/CEO/src/helpers/tcp_communicator.ts b/CEO/src/helpers/tcp_communicator.ts new file mode 100644 index 0000000..ac85298 --- /dev/null +++ b/CEO/src/helpers/tcp_communicator.ts @@ -0,0 +1,81 @@ +import { TcpClient } from "../network/tcp/tcp_client"; +import { ParsedMessage } from "../network/message_handler"; + +export class TcpCommunicator { + private readonly ip: string; + private readonly port: number; + private tcpClient: TcpClient | null = null; + private lastResult: ParsedMessage | null = null; + + constructor(ip: string, port: number) { + this.ip = ip; + this.port = port; + } + + async connect(): Promise { + this.tcpClient = new TcpClient(this.port); + this.tcpClient.openSocket(this.ip); + return this.tcpClient.isSocketConnected(); + } + + async sendMessage(operationCode: string, metaInfo?: { [key: string]: any }, fileContent?: Buffer): Promise { + if (!this.tcpClient || !this.tcpClient.isSocketConnected()) return false; + + // Wait until the AES key is set before sending the message + return new Promise((resolve) => { + const idWaitForAes = setInterval(async () => { + if (this.tcpClient?.isAesKeySet()) { + clearInterval(idWaitForAes); + + // Send the message once AES key is set + const status = await this.tcpClient!.sendMessage(operationCode, metaInfo, fileContent); + + if (status) { + await this.waitForResponse(); + } + + resolve(status); + } + }, 100); + }); + } + + getLastResult(): ParsedMessage | null { + const message = this.lastResult; + this.lastResult = null; + return message; + } + + hasResponseArrived(): boolean { + if(!this.tcpClient) return false; + return this.lastResult !== null; + } + + private waitForResponse(): Promise { + return new Promise((resolve, reject) => { + if (!this.tcpClient) { + reject(); + } + + // Start interval for waiting for the response + const responseInterval = setInterval(() => { + if (!this.tcpClient?.isSocketConnected()) { + clearInterval(responseInterval); + resolve(); + } + + if (this.tcpClient?.isMessageReceived()) { + this.lastResult = this.tcpClient.getLastResult(); + clearInterval(responseInterval); // Stop checking once we have a response + resolve(); + } + }, 100); // Check every 100 milliseconds + }); + } + + async disconnect(): Promise { + if (!this.tcpClient || !this.tcpClient.isSocketConnected()) return true; + this.tcpClient.closeSocket(); + return true; + } +} diff --git a/CEO/src/helpers/users_info_fetcher.ts b/CEO/src/helpers/users_info_fetcher.ts new file mode 100644 index 0000000..a4827f3 --- /dev/null +++ b/CEO/src/helpers/users_info_fetcher.ts @@ -0,0 +1,111 @@ +import { JsonManager } from "./json_manager"; +import { MemoryManager } from "./memory_manager"; +import { operationCodes } from "../network/operation_codes"; +import { TcpCommunicator } from "./tcp_communicator"; + +export class UsersInfoFetcher { + private applicationInfo: JsonManager; + private memoryManager: MemoryManager; + private tcpCommunicator: TcpCommunicator | null = null; + private readonly clientPort: number; + private memoryId: string; + private readonly activeUsersKey: string; + + constructor(applicationInfoPath: string, memoryManagerPath: string, clientPort: number) { + this.applicationInfo = new JsonManager(applicationInfoPath); + this.memoryManager = new MemoryManager(memoryManagerPath); + this.memoryId = ''; + this.clientPort = clientPort; + this.tcpCommunicator = null; + this.activeUsersKey = 'active_users_info'; + } + + // Method to start checking user info periodically (every minute) + async start(): Promise { + setInterval(async () => { + await this.initialize(); // Re-run every minute + }, 60000); // 1 minute interval + } + + // Initialize and fetch user IPs and process users info + private async initialize() { + const usersIps = await this.applicationInfo.readValue('users_ip'); + if (!usersIps) { + console.error('No IP addresses found in users_ip'); + return; + } + + // Ensure active_users_info exists in the memory + this.memoryId = await this.applicationInfo.readValue(this.activeUsersKey); + if (!this.memoryId) { + this.memoryId = await this.memoryManager.storeMetaInformation([]); + await this.applicationInfo.writeValue(this.activeUsersKey, this.memoryId); + } + + // Check user information + await this.checkUsersInfo(usersIps); + } + + // Check user info from the list of IPs + private async checkUsersInfo(usersIps: string[]) { + let usersInfo: Array<{ ip: string, user_info: any }> = []; // Array to store IP and user_info objects + + for (const ip of usersIps) { + this.tcpCommunicator = new TcpCommunicator(ip, this.clientPort); + if (!await this.tcpCommunicator.connect()) { + console.error(`Failed to open connection for IP: ${ip}`); + continue; + } + + // Wait for the response for 10 seconds + const response = await this.waitForResponse(); + console.log(response); + + // If a response is received and is successful, append it to usersInfo + if (response && response.metaInfo) { + console.log(`Response received from ${ip}:`, response); + usersInfo.push({ + ip: ip, + user_info: response.metaInfo // Push the IP and user_info object into the array + }); + } + + await this.tcpCommunicator.disconnect(); + } + + await this.updateActiveUsers(usersInfo); // Update active users information in the memory + } + + // Send the message once and wait for the response for a specific timeout + private async waitForResponse(timeout: number = 10000): Promise { + // Send the message once + const status = await this.tcpCommunicator?.sendMessage(operationCodes.GET_USER_INFORMATION); + if (!status) { + return null; + } + + // Wait for the response until the timeout + return new Promise((resolve) => { + const startTime = Date.now(); + + const checkResponseInterval = setInterval(async () => { + // Check if the message has been received + if (this.tcpCommunicator?.hasResponseArrived()) { + clearInterval(checkResponseInterval); + resolve(this.tcpCommunicator?.getLastResult()); // Return the response once it arrives + } + + // If the timeout is reached, stop checking and resolve with null + if (Date.now() - startTime > timeout) { + clearInterval(checkResponseInterval); + resolve(null); + } + }, 100); // Check every 100ms if the response has arrived + }); + } + + // Update active users information in the memory + private async updateActiveUsers(userInfo: any[]) { + await this.memoryManager.updateMetaInformation(this.memoryId, userInfo); // Update active users in memory + } +} diff --git a/CEO/src/helpers/window_manager.ts b/CEO/src/helpers/window_manager.ts new file mode 100644 index 0000000..c950b4e --- /dev/null +++ b/CEO/src/helpers/window_manager.ts @@ -0,0 +1,92 @@ +import { BrowserWindow, dialog, shell } from 'electron'; +import fs from 'fs'; +import path from 'path'; + +export class WindowManager { + private readonly mainWindow: BrowserWindow; + private readonly pathToPagesDir: string; + + constructor(mainWindow: BrowserWindow, pathToPagesDir: string) { + this.pathToPagesDir = pathToPagesDir; + this.mainWindow = mainWindow; + } + + // Show an alert dialog + async showAlert(message: string): Promise { + if (this.mainWindow) { + await dialog.showMessageBox(this.mainWindow, { + type: 'info', + title: 'Alert', + message: message, + buttons: ['OK'], + }); + } else { + console.error('Main window is not available.'); + } + } + + // Change the content of the current window to load a new HTML file + async changeContent(destination: string): Promise { + if (this.mainWindow) { + try { + const destinationPath = path.join(this.pathToPagesDir, `${destination}.html`); + console.log(`Navigating to: ${destinationPath}`); + + // Load the destination HTML file into the main window + await this.mainWindow.loadFile(destinationPath); + console.log(`Navigated to ${destination}`); + } catch (error) { + console.error('Error changing content:', error); + throw error; // Pass the error back to the render process + } + } else { + console.error('Main window is not available.'); + } + } + + // New method to select a directory + async selectDirectory(): Promise { + const result = await dialog.showOpenDialog(this.mainWindow, { + properties: ['openDirectory'], // Only allow selecting directories + }); + + // If the user cancels, result.filePaths will be an empty array + if (result.filePaths && result.filePaths.length > 0) { + return result.filePaths[0]; // Return the selected directory path + } else { + console.log('No directory selected.'); + return undefined; // Return undefined if no directory was selected + } + } + + async showFileInExplorer(filePath: string): Promise { + if (filePath && fs.existsSync(filePath)) { + try { + // Use Electron's shell module to show the file in the explorer + shell.showItemInFolder(filePath); + console.log(`Opened file explorer for: ${filePath}`); + } catch (error: any) { + console.error(`Error showing file in explorer: ${error.message}`); + } + } else { + console.error('File path is undefined or does not exist.'); + } + } + + // New method to open the file explorer and choose a file + async selectFile(): Promise { + const result = await dialog.showOpenDialog(this.mainWindow, { + properties: ['openFile'], // Allow selecting a file + filters: [ + { name: 'All Files', extensions: ['*'] } // Optionally filter for specific file types + ] + }); + + if (result.filePaths && result.filePaths.length > 0) { + return result.filePaths[0]; // Return the selected file path + } else { + console.log('No file selected.'); + return undefined; // Return undefined if no file was selected + } + } +} diff --git a/CEO/src/helpers/worker_manager.ts b/CEO/src/helpers/worker_manager.ts new file mode 100644 index 0000000..40950b2 --- /dev/null +++ b/CEO/src/helpers/worker_manager.ts @@ -0,0 +1,155 @@ +import { Worker } from 'worker_threads'; +import path from 'path'; +import {WindowManager} from "./window_manager"; + +export class WorkerManager { + private readonly pathToWorkerDir: string; + private windowManager: WindowManager + private workers: Worker[]; // Array to store running workers + + constructor(pathToWorkerDir: string, windowManager: WindowManager) { + this.pathToWorkerDir = pathToWorkerDir; + this.windowManager = windowManager; + this.workers = []; // Initialize the array to store workers + } + + // Start the Connection Pool Worker + async startWatchersWorker(memoryManagerPath: string, applicationInfoPath: string): Promise { + return new Promise((resolve, reject) => { + const worker = new Worker(path.join(this.pathToWorkerDir, 'watcher_worker.js'), { + workerData: { memoryManagerPath, applicationInfoPath }, // Pass the port to the worker + }); + + this.workers.push(worker); // Store the worker reference + + worker.on('message', (data) => { + console.log('Connection Pool Worker message:', data); + }); + + worker.on('error', (err) => { + console.error('Connection Pool Worker error:', err); + worker.terminate(); + this.removeWorker(worker); + reject(err); // Reject the promise if there's an error + }); + + worker.on('exit', (code) => { + console.log(`Connection Pool Worker exited with code ${code}`); + this.removeWorker(worker); // Remove worker reference when it exits + resolve(); // Resolve when the worker exits cleanly + }); + }); + } + + // Start the Servers Worker (UDP and TCP servers) + async startServersWorker(host: string, udpPort: number, tcpPort: number): Promise { + return new Promise((resolve, reject) => { + const worker = new Worker(path.join(this.pathToWorkerDir, 'servers_worker.js'), { + workerData: { HOST: host, USER_UDP_PORT: udpPort, USER_TCP_PORT: tcpPort }, // Pass host and ports to the worker + }); + + this.workers.push(worker); // Store the worker reference + + worker.on('message', (data) => { + console.log('Servers Worker message:', data); + }); + + worker.on('error', (err) => { + console.error('Servers Worker error:', err); + worker.terminate(); + this.removeWorker(worker); + reject(err); // Reject the promise if there's an error + }); + + worker.on('exit', (code) => { + console.log(`Servers Worker exited with code ${code}`); + this.removeWorker(worker); // Remove worker reference when it exits + resolve(); // Resolve when the worker exits cleanly + }); + }); + } + + // Start the Users Info Worker + async startResourceCoordinatorWorker(usersConfigPath: string, applicationInfoPath: string, memoryManagerPath: string, queueManagerPath: string, tcpPort: number): Promise { + return new Promise((resolve, reject) => { + const worker = new Worker(path.join(this.pathToWorkerDir, 'resource_coordinator_worker.js'), { + workerData: { + usersConfigPath, + applicationInfoPath, + memoryManagerPath, + queueManagerPath, + tcpPort + }, // Pass necessary parameters to the worker + }); + + this.workers.push(worker); // Store the worker reference + + worker.on('message', (data) => { + console.log('Users Info Worker message:', data); + }); + + worker.on('error', (err) => { + console.error('Users Info Worker error:', err); + worker.terminate(); + this.removeWorker(worker); + reject(err); // Reject the promise if there's an error + }); + + worker.on('exit', (code) => { + console.log(`Users Info Worker exited with code ${code}`); + this.removeWorker(worker); // Remove worker reference when it exits + resolve(); // Resolve when the worker exits cleanly + }); + }); + } + + // Start the Backup Retrieval Worker + async startBackupRetrievalWorker( + userConfigPath: string, + applicationInfoPath: string, + clientPort: number, + destinationPath: string + ): Promise { + return new Promise((resolve, reject) => { + const worker = new Worker(path.join(this.pathToWorkerDir, 'backup_retrieval_worker.js'), { + workerData: { userConfigPath, applicationInfoPath, clientPort, destinationPath }, // Pass parameters to the worker + }); + + this.workers.push(worker); // Store the worker reference + + worker.on('message', (data) => { + console.log('Backup Retrieval Worker message:', data); + this.windowManager.changeContent('main_menu'); + this.windowManager.showAlert(data.message); + }); + + worker.on('error', (err) => { + console.error('Backup Retrieval Worker error:', err); + worker.terminate(); + this.removeWorker(worker); + reject(err); // Reject the promise if there's an error + }); + + worker.on('exit', (code) => { + console.log(`Backup Retrieval Worker exited with code ${code}`); + this.removeWorker(worker); // Remove worker reference when it exits + resolve(); // Resolve when the worker exits cleanly + }); + }); + } + + // Close all running workers + closeAllWorkers(): void { + console.log('Terminating all running workers...'); + this.workers.forEach(worker => worker.terminate()); // Terminate each worker + this.workers = []; // Clear the array after terminating all workers + } + + // Helper method to remove a worker from the workers array when it exits + private removeWorker(worker: Worker): void { + const index = this.workers.indexOf(worker); + if (index > -1) { + this.workers.splice(index, 1); // Remove the worker from the array + } + } +} diff --git a/CEO/src/interfaces/file_item_task.ts b/CEO/src/interfaces/file_item_task.ts new file mode 100644 index 0000000..6934379 --- /dev/null +++ b/CEO/src/interfaces/file_item_task.ts @@ -0,0 +1,8 @@ +export interface FileItemTask { + ip: string; + path: string; + userName: string; +} + +export const compareFnFileItemTask = (task1: FileItemTask, task2: FileItemTask) => + task1.ip === task2.ip && task1.path === task2.path; diff --git a/CEO/src/interfaces/pool_request.ts b/CEO/src/interfaces/pool_request.ts new file mode 100644 index 0000000..73e9302 --- /dev/null +++ b/CEO/src/interfaces/pool_request.ts @@ -0,0 +1,15 @@ +interface PoolRequest { + type: PoolOperation; // Renamed to PoolOperation + clientId: string; // Add clientId to the request + data: PoolDataBundle; +} + +interface PoolDataBundle { + port?: number; + ip?: string; + operationCode?: string; // Keep operationCode here + metaInfo?: { [key: string]: any }; + fileContent?: Buffer; +} + +type PoolOperation = 'open' | 'send' | 'close'; // Define the allowed PoolOperations diff --git a/CEO/src/interfaces/registered_client.ts b/CEO/src/interfaces/registered_client.ts new file mode 100644 index 0000000..71ac44b --- /dev/null +++ b/CEO/src/interfaces/registered_client.ts @@ -0,0 +1,5 @@ +interface RegisteredClient { + id: string; + ip: string; + port: number; +} \ No newline at end of file diff --git a/CEO/src/main/aes_encrypt.js b/CEO/src/main/aes_encrypt.js deleted file mode 100644 index aaa5abe..0000000 --- a/CEO/src/main/aes_encrypt.js +++ /dev/null @@ -1,115 +0,0 @@ -const crypto = require('crypto'); -const fs = require('fs'); -const path = require('path'); - -async function readKeyFromFile(filePath) { - try { - await fs.promises.access(filePath, fs.constants.F_OK); - return fs.promises.readFile(filePath); // Use asynchronous readFile - } catch (error) { - console.error('Error reading key file:', error); - return null; - } -} - -// Paths to the key files remain the same -const IV_FILE_PATH = path.join(__dirname, '..', '..', 'iv.key'); -const SECRET_KEY_FILE_PATH = path.join(__dirname, '..', '..', 'secret.key'); - -async function encryptFileInPlace(filePath) { - // Await the resolution of these promises - const IV = await readKeyFromFile(IV_FILE_PATH); - const SECRET_KEY = await readKeyFromFile(SECRET_KEY_FILE_PATH); - - if (!IV || !SECRET_KEY) { - throw new Error('Failed to load IV or Secret Key'); - } - - const tempEncryptedFilePath = filePath + '.enc'; // Temporary encrypted file - return new Promise((resolve, reject) => { - const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(SECRET_KEY), IV); - const input = fs.createReadStream(filePath); - const output = fs.createWriteStream(tempEncryptedFilePath); - - input.pipe(cipher).pipe(output); - - output.on('finish', () => { - fs.rename(tempEncryptedFilePath, filePath, (err) => { - if (err) reject(err); - else resolve('File encrypted successfully and replaced original.'); - }); - }); - output.on('error', reject); - }); -} - -async function decryptFileInPlace(filePath) { - // Await the resolution of these promises - const IV = await readKeyFromFile(IV_FILE_PATH); - const SECRET_KEY = await readKeyFromFile(SECRET_KEY_FILE_PATH); - - if (!IV || !SECRET_KEY) { - throw new Error('Failed to load IV or Secret Key'); - } - - const tempDecryptedFilePath = filePath + '.dec'; // Temporary decrypted file - return new Promise((resolve, reject) => { - const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(SECRET_KEY), IV); - const input = fs.createReadStream(filePath); - const output = fs.createWriteStream(tempDecryptedFilePath); - - input.pipe(decipher).pipe(output); - - output.on('finish', () => { - fs.rename(tempDecryptedFilePath, filePath, (err) => { - if (err) reject(err); - else resolve('File decrypted successfully and replaced original.'); - }); - }); - output.on('error', reject); - }); -} - -function cryptForKey(content, key, decrypt = false) { - const algorithm = 'aes-256-ctr'; - const secretKey = crypto.createHash('sha256').update(String(key)).digest('base64').substr(0, 32); - let cipher; - - if (decrypt) { - cipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.alloc(16, 0)); // Using a zeroed IV for CTR - } else { - cipher = crypto.createCipheriv(algorithm, secretKey, Buffer.alloc(16, 0)); - } - - return Buffer.concat([cipher.update(content), cipher.final()]); -} - -// Encrypts a file in place with a given key -async function encryptFileWithKey(filePath, key) { - try { - const fileContent = await fs.promises.readFile(filePath); - const encryptedContent = cryptForKey(fileContent, key, false); - await fs.promises.writeFile(filePath, encryptedContent); - console.log(`File encrypted successfully: ${filePath}`); - } catch (error) { - console.error(`Error encrypting file: ${error.message}`); - } -} - -async function decryptFileWithKey(filePath, key) { - try { - const fileContent = await fs.promises.readFile(filePath); - const decryptedContent = cryptForKey(fileContent, key, true); - await fs.promises.writeFile(filePath, decryptedContent); - console.log(`File decrypted successfully: ${filePath}`); - } catch (error) { - console.error(`Error decrypting file: ${error.message}`); - } -} - -module.exports = { - encryptFileInPlace, - decryptFileInPlace, - encryptFileWithKey, - decryptFileWithKey, -} diff --git a/CEO/src/main/main.js b/CEO/src/main/main.js deleted file mode 100644 index 3e38464..0000000 --- a/CEO/src/main/main.js +++ /dev/null @@ -1,422 +0,0 @@ -const {app, BrowserWindow, screen, ipcMain, dialog, shell} = require('electron'); -const fs = require('fs'); -const path = require('path'); -const crypto = require('crypto'); -const {fork} = require('child_process'); - -const {encryptFileInPlace, decryptFileInPlace} = require('./aes_encrypt'); -const {lockFile, unlockFile} = require("../../helpers/lock_mechanism"); -const exec = require("nodemon/lib/config/exec"); - -const isMac = process.platform === 'darwin'; -let html_page = undefined; -let mainWindow = undefined; -let alertWindow = undefined; - -let fetcherProcess = null; -let backupProcess = null; -let externalEndpointsProcess = null; -let sendFileProcess = null; - -const createInitialKeys = () => { - const SECRET_KEY = crypto.randomBytes(32); - const IV = crypto.randomBytes(16); - - const secretKeyPath = path.join(__dirname, '..', '..', 'secret.key'); - const ivPath = path.join(__dirname, '..', '..', 'iv.key'); - - fs.writeFileSync(secretKeyPath, SECRET_KEY); - console.log(`Secret Key saved to ${secretKeyPath}`); - - fs.writeFileSync(ivPath, IV); - console.log(`IV saved to ${ivPath}`); -} - -const checkForServerConnection = async () => { - const pathToIpConfig = path.join(__dirname, '..', '..', 'ipConfig.json'); - - try { - console.log('Checking for server connection'); - await fs.promises.access(pathToIpConfig); - - await lockFile(pathToIpConfig); - await decryptFileInPlace(pathToIpConfig); - const ipConfig = await fs.promises.readFile(pathToIpConfig, 'utf-8'); - const { ip } = JSON.parse(ipConfig); - - const response = await fetch(`http://${ip}/heartbeat`); - - await encryptFileInPlace(pathToIpConfig) - await unlockFile(pathToIpConfig); - return response.ok; - } catch (error) { - console.error("Error:", error); - try{ - fs.unlinkSync(pathToIpConfig); - } - catch{ - - } - return false; - } -}; - -async function runStartupChecks() { - try { - await fs.promises.access(path.join(__dirname, '..', '..', 'secret.key'), fs.constants.F_OK); - await fs.promises.access(path.join(__dirname, '..', '..', 'iv.key'), fs.constants.F_OK); - } catch (err) { - console.error("Startup error detected:", err); - - // Run the npm clean script - await exec('npm run clean', { cwd: path.join(__dirname, '..', '..') }, (error, stdout, stderr) => { - if (error) { - console.error('Error occurred while running npm run clean:', stderr); - return; - } - console.log('npm run clean output:', stdout); - createInitialKeys(); - }); - } -} - -const createMainWindow = (async (title, width, height) => { - mainWindow = new BrowserWindow({ - title: title, - width: width, - height: height, - resizable: false, - icon: path.join(__dirname, '..', 'renderer', 'assets', 'hard-disk.png'), - webPreferences: { - preload: path.join(__dirname, 'preload.js') - } - }); - - await runStartupChecks(); - html_page = await checkForServerConnection() ? 'login.html' : 'ip_submit.html'; - - //mainWindow.setMenu(null); - mainWindow.loadFile(path.join(__dirname, '..', 'renderer', 'html', html_page)) - .then(() => { - console.log('Main window loaded!') - }) - .catch(err => console.error('Failed to load main window:', err)); -}); - -const createAlertWindow = (title, width, height) => { - alertWindow = new BrowserWindow({ - width: width, - height: height, - title: title, - icon: path.join(__dirname, '..', 'renderer', 'assets', 'hard-disk.png'), - resizable: false, - webPreferences: { - nodeIntegration: false, - preload: path.join(__dirname, 'preload.js') - } - }); - - //alertWindow.setMenu(null); - - alertWindow.loadFile(path.join(__dirname, '..', 'renderer', 'html', 'alert_modal.html')).then(() => { - console.log('Alert window loaded!') - }) - .catch(err => console.error('Failed to load alert window:', err)); - - alertWindow.on('closed', () => { - alertWindow = undefined; - }); -} - -function showAlert(message) { - if (alertWindow === undefined) { - const title = 'Alert'; - const mainScreen = screen.getPrimaryDisplay(); - const {width, height} = mainScreen.size; - createAlertWindow(title, width / 4, height / 4); - } - - alertWindow.webContents.once('dom-ready', () => { - alertWindow.webContents.executeJavaScript(`showAlert("${message}")`); - }); -} - -app.whenReady().then(() => { - const title = "Application"; - const mainScreen = screen.getPrimaryDisplay(); - const {width, height} = mainScreen.size; - createMainWindow(title, width / 1.5, height / 1.5); - - app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0) { - createMainWindow(title, width, height); - } - }); -}); - -app.on('before-quit', () => { - if (backupProcess !== null) { - backupProcess.kill(); - } - if (externalEndpointsProcess !== null) { - externalEndpointsProcess.kill(); - } - if (fetcherProcess !== null) { - fetcherProcess.kill(); - } - - if (sendFileProcess !== null) { - sendFileProcess.kill(); - } -}); - -app.on('window-all-closed', () => { - if (!isMac) { - app.quit(); - } -}); - -ipcMain.handle('write-file', async (event, fileName, content) => { - try { - let filePath = path.join(__dirname, '..', '..', fileName); - await lockFile(filePath); - await fs.promises.writeFile(filePath, content); - await encryptFileInPlace(filePath); - await unlockFile(filePath) - console.log(`File successfully written to ${filePath}`); - return {success: true}; - } catch (error) { - console.error('Failed to write file:', error); - return {success: false, error: error.message}; - } -}); - -ipcMain.handle('delete-file', async (event, fileName) => { - try { - let filePath = path.join(__dirname, '..', '..', fileName); - - await fs.promises.unlink(filePath); - - console.log(`File ${filePath} successfully deleted`); - return {success: true}; - } catch (error) { - console.error('Failed to delete file:', error); - return {success: false, error: error.message}; - } -}); - -ipcMain.handle('read-file', async (event, fileName) => { - try { - let filePath = path.join(__dirname, '..', '..', fileName); - await lockFile(filePath); - await decryptFileInPlace(filePath); - const content = await fs.promises.readFile(filePath, 'utf-8'); - - await encryptFileInPlace(filePath); - await unlockFile(filePath); - return {success: true, content}; - } catch (error) { - console.error('Error reading file:', error); - return {success: false, error: error.message}; - } -}); - -ipcMain.handle('change-content', async (event, nextPage) => { - try { - html_page = nextPage; - await mainWindow.loadFile(path.join(__dirname, '..', 'renderer', 'html', nextPage)); - return true; - } catch (error) { - console.error('Error changing content:', error); - return false; - } -}); - -ipcMain.handle('open-dir-dialog', async (event) => { - try { - const result = await dialog.showOpenDialog({ - properties: ['openDirectory'], - }); - - if (result.canceled || result.filePaths.length === 0) { - return {canceled: true} - } - - return result.filePaths[0]; - } catch (error) { - console.error('Error opening file dialog:', error); - return null; - } -}) - -ipcMain.handle('open-json-dir-config', async (event, fileName) => { - try { - const result = await dialog.showOpenDialog({ - properties: ['openDirectory'], - }); - - if (result.canceled || result.filePaths.length === 0) { - return {canceled: true} - } - - const dirPath = result.filePaths[0]; - await fs.promises.writeFile( - path.join(__dirname, '..', '..', fileName), - JSON.stringify({ - path: dirPath - }, null, 2)); - - return true; - } catch (error) { - console.error('Error opening file dialog:', error); - return {error: error.message}; - } -}); - -ipcMain.handle('open-file-dialog', async (event) => { - const result = await dialog.showOpenDialog({ - properties: ['openFile'] - }); - - return result.filePaths[0] || ''; -}); - -ipcMain.handle('check-file-exists', async (event, fileName) => { - try { - const filePath = path.join(__dirname, '..', '..', fileName); - return await fs.promises.access(filePath) - .then(() => true) - .catch(() => false); - } catch (error) { - console.error('Error checking file existence:', error); - throw error; // Propagate the error to the renderer process - } -}); - -ipcMain.handle('show-alert', async (event, message) => { - showAlert(message); -}); - -ipcMain.on('close-alert-window', () => { - if (alertWindow) { - alertWindow.close(); - alertWindow = undefined; - } -}); - -//External processes -ipcMain.handle('start-main-processes', async (event, args) => { - if (!backupProcess) { - backupProcess = fork(path.join(__dirname, '..', '..', 'jobs', 'backup.js'), args, { silent: false }); - backupProcess.on('exit', () => { - backupProcess = null; - }); - backupProcess.on('error', (err) => { - console.log('Backup process error:', err); - }); - } - - if (!fetcherProcess) { - fetcherProcess = fork(path.join(__dirname, '..', '..', 'jobs', 'fetcher.js'), args, { silent: false }); - fetcherProcess.on('exit', () => { - fetcherProcess = null; - }); - fetcherProcess.on('error', (err) => { - console.log('Fetcher process error:', err); - }); - - fetcherProcess.on('message', (message) => { - if (message.type === 'startBackup') { - backupProcess.send({ - type: 'startBackup' - }); - } - }); - } - - if (!externalEndpointsProcess) { - externalEndpointsProcess = fork(path.join(__dirname, '..', '..', 'jobs', 'external_endpoints.js'), args, { silent: false }); - externalEndpointsProcess.on('exit', () => { - externalEndpointsProcess = null; - }); - externalEndpointsProcess.on('error', (err) => { - console.log('External endpoints process error:', err); - }); - } - - return true; // Indicate that the operation has started -}); - -ipcMain.handle('start-send-file-process', async (event, args) => { - if (sendFileProcess === null) { - sendFileProcess = fork(path.join(__dirname, '..', '..', 'jobs', 'send_file.js'), args, {silent: false}); - sendFileProcess.on('exit', () => { - sendFileProcess = null; - }); - } - return true; // Indicate that the operation has started -}); - -ipcMain.handle('kill-before-logout', async(event) =>{ - if (backupProcess !== null) { - backupProcess.kill('SIGINT'); - } - if (externalEndpointsProcess !== null) { - externalEndpointsProcess.kill('SIGINT'); - } - if (fetcherProcess !== null) { - fetcherProcess.kill('SIGINT'); - } - - if (sendFileProcess !== null) { - sendFileProcess.kill('SIGINT'); - } -}) - -ipcMain.handle('decrypt-backup-files', async(event, destPath) => { - console.log('ai intrat in handle') - if(backupProcess != null){ - console.log('esti in process'); - backupProcess.send({ - type: 'decryptBackup', - decryptDestPath: destPath - }); - } -}) - -// Handle showing a file in the system file explorer -ipcMain.handle('show-file-in-explorer', async (event, filePath) => { - try { - // Ensure the file exists before attempting to show it - await fs.promises.access(filePath, fs.constants.F_OK); - shell.showItemInFolder(filePath); // Opens the file explorer and highlights the file - return true; - } catch (error) { - console.error('File does not exist:', error); - return false - } -}); - -// Handle removing a path from 'filesReceived.json' -ipcMain.handle('remove-path-from-received-files', async (event, filePath) => { - try { - const jsonFilePath = path.join(__dirname, '..', '..', 'filesReceived.json'); - await lockFile(jsonFilePath); - await decryptFileInPlace(jsonFilePath); - const data = await fs.promises.readFile(jsonFilePath, 'utf8'); - - const jsonData = JSON.parse(data); - - // Filter out the specified file path - jsonData.receivedFiles = jsonData.receivedFiles.filter(file => file !== filePath); - - await fs.promises.writeFile(jsonFilePath, JSON.stringify(jsonData, null, 2), 'utf8'); - await encryptFileInPlace(jsonFilePath); - await unlockFile(jsonFilePath); - - return true; - } catch (error) { - console.error('Error updating filesReceived.json:', error); - throw new Error('Failed to update received files list.'); - } -}); diff --git a/CEO/src/main/main.ts b/CEO/src/main/main.ts new file mode 100644 index 0000000..4184506 --- /dev/null +++ b/CEO/src/main/main.ts @@ -0,0 +1,299 @@ +import {app, BrowserWindow, ipcMain, IpcMainInvokeEvent} from 'electron'; +import path from 'path'; +import { promises as fs } from 'fs'; +import dotenv from 'dotenv'; + +import {WorkerManager} from "../helpers/worker_manager"; +import {DirectoryWatcher} from "../helpers/directory_watcher"; +import {QueueManager} from "../helpers/queue_manager"; +import {compareFnFileItemTask, FileItemTask} from "../interfaces/file_item_task"; +import {TaskScheduler} from "../helpers/task_scheduler"; +import {WindowManager} from "../helpers/window_manager"; +import {JsonManager} from "../helpers/json_manager"; +import {MemoryManager} from "../helpers/memory_manager"; +import {TcpCommunicator} from "../helpers/tcp_communicator"; + +import {operationCodes} from "../network/operation_codes"; + +// Load environment variables +dotenv.config({ path: path.join(__dirname, '..', '..', '.env') }); + +const UDP_PORT = process.env.UDP_PORT ? parseInt(process.env.UDP_PORT) : 41233; +const TCP_PORT = process.env.TCP_PORT ? parseInt(process.env.TCP_PORT) : 41234; +const HOST = process.env.HOST || '0.0.0.0'; + +let mainWindow: BrowserWindow | null = null; +let windowManager: WindowManager | null = null; +let tcpCommunicator: TcpCommunicator | null = null; +let userConfig: JsonManager | null = null; +let applicationInfo: JsonManager | null = null; +let memoryManager: MemoryManager | null = null; +let workerManager: WorkerManager | null = null; +let taskScheduler: TaskScheduler | null = null; +let backupDirectoryManager: DirectoryWatcher | null = null; +let departmentShareManager: DirectoryWatcher | null = null; +let sendFileQueue: QueueManager | null = null; + +const pathToPagesDir = path.join(__dirname, '..', '..', 'render', 'html'); +const pathToWorkerDir = path.join(__dirname, '..', 'workers'); +const pathToJsons = path.join(__dirname, '..', 'json_files'); +const pathToClientsBackups = path.join(__dirname, '..', 'backups'); + +async function cleanupAndExit() { + // Stop all workers + if (workerManager) { + console.log('Terminating all workers...'); + workerManager.closeAllWorkers(); + } + + // Reset memory + if (memoryManager) { + await memoryManager.resetFile(); + } + + // Close watchers + if (backupDirectoryManager) { + console.log('Stopping backup directory watcher...'); + backupDirectoryManager.closeWatcher(); // Add this method to DirectoryWatcher to close the watcher + } + + if (departmentShareManager) { + console.log('Stopping department directory watcher...'); + departmentShareManager.closeWatcher(); // Add this method to DirectoryWatcher to close the watcher + } + + if(taskScheduler){ + console.log('Stopping all tasks...'); + taskScheduler.stopAllTasks() + } + + if(workerManager){ + console.log('Terminating all workers...'); + workerManager.closeAllWorkers() + } + + console.log('Cleanup complete, exiting application.'); + app.quit(); // This will properly close the application +} + +async function ensureDirectoryExists(dirPath: string): Promise { + try { + await fs.access(dirPath); + } catch (err) { + // If the directory doesn't exist, create it + await fs.mkdir(dirPath, { recursive: true }); + console.log(`Directory created: ${dirPath}`); + } +} + +app.whenReady().then(async () => { + const title = 'Application'; + const mainScreen = require('electron').screen.getPrimaryDisplay(); + const { width, height } = mainScreen.size; + + mainWindow = new BrowserWindow({ + title, + width: width / 1.5, + height: height / 1.5, + resizable: false, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + nodeIntegration: false, + contextIsolation: true, + }, + }); + + await ensureDirectoryExists(pathToJsons); + await ensureDirectoryExists(pathToClientsBackups); + + windowManager = new WindowManager(mainWindow, pathToPagesDir); + userConfig = new JsonManager(path.join(pathToJsons, 'userConfig.json')); + applicationInfo = new JsonManager(path.join(pathToJsons, 'application.json')); + memoryManager = new MemoryManager(path.join(pathToJsons, 'memory.json')); + sendFileQueue = new QueueManager(path.join(pathToJsons, 'sendFileTasks.json'), compareFnFileItemTask); + + taskScheduler = new TaskScheduler(applicationInfo, windowManager); + workerManager = new WorkerManager(pathToWorkerDir, windowManager); + + await userConfig.writeValue('app_type', 'ceo'); + await applicationInfo.writeValue('users_ip', []); + await memoryManager.resetFile(); + + workerManager.startWatchersWorker(path.join(pathToJsons, 'memory.json'), path.join(pathToJsons, 'application.json')); + workerManager.startServersWorker(HOST, UDP_PORT, TCP_PORT); + workerManager.startResourceCoordinatorWorker( + path.join(pathToJsons, 'userConfig.json'), + path.join(pathToJsons, 'application.json'), + path.join(pathToJsons, 'memory.json'), + path.join(pathToJsons, 'sendFileTasks.json'), + TCP_PORT + ); + + + taskScheduler.startUCCheck(UDP_PORT, 'login', 'uc_not_found'); + taskScheduler.startUserIPLookup(UDP_PORT); + + registerIPCHandlers(); + + await windowManager.changeContent('welcome'); +}); + +app.on('window-all-closed', async () => { + console.log('All windows closed, starting cleanup...'); + await cleanupAndExit(); // Call cleanup when all windows are closed +}); + +// Catch CTRL+C (SIGINT) and clean up resources +process.on('SIGINT', async () => { + console.log('CTRL+C pressed, starting cleanup...'); + await cleanupAndExit(); // Call cleanup on SIGINT +}); + +app.on('before-quit', async () => { + console.log('Application is quitting, starting cleanup...'); + await cleanupAndExit(); // Call cleanup before app quit +}); + +// Register IPC handlers +function registerIPCHandlers() { + // Window Manager IPC Handlers + ipcMain.handle('show-alert', async (event: IpcMainInvokeEvent, message: string) => { + if (!windowManager) throw new Error('WindowManager is not initialized.'); + await windowManager.showAlert(message); + }); + + ipcMain.handle('change-content', async (_event: IpcMainInvokeEvent, destination: string) => { + if (!windowManager) throw new Error('WindowManager is not initialized.'); + await windowManager.changeContent(destination); + }); + + ipcMain.handle('select-directory', async (_event: IpcMainInvokeEvent) => { + if (!windowManager) throw new Error('WindowManager is not initialized.'); + return await windowManager.selectDirectory(); + }); + + ipcMain.handle('select-file', async (_event: IpcMainInvokeEvent) => { + if (!windowManager) throw new Error('WindowManager is not initialized.'); + return await windowManager.selectFile(); + }); + + ipcMain.handle('show-file-in-explorer', async (_event: IpcMainInvokeEvent, path: string) => { + if (!windowManager) throw new Error('WindowManager is not initialized.'); + return await windowManager.showFileInExplorer(path); + }); + + // TcpMethods IPC Handlers + ipcMain.handle('open-socket', async (_event: IpcMainInvokeEvent) => { + if (!applicationInfo) throw new Error('TcpMethods is not initialized.'); + + const serverIp = await applicationInfo.readValue('serverIp'); + if (!serverIp) return; + + tcpCommunicator = new TcpCommunicator(serverIp, TCP_PORT); + return await tcpCommunicator.connect() + }); + + ipcMain.handle('send-message', async (_event: IpcMainInvokeEvent, operationCode: string, metaInfo?: { [key: string]: any }, fileContent?: Buffer) => { + if (!tcpCommunicator) throw new Error('TcpMethods is not initialized.'); + return await tcpCommunicator.sendMessage(operationCode, metaInfo, fileContent); + }); + + ipcMain.handle('has-response-arrived', async (_event: IpcMainInvokeEvent) => { + if (!tcpCommunicator) throw new Error('TcpMethods is not initialized.'); + return tcpCommunicator.hasResponseArrived(); + }); + + ipcMain.handle('close-socket', async (_event: IpcMainInvokeEvent) => { + if (!tcpCommunicator) throw new Error('TcpMethods is not initialized.'); + return await tcpCommunicator.disconnect(); + }); + + ipcMain.handle('get-last-result', async (_event: IpcMainInvokeEvent) => { + if (!tcpCommunicator) throw new Error('TcpMethods is not initialized.'); + return tcpCommunicator.getLastResult(); + }); + + ipcMain.handle('get-operation-codes', () => { + return operationCodes; + }); + + // UserConfig IPC Handlers + ipcMain.handle('read-user-json-files', async (_event: IpcMainInvokeEvent, key: string) => { + if (!userConfig) throw new Error('UserConfig is not initialized.'); + return await userConfig.readValue(key); + }); + + ipcMain.handle('write-user-json-files', async (_event: IpcMainInvokeEvent, key: string, value: any) => { + if (!userConfig) throw new Error('UserConfig is not initialized.'); + return userConfig.writeValue(key, value); + }); + + ipcMain.handle('reset-user-json-files', async () => { + if (!userConfig) throw new Error('UserConfig is not initialized.'); + return userConfig.resetFile(); + }); + + ipcMain.handle('remove-user-json-files-key', async (_event: IpcMainInvokeEvent, key: string) => { + if (!userConfig) throw new Error('UserConfig is not initialized.'); + return userConfig.removeValue(key); + }); + + // ApplicationPreferences IPC Handlers + ipcMain.handle('read-application-json-files', async (_event: IpcMainInvokeEvent, key: string) => { + if (!applicationInfo) throw new Error('ApplicationInfo is not initialized.'); + return await applicationInfo.readValue(key); + }); + + ipcMain.handle('write-application-json-files', async (_event: IpcMainInvokeEvent, key: string, value: any) => { + if (!applicationInfo) throw new Error('ApplicationInfo is not initialized.'); + return applicationInfo.writeValue(key, value); + }); + + ipcMain.handle('reset-application-json-files', async () => { + if (!applicationInfo) throw new Error('ApplicationInfo is not initialized.'); + return applicationInfo.resetFile(); + }); + + ipcMain.handle('remove-application-json-files-key', async (_event: IpcMainInvokeEvent, key: string) => { + if (!applicationInfo) throw new Error('ApplicationInfo is not initialized.'); + return applicationInfo.removeValue(key); + }); + + // Memory IPC Handlers + ipcMain.handle('memory-create-entry', async () => { + if (!memoryManager) throw new Error('MemoryManager is not initialized.'); + return memoryManager.storeMetaInformation({}); + }); + + ipcMain.handle('memory-read-entry', async (_event: IpcMainInvokeEvent, id: string) => { + if (!memoryManager) throw new Error('MemoryManager is not initialized.'); + return memoryManager.retrieveMetaInformation(id); + }); + + ipcMain.handle('memory-update-entry', async (_event: IpcMainInvokeEvent, id: string, data: any) => { + if (!memoryManager) throw new Error('MemoryManager is not initialized.'); + return memoryManager.updateMetaInformation(id, data); + }); + + ipcMain.handle('memory-remove-entry', async (_event: IpcMainInvokeEvent, id: string) => { + if (!memoryManager) throw new Error('MemoryManager is not initialized.'); + return memoryManager.removeMetaInformation(id); + }); + + // Queue IPC Handlers + ipcMain.handle('add-task-to-send-file-queue', async (event: IpcMainInvokeEvent, task: FileItemTask) => { + if (!sendFileQueue) throw new Error('SendFileQueue is not initialized.'); + sendFileQueue.enqueue(task); + }); + + // BackupRetrievalWorker IPC Handler + ipcMain.handle('start-backup-retrieval', async (_event: IpcMainInvokeEvent, destinationPath: string) => { + if (!workerManager) throw new Error('WorkerManager is not initialized.'); + return workerManager.startBackupRetrievalWorker( + path.join(pathToJsons, 'userConfig.json'), + path.join(pathToJsons, 'application.json'), + TCP_PORT, + destinationPath + ); + }); +} \ No newline at end of file diff --git a/CEO/src/main/preload.js b/CEO/src/main/preload.js deleted file mode 100644 index a1ac5ca..0000000 --- a/CEO/src/main/preload.js +++ /dev/null @@ -1,21 +0,0 @@ -const {contextBridge, ipcRenderer} = require('electron'); - -contextBridge.exposeInMainWorld('electronAPI', { - writeFile: (fileName, content) => ipcRenderer.invoke('write-file', fileName, content), - readFile: (fileName) => ipcRenderer.invoke('read-file', fileName), - deleteFile: (fileName) => ipcRenderer.invoke('delete-file', fileName), - changeContent: (nextPage) => ipcRenderer.invoke('change-content', nextPage), - showAlert: (message) => ipcRenderer.invoke('show-alert', message), - closeAlertWindow: () => ipcRenderer.send('close-alert-window'), - openJsonDirConfigDialog: (fileName) => ipcRenderer.invoke('open-json-dir-config', fileName), - openDirDialog: () => ipcRenderer.invoke('open-dir-dialog'), - openFileDialog: () => ipcRenderer.invoke('open-file-dialog'), - checkFileExists: (fileName) => ipcRenderer.invoke('check-file-exists', fileName), - - startMainProcesses: async (args) => ipcRenderer.invoke('start-main-processes', args), - killBeforeLogout: async() => ipcRenderer.invoke('kill-before-logout'), - decryptFiles: async(destPath) => ipcRenderer.invoke('decrypt-backup-files', destPath), - - showFileInExplorer: (filePath) => ipcRenderer.invoke('show-file-in-explorer', filePath), - removePathFromReceivedFiles: (filePath) => ipcRenderer.invoke('remove-path-from-received-files', filePath) -}); \ No newline at end of file diff --git a/CEO/src/main/preload.ts b/CEO/src/main/preload.ts new file mode 100644 index 0000000..0aa21cc --- /dev/null +++ b/CEO/src/main/preload.ts @@ -0,0 +1,43 @@ +import { contextBridge, ipcRenderer } from 'electron'; +import {FileItemTask} from "../interfaces/file_item_task"; + +contextBridge.exposeInMainWorld('electronAPI', { + // UserConfig methods + readUserConfig: (key: string): Promise => ipcRenderer.invoke('read-user-json-files', key), + writeUserConfig: (key: string, value: any): Promise => ipcRenderer.invoke('write-user-json-files', key, value), + removeUserConfig: (key: string) : Promise => ipcRenderer.invoke('remove-user-json-files', key), + resetUserConfig: (): Promise => ipcRenderer.invoke('reset-user-json-files'), + + // ApplicationPreferences methods + readApplicationInfo: (key: string): Promise => ipcRenderer.invoke('read-application-json-files', key), + writeApplicationInfo: (key: string, value: any): Promise => ipcRenderer.invoke('write-application-json-files', key, value), + removeApplicationInfo: (key: string) : Promise => ipcRenderer.invoke('remove-application-preferences', key), + resetApplicationInfo: (): Promise => ipcRenderer.invoke('reset-application-json-files'), + + // UcCommunication methods + openUcSocket: (): Promise => ipcRenderer.invoke('open-socket'), + sendUcMessage: (operationCode: string, metaInfo: any, fileContent: any): Promise => ipcRenderer.invoke('send-message', operationCode, metaInfo, fileContent), + closeUcSocket: (): Promise => ipcRenderer.invoke('close-socket'), + hasResponseArrived: (): Promise => ipcRenderer.invoke('has-response-arrived'), + getLastUcResult: (): Promise => ipcRenderer.invoke('get-last-result'), + getOperationsCodes: (): Promise<{ data: { [key: string]: string } }> => ipcRenderer.invoke('get-operation-codes'), + + // MemoryManager methods + createMemoryEntry: (): Promise => ipcRenderer.invoke('memory-create-entry'), + readMemoryEntry: (id: string): Promise => ipcRenderer.invoke('memory-read-entry', id), + updateMemoryEntry: (id: string, data: any): Promise => ipcRenderer.invoke('memory-update-entry', id, data), + removeMemoryEntry: (id: string): Promise => ipcRenderer.invoke('memory-remove-entry', id), + + // UI methods + showAlert: (message: string): Promise => ipcRenderer.invoke('show-alert', message), + changeContent: (destination: string): Promise => ipcRenderer.invoke('change-content', destination), + selectDirectory: (): Promise => ipcRenderer.invoke('select-directory'), + selectFile: (): Promise => ipcRenderer.invoke('select-file'), + showFileInExplorer: (path: string): Promise => ipcRenderer.invoke('show-file-in-explorer', path), + + // Queue methods + addTaskToSendFileQueue: (task: FileItemTask): Promise => ipcRenderer.invoke('add-task-to-send-file-queue', task), + + // BackupRetrievalWorker + startBackupRetrieval: (destinationPath: string): Promise => ipcRenderer.invoke('start-backup-retrieval', destinationPath) +}); diff --git a/CEO/src/network/connection_manager.ts b/CEO/src/network/connection_manager.ts new file mode 100644 index 0000000..f7f29f8 --- /dev/null +++ b/CEO/src/network/connection_manager.ts @@ -0,0 +1,46 @@ +import { SocketCommunicatorBase } from "./socket_communicator/socket_communicator_base"; + +interface Connection { + communicator: SocketCommunicatorBase; +} + +export class ConnectionManager { + private readonly connections: { [key: string]: Connection }; + + constructor() { + this.connections = {}; + } + + // Adds a new communicator, keyed by both IP and port + addConnection(ip: string, port: number, communicator: SocketCommunicatorBase): void { + const key = `${ip}:${port}`; + + // Store the communicator along with the client's public and private keys + this.connections[key] = { + communicator + }; + + console.log(`Added communicator for ${ip}:${port}, generated public and private keys.`); + } + + // Removes a communicator based on IP and port + removeCommunicator(ip: string, port: number): void { + const key = `${ip}:${port}`; + if (this.connections[key]) { + delete this.connections[key]; + console.log(`Removed communicator for ${ip}:${port}`); + } + } + + // Retrieves a communicator based on IP and port + getCommunicator(ip: string, port: number): SocketCommunicatorBase | null { + const key = `${ip}:${port}`; + return this.connections[key] ? this.connections[key].communicator : null; + } + + // Checks if a communicator exists for a given IP and port + communicatorExists(ip: string, port: number): boolean { + const key = `${ip}:${port}`; + return this.connections[key] !== undefined; + } +} diff --git a/CEO/src/network/message_handler.ts b/CEO/src/network/message_handler.ts new file mode 100644 index 0000000..6583a1d --- /dev/null +++ b/CEO/src/network/message_handler.ts @@ -0,0 +1,65 @@ +export interface ParsedMessage { + operationCode: string; + metaInfo?: { [key: string]: any }; + fileContent?: Buffer; +} + +export class MessageHandler { + // Format the message with operationCode, guid, metaInfo, and fileContent (Base64 for fileContent) + static formatMessage( + operationCode: string, + metaInfo?: { [key: string]: any }, + fileContent?: Buffer + ): string { + let message = `${operationCode}\n`; // First part: operationCode and guid + + if (metaInfo && Object.keys(metaInfo).length > 0) { + message += `${JSON.stringify(metaInfo)}\n`; // Add metaInfo + } + + if (fileContent && fileContent.length > 0) { + message += fileContent.toString('base64'); // Convert buffer to Base64 for fileContent + } + + return message; + } + + // Parse the incoming message (convert Base64 back to Buffer if fileContent is present) + static parseMessage(msg: string): ParsedMessage { + const parts = msg.split('\n'); // Split by \n (operationCode, metaInfo, and fileContent are on separate lines) + + // First part should always be the operation code + const operationCode = parts[0]?.trim(); + if (!operationCode) { + throw new Error('Missing operation code in the message'); + } + + let metaInfo: { [key: string]: any } | undefined = undefined; + let fileContent: Buffer | undefined = undefined; + + // Parse the metaInfo (JSON object) if present + if (parts[1]) { + try { + metaInfo = JSON.parse(parts[1].trim()); + } catch (err) { + console.error('Invalid metaInfo JSON format:', err); + } + } + + // Convert Base64 string back to Buffer for fileContent if present + if (parts[2]) { + fileContent = Buffer.from(parts[2].trim(), 'base64'); + } + + return { + operationCode, + metaInfo, + fileContent, + }; + } + + // Validate if the parsed message contains an operation code + static validateMessage(parsedMessage: ParsedMessage | null): boolean { + return !!parsedMessage?.operationCode; + } +} \ No newline at end of file diff --git a/CEO/src/network/network.ts b/CEO/src/network/network.ts new file mode 100644 index 0000000..8f65956 --- /dev/null +++ b/CEO/src/network/network.ts @@ -0,0 +1,2 @@ +export { UdpClient} from './udp/udp_client'; +export { TcpClient } from './tcp/tcp_client' \ No newline at end of file diff --git a/CEO/src/network/operation_codes.ts b/CEO/src/network/operation_codes.ts new file mode 100644 index 0000000..7e6835e --- /dev/null +++ b/CEO/src/network/operation_codes.ts @@ -0,0 +1,37 @@ +export let operationCodes = { + HEARTBEAT: 'HEARTBEAT', + ALIVE: 'ALIVE', + SET_PUBLIC_KEY: 'SET_PUBLIC_KEY', + SET_AES_KEY: 'SET_AES_KEY', + RESET_DATABASE: 'RESET_DATABASE', + OK: 'OK', + ERR: 'ERR', + END: 'END', + UNKNOWN_COMMAND: 'UNKNOWN_COMMAND', + + LOGIN: 'LOGIN', + SIGN_UP: 'SIGN_UP', + RESET_PASSWORD: 'RESET_PASSWORD', + + GET_DEPARTMENTS: 'GET_DEPARTMENTS', + CREATE_DEPARTMENT: 'CREATE_DEPARTMENT', + MODIFY_DEPARTMENT: 'MODIFY_DEPARTMENT', + DELETE_DEPARTMENT: 'DELETE_DEPARTMENT', + + GET_USERS: 'GET_USERS', + DELETE_USER: 'DELETE_USER', + FIND_KEY_BY_USER_ID: 'FIND_KEY_BY_USER_ID', + FIND_BY_EMAIL: 'FIND_BY_EMAIL', + MODIFY_USER: 'MODIFY_USER', + + GET_USER_INFORMATION: 'GET_USER_INFORMATION', + CLEAR_BACKUP: 'CLEAR_BACKUP', + BACKUP_FILE: 'BACKUP_FILE', + SHARE_FILE: 'SHARE_FILE', + CLEAR_DEPARTMENT: 'CLEAR_DEPARTMENT', + DEPARTMENT_FILE: 'DEPARTMENT_FILE', + IS_BACKUP_CREATED: 'IS_BACKUP_CREATED', + GET_BACKUP_STRUCTURE: 'GET_BACKUP_STRUCTURE', + REQ_FILE_FROM_BACKUP: 'REQ_FILE_FROM_BACKUP', + DECRYPT_AND_SAVE_OLD_BACKUP_FILE: 'DECRYPT_AND_SAVE_OLD_BACKUP_FILE', +}; \ No newline at end of file diff --git a/CEO/src/network/operations_base/operation_base.ts b/CEO/src/network/operations_base/operation_base.ts new file mode 100644 index 0000000..d0d62da --- /dev/null +++ b/CEO/src/network/operations_base/operation_base.ts @@ -0,0 +1,43 @@ +import { ParsedMessage } from '../message_handler'; +import { OperationHandler } from './operation_handler'; +import { OperationPlugin } from './operation_plugin'; + +export abstract class OperationBase implements OperationPlugin { + // Shared operation codes + public static readonly operationCodes = { + OK: 'OK', + ERR: 'ERR', + END: 'END', + }; + + // Default handler for OK operation + public static handleOk(parsedMessage: ParsedMessage): ParsedMessage { + console.log('OK operation received'); + return parsedMessage; // Typically, you would just acknowledge with OK, returning as-is + } + + // Default handler for ERR operation + public static handleErr(parsedMessage: ParsedMessage): ParsedMessage { + console.log('ERR operation received: ', parsedMessage.metaInfo?.message || 'No error details provided'); + return parsedMessage; // Typically, you would log the error and return + } + + // Default handler for END operation + public static handleEnd(parsedMessage: ParsedMessage): ParsedMessage { + console.log('END operation received'); + return { + operationCode: OperationBase.operationCodes.END, + metaInfo: { message: 'Connection ended.' }, + }; + } + + // Register the common OK, ERR, and END handlers + public static registerCommonOperations(operationHandler: OperationHandler): void { + operationHandler.registerHandler(OperationBase.operationCodes.OK, OperationBase.handleOk); + operationHandler.registerHandler(OperationBase.operationCodes.ERR, OperationBase.handleErr); + operationHandler.registerHandler(OperationBase.operationCodes.END, OperationBase.handleEnd); + } + + // Abstract register method that will be implemented by subclasses + public abstract register(operationHandler: OperationHandler): void; +} diff --git a/CEO/src/network/operations_base/operation_handler.ts b/CEO/src/network/operations_base/operation_handler.ts new file mode 100644 index 0000000..76c00ce --- /dev/null +++ b/CEO/src/network/operations_base/operation_handler.ts @@ -0,0 +1,58 @@ +// operation_handler.ts +import { ParsedMessage, MessageHandler } from '../message_handler'; +import { OperationPlugin } from './operation_plugin'; + +type OperationHandlerFunction = (parsedMessage: ParsedMessage) => ParsedMessage; + +export class OperationHandler { + private static instance: OperationHandler; + private handlers: { [operationCode: string]: OperationHandlerFunction } = {}; + + private constructor() { + // Register only the unknown command handler on initialization + this.registerHandler('UNKNOWN_COMMAND', this.handleUnknownCommand); + } + + // Singleton instance + public static getInstance(): OperationHandler { + if (!OperationHandler.instance) { + OperationHandler.instance = new OperationHandler(); + } + return OperationHandler.instance; + } + + // Register a handler for a specific operation code + public registerHandler(operationCode: string, handler: OperationHandlerFunction): void { + this.handlers[operationCode] = handler; + } + + // Handle operation request + public handleOperation(rawMessage: string): ParsedMessage { + // Parse and validate the message + const parsedMessage = MessageHandler.parseMessage(rawMessage); + if (!parsedMessage || !MessageHandler.validateMessage(parsedMessage)) { + return this.handleUnknownCommand(parsedMessage); + } + + // Dispatch the handler for the given operation code + const handler = this.handlers[parsedMessage.operationCode]; + if (handler) { + return handler(parsedMessage); + } else { + return this.handleUnknownCommand(parsedMessage); + } + } + + // Default handler for unknown commands + private handleUnknownCommand(parsedMessage: ParsedMessage): ParsedMessage { + return { + operationCode: 'UNKNOWN_COMMAND', + metaInfo: { message: 'Unknown command received.' }, + }; + } + + // Plugin system: Load plugins to register handlers + public loadPlugin(plugin: OperationPlugin): void { + plugin.register(this); + } +} diff --git a/CEO/src/network/operations_base/operation_plugin.ts b/CEO/src/network/operations_base/operation_plugin.ts new file mode 100644 index 0000000..6b11a29 --- /dev/null +++ b/CEO/src/network/operations_base/operation_plugin.ts @@ -0,0 +1,6 @@ +// operation_plugin.ts +import { OperationHandler } from './operation_handler'; + +export interface OperationPlugin { + register(operationHandler: OperationHandler): void; +} diff --git a/CEO/src/network/operations_custom/general_operations.ts b/CEO/src/network/operations_custom/general_operations.ts new file mode 100644 index 0000000..d9d83a2 --- /dev/null +++ b/CEO/src/network/operations_custom/general_operations.ts @@ -0,0 +1,80 @@ +import { ParsedMessage } from '../message_handler'; +import { OperationBase } from '../operations_base/operation_base'; +import { OperationHandler } from '../operations_base/operation_handler'; +import os from 'node:os'; + +export class GeneralOperations extends OperationBase { + public static readonly operationCodes = { + ...OperationBase.operationCodes, // Inherit common operation codes (OK, ERR, END) + HEARTBEAT: 'HEARTBEAT', + ALIVE: 'ALIVE', + SET_PUBLIC_KEY: 'SET_PUBLIC_KEY', + SET_AES_KEY: 'SET_AES_KEY', + }; + + // Handle heartbeat operation + public static handleHeartbeat(): ParsedMessage { + const networkInterfaces = os.networkInterfaces(); + let ipAddress = 'Unknown'; + + for (const iface of Object.values(networkInterfaces)) { + // @ts-ignore + for (const address of iface) { + if (address.family === 'IPv4' && !address.internal) { + ipAddress = address.address; + break; + } + } + if (ipAddress !== 'Unknown') break; + } + + return { + operationCode: GeneralOperations.operationCodes.ALIVE, + metaInfo: { ipAddress }, + }; + } + + // Handle public key exchange + public static handlePublicKey(parsedMessage: ParsedMessage): ParsedMessage { + const clientPublicKey = parsedMessage.metaInfo?.publicKey; + if (clientPublicKey) { + return { + operationCode: GeneralOperations.operationCodes.SET_PUBLIC_KEY, + metaInfo: { publicKey: clientPublicKey }, + }; + } else { + return { + operationCode: GeneralOperations.operationCodes.ERR, + metaInfo: { message: 'No public key provided.' }, + }; + } + } + + // Handle AES key exchange + public static handleAESKey(parsedMessage: ParsedMessage): ParsedMessage { + const aesKey = parsedMessage.metaInfo?.aesKey; + const aesIv = parsedMessage.metaInfo?.aesIv; + if (aesKey && aesIv) { + return { + operationCode: GeneralOperations.operationCodes.SET_AES_KEY, + metaInfo: { aesKey: aesKey, aesIv: aesIv }, + }; + } else { + return { + operationCode: GeneralOperations.operationCodes.ERR, + metaInfo: { message: 'No AES key provided.' }, + }; + } + } + + // Register general operations with the OperationHandler + public register(operationHandler: OperationHandler): void { + // Register specific handlers for the general operations + operationHandler.registerHandler(GeneralOperations.operationCodes.HEARTBEAT, GeneralOperations.handleHeartbeat); + operationHandler.registerHandler(GeneralOperations.operationCodes.SET_PUBLIC_KEY, GeneralOperations.handlePublicKey); + operationHandler.registerHandler(GeneralOperations.operationCodes.SET_AES_KEY, GeneralOperations.handleAESKey); // Register AES key handler + + // Register common operations inherited from the base class + OperationBase.registerCommonOperations(operationHandler); + } +} diff --git a/CEO/src/network/operations_custom/user_to_user_operations.ts b/CEO/src/network/operations_custom/user_to_user_operations.ts new file mode 100644 index 0000000..9bf7b88 --- /dev/null +++ b/CEO/src/network/operations_custom/user_to_user_operations.ts @@ -0,0 +1,500 @@ +import { ParsedMessage } from '../message_handler'; +import { OperationBase } from '../operations_base/operation_base'; +import { OperationHandler } from '../operations_base/operation_handler'; +import path from 'path'; +import fs from 'fs'; +import { FileEncryptor } from '../../helpers/file_encryptor'; + +const LOCK_FILE_EXTENSION = '.lock'; + +export class UserToUserOperations extends OperationBase { + public static readonly operationCodes = { + ...OperationBase.operationCodes, // Inherit common operation codes (OK, ERR, END) + GET_USER_INFORMATION: 'GET_USER_INFORMATION', + BACKUP_FILE: 'BACKUP_FILE', + CLEAR_BACKUP: 'CLEAR_BACKUP', + SHARE_FILE: 'SHARE_FILE', + CLEAR_DEPARTMENT: 'CLEAR_DEPARTMENT', + DEPARTMENT_FILE: 'DEPARTMENT_FILE', + IS_BACKUP_CREATED: 'IS_BACKUP_CREATED', + GET_BACKUP_STRUCTURE: 'GET_BACKUP_STRUCTURE', + REQ_FILE_FROM_BACKUP: 'REQ_FILE_FROM_BACKUP', + DECRYPT_AND_SAVE_OLD_BACKUP_FILE: 'DECRYPT_AND_SAVE_OLD_BACKUP_FILE', + }; + + // Utility function to pause execution (sleep) + static sleep(ms: number): void { + const start = Date.now(); + while (Date.now() - start < ms) { + // Busy wait loop (not optimal but fine for this short duration) + } + } + + // Read JSON file with a lock mechanism + static readJsonSync(filePath: string): any { + const lockFilePath = `${filePath}${LOCK_FILE_EXTENSION}`; + const absolutePath = path.resolve(filePath); + + try { + // Loop until the lock file is removed by another process + while (fs.existsSync(lockFilePath)) { + console.log(`Waiting for lock file to be released: ${lockFilePath}`); + UserToUserOperations.sleep(100); + } + + // Create lock file to signal this process is working on the file + fs.writeFileSync(lockFilePath, ''); // Create the lock file + + // Read and parse the JSON file + const fileContents = fs.readFileSync(absolutePath, 'utf-8'); + const parsedJson = JSON.parse(fileContents); + + // Once processing is done, delete the lock file + fs.unlinkSync(lockFilePath); // Remove the lock file + + return parsedJson; // Return the parsed JSON data + } catch (error) { + console.error(`Error reading or parsing JSON from ${filePath}:`, error); + + // Ensure the lock file is removed even in case of an error + if (fs.existsSync(lockFilePath)) { + fs.unlinkSync(lockFilePath); + } + + return null; // Return null or throw error based on preference + } + } + + // Handle user information retrieval + public static handleGetUserInformation(parsedMessage: ParsedMessage): ParsedMessage { + const pathToUserJson = path.join(__dirname, '..', '..', 'json_files', 'userConfig.json'); + const userInfo = UserToUserOperations.readJsonSync(pathToUserJson); + + if (userInfo) { + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: userInfo.user_info, + }; + } else { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Error fetching user information.' }, + }; + } + } + + // Handle file reception and saving + public static handleBackupFile(parsedMessage: ParsedMessage): ParsedMessage { + // Ensure metaInfo and fileContent are available + if (!parsedMessage.metaInfo || !parsedMessage.fileContent) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing file content or meta information.' }, + }; + } + + const { userName, relativeFilePath } = parsedMessage.metaInfo; + + if (!userName || !relativeFilePath) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing user name or file path information.' }, + }; + } + + // Base directory where backups will be stored + const baseBackupDir = path.join(__dirname, '..', '..', 'backups'); + + // Full path where the file will be stored (under the user's directory) + const fullFilePath = path.join(baseBackupDir, userName, relativeFilePath); + + try { + // Ensure the directory structure exists (create directories if they don't exist) + const dirPath = path.dirname(fullFilePath); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + + // Write the file content to the correct path + fs.writeFileSync(fullFilePath, Buffer.from(parsedMessage.fileContent as Buffer), 'base64'); + + console.log(`File saved successfully: ${fullFilePath}`); + + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: { message: `File saved successfully: ${relativeFilePath}` }, + }; + } catch (error: any) { + console.error(`Error saving file: ${error.message}`); + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: `Error saving file: ${error.message}` }, + }; + } + } + + // Handle clearing all backups for a user + public static handleClearBackup(parsedMessage: ParsedMessage): ParsedMessage { + // Ensure the userName is available in metaInfo + if (!parsedMessage.metaInfo || !parsedMessage.metaInfo.userName) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing user name in meta information.' }, + }; + } + + const { userName } = parsedMessage.metaInfo; + + // Base directory where backups are stored + const baseBackupDir = path.join(__dirname, '..', '..', 'backups'); + const userBackupDir = path.join(baseBackupDir, userName); + + try { + // Check if the user's backup directory exists + if (fs.existsSync(userBackupDir)) { + // Recursively delete the user's backup directory + fs.rmSync(userBackupDir, { recursive: true, force: true }); + console.log(`Backup cleared successfully for user: ${userName}`); + + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: { message: `Backup cleared successfully for user: ${userName}` }, + }; + } else { + console.warn(`Backup directory not found for user: ${userName}`); + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: `Backup directory not found for user: ${userName}` }, + }; + } + } catch (error: any) { + console.error(`Error clearing backup for user: ${userName} - ${error.message}`); + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: `Error clearing backup for user: ${userName}` }, + }; + } + } + + // Handle sharing file operation + public static handleShareFile(parsedMessage: ParsedMessage): ParsedMessage { + // Ensure metaInfo and fileContent are available + if (!parsedMessage.metaInfo || !parsedMessage.fileContent) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing file content or meta information.' }, + }; + } + + const { userName, relativeFilePath } = parsedMessage.metaInfo; + + if (!userName || !relativeFilePath) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing user name or file path information.' }, + }; + } + + // Path to the application.json to read the shareDirectory field + const pathToApplicationJson = path.join(__dirname, '..', '..', 'json_files', 'application.json'); + const appInfo = UserToUserOperations.readJsonSync(pathToApplicationJson); + + // Corrected the condition + if (!appInfo || !appInfo.shareDirectory) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Error retrieving share directory from application.json.' }, + }; + } + + // Get the share directory path + const shareDirectory = appInfo.shareDirectory.path; + + // Full path where the file will be stored (under the user's directory in the shared folder) + const fullFilePath = path.join(shareDirectory, userName, relativeFilePath); + + try { + // Ensure the directory structure exists (create directories if they don't exist) + const dirPath = path.dirname(fullFilePath); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + + // Write the file content to the correct path + fs.writeFileSync(fullFilePath, Buffer.from(parsedMessage.fileContent as Buffer), 'base64'); + + console.log(`File shared successfully: ${fullFilePath}`); + + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: { message: `File shared successfully: ${relativeFilePath}` }, + }; + } catch (error: any) { + console.error(`Error sharing file: ${error.message}`); + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: `Error sharing file: ${error.message}` }, + }; + } + } + + public static handleClearDepartment(parsedMessage: ParsedMessage): ParsedMessage { + // Ensure the userName is available in metaInfo + if (!parsedMessage.metaInfo || !parsedMessage.metaInfo.userName) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing user name in meta information.' }, + }; + } + + const { userName } = parsedMessage.metaInfo; + + const pathToApplicationJson = path.join(__dirname, '..', '..', 'json_files', 'application.json'); + const appInfo = UserToUserOperations.readJsonSync(pathToApplicationJson); + + // Corrected the condition + if (!appInfo || !appInfo.departmentDirectory) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Error retrieving share directory from application.json.' }, + }; + } + + // Get the share directory path + const baseDepartmentDir = appInfo.departmentDirectory.path; + const userDepartmentDir = path.join(baseDepartmentDir, userName); + + try { + // Check if the user's backup directory exists + if (fs.existsSync(userDepartmentDir)) { + // Recursively delete the user's backup directory + fs.rmSync(userDepartmentDir, { recursive: true, force: true }); + console.log(`Backup cleared successfully for user: ${userName}`); + + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: { message: `Backup cleared successfully for user: ${userName}`}, + }; + } else { + console.warn(`Backup directory not found for user: ${userName}`); + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: `Backup directory not found for user: ${userName}`}, + }; + } + } catch (error: any) { + console.error(`Error clearing backup for user: ${userName} - ${error.message}`); + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: `Error clearing backup for user: ${userName}`}, + }; + } + } + + public static handleDepartmentFile(parsedMessage: ParsedMessage): ParsedMessage { + // Ensure metaInfo and fileContent are available + if (!parsedMessage.metaInfo || !parsedMessage.fileContent) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing file content or meta information.' }, + }; + } + + const { userName, relativeFilePath } = parsedMessage.metaInfo; + + if (!userName || !relativeFilePath) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing user name or file path information.' }, + }; + } + + // Path to the application.json to read the shareDirectory field + const pathToApplicationJson = path.join(__dirname, '..', '..', 'json_files', 'application.json'); + const appInfo = UserToUserOperations.readJsonSync(pathToApplicationJson); + + // Corrected the condition + if (!appInfo || !appInfo.departmentDirectory) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Error retrieving share directory from application.json.' }, + }; + } + + // Get the share directory path + const departmentDirectory = appInfo.departmentDirectory.path; + + // Full path where the file will be stored (under the user's directory in the shared folder) + const fullFilePath = path.join(departmentDirectory, userName, relativeFilePath); + + try { + // Ensure the directory structure exists (create directories if they don't exist) + const dirPath = path.dirname(fullFilePath); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + + // Write the file content to the correct path + fs.writeFileSync(fullFilePath, Buffer.from(parsedMessage.fileContent as Buffer), 'base64'); + + console.log(`File shared successfully: ${fullFilePath}`); + + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: { message: `File shared successfully: ${relativeFilePath}`}, + }; + } catch (error: any) { + console.error(`Error sharing file: ${error.message}`); + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: {message: `Error sharing file: ${error.message}`}, + }; + } + } + + // Check if a backup has been created for a user + public static handleIsBackupCreated(parsedMessage: ParsedMessage): ParsedMessage { + if(!parsedMessage.metaInfo) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing metaInfo.' }, + }; + } + + const {name} = parsedMessage.metaInfo; + if (!name) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing name in meta information.' }, + }; + } + + const baseBackupDir = path.join(__dirname, '..', '..', 'backups'); + const userBackupDir = path.join(baseBackupDir, name); + + console.log(userBackupDir); + + const exists = fs.existsSync(userBackupDir); + + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: { backupExists: exists }, + }; + } + + // Get the structure of the backup directory for a user + public static handleGetBackupStructure(parsedMessage: ParsedMessage): ParsedMessage { + if(!parsedMessage.metaInfo) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing metaInfo.' }, + }; + } + + const {name} = parsedMessage.metaInfo; + if (!name) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing name in meta information.' }, + }; + } + + const baseBackupDir = path.join(__dirname, '..', '..', 'backups'); + const userBackupDir = path.join(baseBackupDir, name); + + if (!fs.existsSync(userBackupDir)) { + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: { structure: {} }, // Return empty structure if directory doesn't exist + }; + } + + const fileStructure = UserToUserOperations.buildDirectoryStructure(userBackupDir); + + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: { structure: fileStructure }, + }; + } + + // Build the directory structure recursively + private static buildDirectoryStructure(directoryPath: string): any { + const structure: any = {}; + const files = fs.readdirSync(directoryPath); + + for (const file of files) { + const filePath = path.join(directoryPath, file); + const stats = fs.statSync(filePath); + + if (stats.isDirectory()) { + structure[file] = UserToUserOperations.buildDirectoryStructure(filePath); // Recursive for subdirectories + } else { + structure[file] = path.relative(directoryPath, filePath); + } + } + + return structure; + } + + // Handle file request from backup directory + public static handleReqFileFromBackup(parsedMessage: ParsedMessage): ParsedMessage { + if(!parsedMessage.metaInfo) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing metaInfo.' }, + }; + } + + const { name, relativeFilePath } = parsedMessage.metaInfo; + + if (!name || !relativeFilePath) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing name or file path in meta information.' }, + }; + } + + const baseBackupDir = path.join(__dirname, '..', '..', 'backups'); + const userBackupDir = path.join(baseBackupDir, name); + const fullFilePath = path.join(userBackupDir, relativeFilePath); + + if (!fs.existsSync(fullFilePath)) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'File not found in backup.' }, + }; + } + + try { + const fileContent = fs.readFileSync(fullFilePath, 'base64'); + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: { relativeFilePath }, + fileContent: Buffer.from(fileContent, 'base64') + }; + } catch (error: any) { + console.error(`Error reading file: ${error.message}`); + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: `Error reading file: ${error.message}`}, + }; + } + } + + // Register user-to-user operations with the OperationHandler + public register(operationHandler: OperationHandler): void { + // Register specific handlers for user-to-user operations + operationHandler.registerHandler(UserToUserOperations.operationCodes.GET_USER_INFORMATION, UserToUserOperations.handleGetUserInformation); + operationHandler.registerHandler(UserToUserOperations.operationCodes.BACKUP_FILE, UserToUserOperations.handleBackupFile); + operationHandler.registerHandler(UserToUserOperations.operationCodes.CLEAR_BACKUP, UserToUserOperations.handleClearBackup); + operationHandler.registerHandler(UserToUserOperations.operationCodes.SHARE_FILE, UserToUserOperations.handleShareFile); + operationHandler.registerHandler(UserToUserOperations.operationCodes.DEPARTMENT_FILE, UserToUserOperations.handleDepartmentFile); + operationHandler.registerHandler(UserToUserOperations.operationCodes.CLEAR_DEPARTMENT, UserToUserOperations.handleClearDepartment); + operationHandler.registerHandler(UserToUserOperations.operationCodes.IS_BACKUP_CREATED, UserToUserOperations.handleIsBackupCreated); + operationHandler.registerHandler(UserToUserOperations.operationCodes.GET_BACKUP_STRUCTURE, UserToUserOperations.handleGetBackupStructure); + operationHandler.registerHandler(UserToUserOperations.operationCodes.REQ_FILE_FROM_BACKUP, UserToUserOperations.handleReqFileFromBackup); + + // Register common operations inherited from the base class + OperationBase.registerCommonOperations(operationHandler); + } +} diff --git a/CEO/src/network/socket_communicator/socket_communicator_base.ts b/CEO/src/network/socket_communicator/socket_communicator_base.ts new file mode 100644 index 0000000..cbe8e7d --- /dev/null +++ b/CEO/src/network/socket_communicator/socket_communicator_base.ts @@ -0,0 +1,21 @@ +import { ParsedMessage } from '../message_handler'; +import { OperationHandler } from '../operations_base/operation_handler'; + +export abstract class SocketCommunicatorBase { + protected readonly ip: string; + protected readonly port: number; + protected readonly operationHandler: OperationHandler; + protected handlerResult: ParsedMessage | null; + + protected constructor(ip: string, port: number, operationHandler: OperationHandler) { + this.ip = ip; + this.port = port; + this.operationHandler = operationHandler + this.handlerResult = null; + } + + // Getter for the handler result + getHandlerResult(): ParsedMessage | null { + return this.handlerResult; + } +} diff --git a/CEO/src/network/socket_communicator/tcp_client_communicator.ts b/CEO/src/network/socket_communicator/tcp_client_communicator.ts new file mode 100644 index 0000000..7f262e6 --- /dev/null +++ b/CEO/src/network/socket_communicator/tcp_client_communicator.ts @@ -0,0 +1,167 @@ +import { Socket } from 'net'; +import { createCipheriv, createDecipheriv } from 'crypto'; +import { MessageHandler, ParsedMessage } from '../message_handler'; +import { SocketCommunicatorBase } from './socket_communicator_base'; +import { OperationHandler } from '../operations_base/operation_handler'; +import {constants, publicDecrypt} from "node:crypto"; +import {operationCodes} from "../operation_codes"; + +const END_OF_MESSAGE = ''; // Define a unique marker for end of message + +export class TcpClientCommunicator extends SocketCommunicatorBase { + private readonly socket: Socket; + private aesKey: Buffer | null; + private aesIv: Buffer | null; + private messageBuffer: string; + private serverPublicKey: string | null; + private isAesKeySetFlag: boolean; + + constructor(socket: Socket, ip: string, port: number, operationHandler: OperationHandler) { + super(ip, port, operationHandler); + this.socket = socket; + this.aesKey = null; + this.aesIv = null; + this.serverPublicKey = null; + this.messageBuffer = ''; // Buffer for message reassembly + this.isAesKeySetFlag = false; + } + + setServerPublicKey(publicKey: string): void { + this.serverPublicKey = publicKey; + console.log('Server public key set.'); + } + + // Set the AES key when received + setAesKey(aesKey: string, aesIv: string): void { + this.aesKey = Buffer.from(aesKey, 'base64'); + this.aesIv = Buffer.from(aesIv, 'base64'); + console.log('AES key set.'); + } + + // Encrypt a message with AES + private encryptWithAes(message: string): string { + if (!this.aesKey || !this.aesIv) { + throw new Error('AES key or IV is not set.'); + } + const cipher = createCipheriv('aes-256-cbc', this.aesKey, this.aesIv); + let encrypted = cipher.update(message, 'utf-8'); + encrypted = Buffer.concat([encrypted, cipher.final()]); + return encrypted.toString('base64'); + } + + private decryptWithAes(encryptedMessage: string): string { + if (!this.aesKey || !this.aesIv) { + throw new Error('AES key or IV is not set.'); + } + const decipher = createDecipheriv('aes-256-cbc', this.aesKey, this.aesIv); + let decrypted = decipher.update(Buffer.from(encryptedMessage, 'base64')); + decrypted = Buffer.concat([decrypted, decipher.final()]); + return decrypted.toString('utf-8'); + } + + private decryptWithRsa(message: string): string { + if (!this.serverPublicKey) { + throw new Error('Server public key not set.'); + } + try { + const encryptedMessage = Buffer.from(message.toString(), 'base64'); + // Decrypt the message using the server's public key + const decrypted = publicDecrypt( + { + key: this.serverPublicKey, + padding: constants.RSA_PKCS1_PADDING, // Matching padding for decryption + }, + encryptedMessage + ); + return decrypted.toString('utf-8'); + } catch (error) { + console.error('RSA decryption failed:', error); + throw new Error('Failed to decrypt RSA message.'); + } + } + + // Handle incoming chunks of data + async handleIncomingChunk(data: Buffer): Promise { + const incomingMessage = data.toString(); + this.messageBuffer += incomingMessage; + + // Check if the message ends with END_OF_MESSAGE + if (this.messageBuffer.endsWith(END_OF_MESSAGE)) { + // Remove the END_OF_MESSAGE marker and process the message + const completeMessage = this.messageBuffer.slice(0, -END_OF_MESSAGE.length); + + this.handleIncomingMessage(completeMessage); + + // Clear the message buffer after processing + this.messageBuffer = ''; + } + } + + // Send a chunked message over the socket + async sendChunkedMessage(operationCode: string, metaInfo?: { [key: string]: any }, fileContent?: Buffer): Promise { + const message = MessageHandler.formatMessage(operationCode, metaInfo, fileContent); + let outgoingMessage: string; + + // Encrypt the message with AES if available + if (this.aesKey && this.aesIv) { + outgoingMessage = this.encryptWithAes(message); + } else { + outgoingMessage = message // Send plain text if AES is not set + } + + // Append the end marker to the message + outgoingMessage += END_OF_MESSAGE; + + await this.writeToSocket(outgoingMessage); + } + + // Write message to socket + private writeToSocket(message: string): Promise { + return new Promise((resolve, reject) => { + this.socket.write(message, (err: any) => { + if (err) { + console.error('Error sending message over TCP:', err); + return reject(err); + } + resolve(); + }); + }); + } + + // Handle incoming message (decrypted if AES is set) + handleIncomingMessage(incomingMessage: string): void { + let messageToProcess = incomingMessage; + if (this.aesKey && this.aesIv) { + messageToProcess = this.decryptWithAes(incomingMessage); + }else if(this.serverPublicKey){ + messageToProcess = this.decryptWithRsa(incomingMessage); + } + + console.log(`\n\nComplete Message:\n${messageToProcess}\n\n`); + + const result = this.operationHandler.handleOperation(messageToProcess); + + if(result.operationCode === operationCodes.SET_AES_KEY){ + this.isAesKeySetFlag = true; + this.setAesKey(result.metaInfo?.aesKey, result.metaInfo?.aesIv); + return; + } + + if(result.operationCode === operationCodes.SET_PUBLIC_KEY) { + this.setServerPublicKey(result.metaInfo?.publicKey); + return; + } + + this.handlerResult = result + } + + // Check if AES key is set + isAesKeySet(): boolean { + return this.isAesKeySetFlag; + } + + // Get handler result for operation handling + getHandlerResult(): ParsedMessage | null { + return this.handlerResult; + } +} diff --git a/CEO/src/network/socket_communicator/tcp_server_communicator.ts b/CEO/src/network/socket_communicator/tcp_server_communicator.ts new file mode 100644 index 0000000..8f57dba --- /dev/null +++ b/CEO/src/network/socket_communicator/tcp_server_communicator.ts @@ -0,0 +1,161 @@ +import { Socket } from 'net'; +import { privateEncrypt, privateDecrypt, generateKeyPairSync, createCipheriv, createDecipheriv, randomBytes } from 'crypto'; +import { MessageHandler, ParsedMessage } from '../message_handler'; +import { SocketCommunicatorBase } from './socket_communicator_base'; +import { OperationHandler } from '../operations_base/operation_handler'; +import {constants} from "node:crypto"; + +const END_OF_MESSAGE = ''; // Define a unique marker for end of message + +export class TcpServerCommunicator extends SocketCommunicatorBase { + private readonly socket: Socket; + private privateKey: string | null; + private publicKey: string | null; + private aesKey: Buffer | null; + private aesIv: Buffer | null; + private messageBuffer: string; + + constructor(socket: Socket, ip: string, port: number, operationHandler: OperationHandler) { + super(ip, port, operationHandler); + this.socket = socket; + this.privateKey = null; + this.publicKey = null; // Client public key will be set later + this.aesKey = null; + this.aesIv = null; + this.messageBuffer = ''; // Initialize the message buffer + this.generateKeyPair(); // Generate RSA key pair for encryption + } + + // Generate RSA key pair (public and private keys) + generateKeyPair(): void { + const { privateKey, publicKey } = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + this.privateKey = privateKey; + this.publicKey = publicKey; + console.log('RSA key pair generated.'); + } + + // Send the server's public key to the client + async sendPublicKey(): Promise { + if (!this.publicKey) { + throw new Error('Public key is not available. Please generate RSA key pair.'); + } + + const message = MessageHandler.formatMessage('SET_PUBLIC_KEY', { publicKey: this.publicKey }); + await this.writeToSocket(message + END_OF_MESSAGE); + console.log('Public key sent to client.'); + } + + // Generate AES key and IV, then send them to the client + async sendAesKey(): Promise { + this.aesKey = randomBytes(32); // 256-bit AES key + this.aesIv = randomBytes(16); // AES IV + + const aesKeyBase64 = this.aesKey.toString('base64'); + const aesIvBase64 = this.aesIv.toString('base64'); + + const formattedMessage = MessageHandler.formatMessage('SET_AES_KEY', { aesKey: aesKeyBase64, aesIv: aesIvBase64 }); + const encryptedMessage = this.encryptWithRsa(Buffer.from(formattedMessage)).toString('base64'); + + await this.writeToSocket(encryptedMessage + END_OF_MESSAGE); + console.log('AES key and IV sent to client.'); + } + + // Encrypt a message with the server's private key (RSA encryption) + private encryptWithRsa(message: Buffer): Buffer { + const bufferMessage = Buffer.isBuffer(message) ? message : Buffer.from(message); + if (!this.privateKey) throw new Error('Server private key not set.'); + + return privateEncrypt( + { + key: this.privateKey, + padding: constants.RSA_PKCS1_PADDING, // PKCS1 padding + }, + bufferMessage + ); + } + + // Decrypt AES-encrypted messages + private decryptWithAes(encryptedMessage: string): string { + if (!this.aesKey || !this.aesIv) { + throw new Error('AES key not set.'); + } + + const decipher = createDecipheriv('aes-256-cbc', this.aesKey, this.aesIv); + let decrypted = decipher.update(Buffer.from(encryptedMessage, 'base64')); + decrypted = Buffer.concat([decrypted, decipher.final()]); + return decrypted.toString('utf-8'); + } + + // Encrypt a message with AES + private encryptWithAes(message: string): string { + if (!this.aesKey || !this.aesIv) { + throw new Error('AES key or IV is not set.'); + } + const cipher = createCipheriv('aes-256-cbc', this.aesKey, this.aesIv); + let encrypted = cipher.update(message, 'utf-8'); + encrypted = Buffer.concat([encrypted, cipher.final()]); + return encrypted.toString('base64'); + } + + // Handle incoming chunks of data + async handleIncomingChunk(data: Buffer): Promise { + const incomingMessage = data.toString(); + this.messageBuffer += incomingMessage; + + // Check if the message ends with END_OF_MESSAGE + if (this.messageBuffer.endsWith(END_OF_MESSAGE)) { + // Remove the END_OF_MESSAGE marker and process the message + const completeMessage = this.messageBuffer.slice(0, -END_OF_MESSAGE.length); + + console.log(`\n\nComplete Message:\n${completeMessage}\n\n`); + + this.handleIncomingMessage(completeMessage); + + // Clear the message buffer after processing + this.messageBuffer = ''; + } + } + + // Send chunked message + async sendChunkedMessage(operationCode: string, metaInfo?: { [key: string]: any }, fileContent?: Buffer): Promise { + const message = MessageHandler.formatMessage(operationCode, metaInfo, fileContent); + let outgoingMessage: string; + + if (this.aesKey && this.aesIv) { + outgoingMessage = this.encryptWithAes(message); + } else { + outgoingMessage = message; + } + + outgoingMessage += END_OF_MESSAGE; // Append end-of-message marker + await this.writeToSocket(outgoingMessage); + } + + // Handle incoming message (decrypt with AES if available) + handleIncomingMessage(incomingMessage: string): void { + let messageToProcess = incomingMessage; + + if (this.aesKey && this.aesIv) { + messageToProcess = this.decryptWithAes(incomingMessage); + } + + this.handlerResult = this.operationHandler.handleOperation(messageToProcess); + } + + // Write message to socket + private writeToSocket(message: string): Promise { + return new Promise((resolve, reject) => { + this.socket.write(message, (err: any) => { + if (err) { + console.error('Error sending message over TCP:', err); + return reject(err); + } + resolve(); + }); + }); + } +} diff --git a/CEO/src/network/socket_communicator/udp_socket_communicator.ts b/CEO/src/network/socket_communicator/udp_socket_communicator.ts new file mode 100644 index 0000000..0385573 --- /dev/null +++ b/CEO/src/network/socket_communicator/udp_socket_communicator.ts @@ -0,0 +1,39 @@ +import { Socket as UdpSocket } from 'dgram'; +import { MessageHandler } from '../message_handler'; +import { SocketCommunicatorBase } from './socket_communicator_base'; +import {OperationHandler} from "../operations_base/operation_handler"; + +export class UdpSocketCommunicator extends SocketCommunicatorBase { + private readonly socket: UdpSocket; + + constructor(socket: UdpSocket, ip: string, port: number, operationHandler: OperationHandler) { + super(ip, port, operationHandler); + this.socket = socket; + } + + // Handle incoming message (no decryption needed for UDP) + handleIncomingMessage(incomingMessage: string): void { + this.handlerResult = this.operationHandler.handleOperation(incomingMessage); + } + + // Send plain message over UDP (metaInfo as JSON and fileContent as Buffer) + async sendMessage(operationCode: string, metaInfo?: { [key: string]: any }, fileContent?: Buffer): Promise { + const message = MessageHandler.formatMessage(operationCode, metaInfo, fileContent); + + await this.sendUdpMessage(message); + } + + // Helper method to wrap socket.send in a Promise for async/await support + private sendUdpMessage(message: string): Promise { + return new Promise((resolve, reject) => { + this.socket.send(message, this.port, this.ip, (err: any) => { + if (err) { + console.error('Error sending UDP message:', err); + return reject(err); + } + console.log(`Plain message sent to ${this.ip}:${this.port} (UDP)`); + resolve(); + }); + }); + } +} diff --git a/CEO/src/network/tcp/tcp_client.ts b/CEO/src/network/tcp/tcp_client.ts new file mode 100644 index 0000000..557939a --- /dev/null +++ b/CEO/src/network/tcp/tcp_client.ts @@ -0,0 +1,100 @@ +import net, { Socket } from 'net'; +import { TcpClientCommunicator } from '../socket_communicator/tcp_client_communicator'; +import { OperationHandler } from '../operations_base/operation_handler'; +import { operationCodes } from "../operation_codes"; +import { GeneralOperations } from "../operations_custom/general_operations"; +import { UserToUserOperations } from "../operations_custom/user_to_user_operations"; +import { ParsedMessage } from "../message_handler"; + +export class TcpClient { + private readonly tcp_port: number; + private socket: Socket | null; + private communicator: TcpClientCommunicator | null; + private readonly operationHandler: OperationHandler; + private lastResult: ParsedMessage | null; + + constructor(tcp_port: number) { + this.tcp_port = tcp_port; + this.socket = null; + this.communicator = null; + this.operationHandler = OperationHandler.getInstance(); + this.lastResult = null; + + this.operationHandler.loadPlugin(new GeneralOperations()); + this.operationHandler.loadPlugin(new UserToUserOperations()); + } + + // Open a TCP socket connection + openSocket(ip: string): void { + this.socket = new net.Socket(); + + this.socket.connect(this.tcp_port, ip, () => { + console.log(`Client connected to server at ${ip}:${this.tcp_port}`); + this.communicator = new TcpClientCommunicator(this.socket as Socket, ip, this.tcp_port, this.operationHandler); + }); + + this.socket.on('error', (err) => { + console.error(`Connection error to server at ${ip}:${this.tcp_port}: ${err.message}`); + }); + + this.socket.on('data', async (data: Buffer) => { + if (this.communicator) { + await this.communicator.handleIncomingChunk(data); // Let the communicator handle the chunks + this.lastResult = this.communicator.getHandlerResult(); + } + }); + + this.socket.on('close', () => { + console.log(`Connection closed: ${ip}:${this.tcp_port}`); + this.lastResult = null; // Clear the last result on socket close + }); + } + + // Close the socket connection + closeSocket(): void { + if (this.socket) { + this.socket.end(); + this.socket = null; + this.communicator = null; + this.lastResult = null; // Clear the last result on close + console.log('Client socket connection closed.'); + } + } + + // Send a message with operationCode, metaInfo, and fileContent in chunks + async sendMessage(operationCode: string, metaInfo?: { [key: string]: any }, fileContent?: Buffer): Promise { + if (!this.communicator || !this.isAesKeySet()) { + console.error('Communicator not initialized or AES key not set.'); + return false; + } + + await this.communicator.sendChunkedMessage(operationCode, metaInfo, fileContent); // Send message via communicator + return true; + } + + // Check if AES key is set + isAesKeySet(): boolean { + if(!this.communicator) return false; + return this.communicator?.isAesKeySet() + } + + // Check if the message is received (based on if lastResult is available) + isMessageReceived(): boolean { + console.log('Is message received?'); + console.log(this.lastResult); + console.log(this.lastResult !== null); + return this.lastResult !== null; + } + + // Get the last result (and clear it after returning) + getLastResult(): ParsedMessage | null { + const result = this.lastResult; + this.lastResult = null; + return result; + } + + // Check if the socket is still connected + isSocketConnected(): boolean { + return this.socket !== null && !this.socket.destroyed; + } +} diff --git a/CEO/src/network/tcp/tcp_server.ts b/CEO/src/network/tcp/tcp_server.ts new file mode 100644 index 0000000..98d1504 --- /dev/null +++ b/CEO/src/network/tcp/tcp_server.ts @@ -0,0 +1,111 @@ +import net, { Socket } from 'net'; +import path from 'path'; +import dotenv from 'dotenv'; +import { ConnectionManager } from "../connection_manager"; +import { TcpServerCommunicator } from "../socket_communicator/tcp_server_communicator"; +import { GeneralOperations } from "../operations_custom/general_operations"; +import { OperationHandler } from "../operations_base/operation_handler"; +import { UserToUserOperations } from "../operations_custom/user_to_user_operations"; + +dotenv.config({ path: path.resolve(__dirname, './config/.env') }); + +export class TcpServer { + private readonly connectionManager: ConnectionManager; + private readonly operationHandler: OperationHandler; + private readonly port: number; + private readonly host: string; + + constructor(host: string, port: number) { + this.connectionManager = new ConnectionManager(); + this.operationHandler = OperationHandler.getInstance(); + this.host = host; + this.port = port; + + this.operationHandler.loadPlugin(new GeneralOperations()); + this.operationHandler.loadPlugin(new UserToUserOperations()); + } + + // Start the TCP server + public start(): void { + const tcpServer = net.createServer(); + + // Handle incoming connections + tcpServer.on('connection', (socket: Socket) => { + const ip = socket.remoteAddress || 'unknown'; + const port = socket.remotePort || 0; + + const clientId = `${ip}:${port}`; // Use IP and port to identify the client + console.log(`Client connected: ${clientId}`); + + const tcpCommunicator = new TcpServerCommunicator(socket, ip, port, this.operationHandler); + this.connectionManager.addConnection(ip, port, tcpCommunicator); + + // Generate RSA key pair and start key exchange + tcpCommunicator.generateKeyPair(); + tcpCommunicator.sendPublicKey() + .then(() => tcpCommunicator.sendAesKey()) + .then(() => console.log('Public key and AES key sent successfully.')) + .catch(err => { + console.error(`Error during key exchange with client ${clientId}:`, err); + socket.end(); // Close the connection in case of any error + }); + + // Handle incoming data in chunks + socket.on('data', async (data: Buffer) => { + await this.handleData(data, ip, port); + }); + + // Handle client disconnect + socket.on('end', () => { + console.log(`Client disconnected: ${clientId}`); + this.connectionManager.removeCommunicator(ip, port); + }); + + // Handle socket errors + socket.on('error', (err: Error) => { + console.error(`Error from client ${clientId}: ${err.message}`); + this.connectionManager.removeCommunicator(ip, port); + }); + }); + + // Handle server errors + tcpServer.on('error', (err: Error) => { + console.error(`TCP server error: ${err.message}`); + }); + + // Start listening for connections + tcpServer.listen(this.port, this.host, () => { + console.log(`TCP server listening on ${this.host}:${this.port}`); + }); + } + + // Handle incoming data from a client + private async handleData(data: Buffer, ip: string, port: number): Promise { + const clientId = `${ip}:${port}`; + + // Retrieve the communicator associated with this connection + const communicator = this.connectionManager.getCommunicator(ip, port) as TcpServerCommunicator; + if (!communicator) { + console.error(`No communicator found for ${clientId}`); + return; + } + + // Handle incoming chunked message via communicator + await communicator.handleIncomingChunk(data); + + // Fetch and process result if available + const handlerResult = communicator.getHandlerResult(); + if (handlerResult) { + try { + await communicator.sendChunkedMessage( + handlerResult.operationCode, + handlerResult.metaInfo, + handlerResult.fileContent + ); + console.log(`Response sent to ${clientId}`); + } catch (err) { + console.error(`Failed to send response to ${clientId}:`, err); + } + } + } +} diff --git a/CEO/src/network/udp/udp_client.ts b/CEO/src/network/udp/udp_client.ts new file mode 100644 index 0000000..03789b2 --- /dev/null +++ b/CEO/src/network/udp/udp_client.ts @@ -0,0 +1,140 @@ +import dgram from 'dgram'; +import ping from 'ping'; +import { OperationHandler } from '../operations_base/operation_handler'; +import { MessageHandler } from '../message_handler'; +import { GeneralOperations } from "../operations_custom/general_operations"; +import { operationCodes } from "../operation_codes"; +import os from 'os'; + +export class UdpClient { + private udpSocket: dgram.Socket; + private readonly port: number; + private operationHandler: OperationHandler; + + constructor(port: number) { + this.port = port; + this.udpSocket = dgram.createSocket('udp4'); + this.operationHandler = OperationHandler.getInstance(); + this.operationHandler.loadPlugin(new GeneralOperations()); + } + + // Send heartbeat and get clients that respond with ALIVE, excluding the host machine's IP addresses + async getAliveClients(): Promise { + const subnet = this.getSubnet(); + const ipRange = this.getIPRange(subnet); + + // Get local machine's IP addresses to exclude + const localIPs = this.getLocalIPs(); + + // First, filter active IPs that respond to ping + const activeIps = await this.filterActiveIps(ipRange); + + // Send heartbeat to each active IP and keep only those that respond with ALIVE + const aliveClients: string[] = []; + for (const ip of activeIps) { + if (!localIPs.includes(ip)) { + const result = await this.sendHeartbeat(ip); + if (result.found) { + aliveClients.push(ip); + } + } + } + + return aliveClients; // Return the list of IPs that responded with ALIVE, excluding the host machine + } + + // Get local IP addresses of the host machine (excluding loopback) + private getLocalIPs(): string[] { + const interfaces = os.networkInterfaces(); + const localIPs: string[] = []; + + Object.values(interfaces).forEach((iface) => { + iface?.forEach((address) => { + if (address.family === 'IPv4' && !address.internal) { + localIPs.push(address.address); + } + }); + }); + + return localIPs; + } + + // Send heartbeat to an IP + private async sendHeartbeat(ip: string): Promise<{ found: boolean }> { + return new Promise((resolve) => { + const heartbeatCode = operationCodes.HEARTBEAT; + const heartbeatMessage = MessageHandler.formatMessage(heartbeatCode); + + this.udpSocket.send(heartbeatMessage, this.port, ip, (err) => { + if (err) { + resolve({ found: false }); + } else { + const timeout = setTimeout(() => { + this.dropConnection(ip); + resolve({ found: false }); + }, 1500); + + this.udpSocket.once('message', (msg, rinfo) => { + if (rinfo.address === ip) { + clearTimeout(timeout); + const parsedMessage = MessageHandler.parseMessage(msg.toString()); + if (parsedMessage?.operationCode === operationCodes.ALIVE) { + resolve({ found: true }); + } else { + resolve({ found: false }); + } + } + }); + } + }); + }); + } + + // Drop connection for a specific IP + private dropConnection(ip: string): void { + try { + this.udpSocket.removeAllListeners('message'); + } catch (err: any) { + console.error(`Error dropping connection to ${ip}: ${err.message}`); + } + } + + // Get the subnet (e.g., 192.168.1) + private getSubnet(): string { + const interfaces = os.networkInterfaces(); + for (const iface of Object.values(interfaces)) { + for (const address of iface || []) { + if (address.family === 'IPv4' && !address.internal) { + return address.address.split('.').slice(0, 3).join('.'); + } + } + } + return ''; + } + + // Get IP range (assuming /24 subnet) + private getIPRange(subnet: string): string[] { + const ipRange = []; + for (let i = 1; i < 255; i++) { + ipRange.push(`${subnet}.${i}`); + } + return ipRange; + } + + // Filter only active IPs by pinging each IP in the range + private async filterActiveIps(ipRange: string[]): Promise { + const activeIps: string[] = []; + + const pingPromises = ipRange.map(ip => ping.promise.probe(ip, { timeout: 1 })); + + const pingResults = await Promise.all(pingPromises); + + for (const result of pingResults) { + if (result.alive) { + activeIps.push(result.host); + } + } + + return activeIps; + } +} diff --git a/CEO/src/network/udp/udp_server.ts b/CEO/src/network/udp/udp_server.ts new file mode 100644 index 0000000..b87b82f --- /dev/null +++ b/CEO/src/network/udp/udp_server.ts @@ -0,0 +1,64 @@ +import dgram, { RemoteInfo } from 'dgram'; +import { UdpSocketCommunicator } from "../socket_communicator/udp_socket_communicator"; +import { OperationHandler } from "../operations_base/operation_handler"; +import { GeneralOperations} from "../operations_custom/general_operations"; + +export class UdpServer { + private readonly udpServer: dgram.Socket; + private readonly operationHandler: OperationHandler; + private readonly port: number; + private readonly host: string; + + constructor(host: string, port: number) { + this.udpServer = dgram.createSocket('udp4'); + this.operationHandler = OperationHandler.getInstance(); + this.host = host; + this.port = port; + + // Load only the GeneralOperations into the operation handler + this.operationHandler.loadPlugin(new GeneralOperations()); + } + + // Start the UDP server + public start(): void { + this.udpServer.on('message', this.handleUdpMessages.bind(this)); + this.udpServer.on('error', this.handleError); + this.udpServer.on('listening', this.handleListening.bind(this)); + + // Bind the server to the UDP port and host + this.udpServer.bind(this.port, this.host); + } + + // Handle incoming UDP messages + private async handleUdpMessages(msg: Buffer, rinfo: RemoteInfo): Promise { + const ip = rinfo.address; + const port = rinfo.port; + + console.log(`Received message from ${ip}:${port}`); + + // Create a temporary communicator for the incoming message + const communicator = new UdpSocketCommunicator(this.udpServer, ip, port, this.operationHandler); + + // Process the incoming message using the communicator + communicator.handleIncomingMessage(msg.toString()); + + const communicatorResult = communicator.getHandlerResult(); + if (communicatorResult) { + // Send response back to the client using the temporary communicator + await communicator.sendMessage(communicatorResult.operationCode, communicatorResult.metaInfo); + } else { + console.error(`No handler result for ${ip}:${port}`); + } + } + + // Handle UDP server errors + private handleError(err: Error): void { + console.error(`UDP server error:\n${err.stack}`); + } + + // Handle when the UDP server starts listening + private handleListening(): void { + const address = this.udpServer.address(); + console.log(`UDP server listening on ${address.address}:${address.port}`); + } +} diff --git a/CEO/src/renderer/css/ip_submit.css b/CEO/src/renderer/css/ip_submit.css deleted file mode 100644 index 09fbc06..0000000 --- a/CEO/src/renderer/css/ip_submit.css +++ /dev/null @@ -1,98 +0,0 @@ -body, html { - height: 100%; - width: 100%; - margin: 0; - padding: 0; - - font-family: "Roboto", sans-serif; - font-weight: 500; - font-style: normal; - - background-image: url('../assets/background.png'); - background-size: cover; - background-repeat: no-repeat; - -} - -.container { - opacity: 0; - - display: flex; - flex-direction: row; - justify-content: space-evenly; - align-items: center; - min-height: 100vh; -} - - -.ip-form { - background-color: #535C91; - opacity: 71; - padding: 9vh 3vh 5vh; - border-radius: 1rem; - box-shadow: 10px 10px 10px rgba(0, 0, 0, 0.9); - width: 25%; -} - -.ip-form-title { - margin: 0 0 5vh 0; - text-align: center; -} - -.ip-form-title h2 { - color: #FFFFFF; - font-size: 5vh; - margin: 0; - padding: 0; -} - -.ip-form hr { - width: 40%; -} - -.ip-form-content { - display: flex; - flex-wrap: wrap; - justify-content: center; -} - -input { - text-align: center; - color: white; - font-weight: bold; - font-size: 1.2rem; - width: 70%; - padding: 1rem; - margin: 0.7rem; - background-color: #1B1A55; - border-radius: 10px; -} - -button { - font-weight: bold; - width: 35%; - font-size: 1rem; - padding: 10px; - border: none; - border-radius: 5px; - margin-top: 1rem; - cursor: pointer; -} - -button:hover { - filter: brightness(85%); -} - -.ip-form-footer { - margin-top: 3vh; - display: flex; - flex-wrap: wrap; - align-content: center; - align-items: center; - justify-content: space-evenly; -} - -button[name="submit"] { - background-color: #F44336; - color: white; -} \ No newline at end of file diff --git a/CEO/src/renderer/css/manage_departments.css b/CEO/src/renderer/css/manage_departments.css deleted file mode 100644 index d614ce3..0000000 --- a/CEO/src/renderer/css/manage_departments.css +++ /dev/null @@ -1,201 +0,0 @@ -body, html { - height: 100%; - width: 100%; - margin: 0; - padding: 0; - - font-family: "Roboto", sans-serif; - font-weight: 500; - font-style: normal; - - background-image: url('../assets/background.png'); - background-size: cover; - background-repeat: no-repeat; - -} - -.container { - opacity: 0; - - display: flex; - flex-direction: row; - justify-content: space-evenly; - align-items: center; - min-height: 100vh; - text-align: center; -} - -h1, h2{ - padding: 0; - margin: 0; -} - -.left_block{ - display: flex; - flex-direction: column; - background-color: #535C91; - opacity: 71; - padding: 2rem; - border-radius: 1rem; - box-shadow: 10px 10px 10px rgba(0, 0, 0, 0.9); - color: white; -} - -.left_block_top{ - text-align: left; - width: 50%; - display: flex; - flex-wrap: wrap; - flex-direction: column; - justify-content: left; -} - - -.left_block_top hr{ - margin: 0 0 1rem 0; -} - -.left_block_content{ - display: flex; - justify-content: start; - align-items: center; - margin: 2rem 0 2rem 0; -} - -.left_block_buttons{ - display: flex; - flex-direction: row; - align-content: center; - align-items: center; - justify-content: center; -} - -button{ - margin: 0 1rem 0 1rem; - width: 10vw; - height: 5vh; - - border: none; - border-radius: 10px; - - color: white; - font-size: 1rem; - font-weight: bold; - - text-transform: uppercase; - cursor: pointer; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); - outline: none; - transition: background-color 0.3s ease; -} - -button:hover{ - filter: brightness(85%); -} - -.left_block_footer{ - display: flex; - align-items: center; - justify-content: end; -} - -.right_block{ - display: flex; - flex-wrap: wrap; - flex-direction: column; - align-content: center; - align-items: center; - - background-color: #535C91; - padding: 2vw; - opacity: 71; - border-radius: 1rem; - - box-shadow: 10px 10px 10px rgba(0, 0, 0, 0.9); - color: white; -} - -.security_level_form_title{ - width: 80%; - text-align: left; - display: flex; - flex-wrap: wrap; - flex-direction: column; - justify-content: left; - align-content: start; -} - -.security_level_form_title hr{ - width: 100%; -} - -#security_level_form_content { - list-style-type: none; - padding: 0 7vw 0 0; - height: 30vh; /* Adjust if needed */ - max-height: 30vh; /* Prevents it from growing taller */ - overflow-y: auto; /* Enables vertical scrolling */ - box-sizing: border-box; -} - -input { - text-align: center; - color: white; - font-weight: bold; - font-size: 1.2rem; - width: 40%; - padding: 0.5rem 2rem 0.5rem 2rem; - margin: 0.7rem; - background-color: #1B1A55; - border-radius: 10px; -} - -.hide { - display: none; -} - -.over { - background-color: #f1f1f1; -} - - -#security_level_form_content li { - cursor: move; - padding: 0.5rem 1rem 0.5rem 1rem; - margin: 10px; - background-color: #1B1A55; - border-radius: 1rem; - border: none; - box-sizing: border-box; /* Includes padding and border in the element's total width and height */ -} - -#security_level_form_content::-webkit-scrollbar { - width: 10px; /* Reduced width of the scrollbar by 2px */ -} - -#security_level_form_content::-webkit-scrollbar-track { - background: #1B1A55; /* Updated track color */ -} - -#security_level_form_content::-webkit-scrollbar-thumb { - background: #535C91; /* Updated handle color */ - border: 2px solid #1B1A55; /* Adding a border to effectively reduce the thumb size */ -} - -#security_level_form_content::-webkit-scrollbar-thumb:hover { - background: #555; /* Updated handle color on hover */ -} - -button[name="back"]{ - background-color: #23BDEE; -} - -button[name="submit"], button[name="create"]{ - background-color: #F44336; -} - -button[name="select_file"]{ - margin: 0; - width: 8vw; - background-color: #1B1A55; -} \ No newline at end of file diff --git a/CEO/src/renderer/css/manage_users.css b/CEO/src/renderer/css/manage_users.css deleted file mode 100644 index 67c5662..0000000 --- a/CEO/src/renderer/css/manage_users.css +++ /dev/null @@ -1,121 +0,0 @@ -body, html { - height: 100%; - width: 100%; - margin: 0; - padding: 0; - - font-family: "Roboto", sans-serif; - font-weight: 500; - font-style: normal; - - background-image: url('../assets/background.png'); - background-size: cover; - background-repeat: no-repeat; - -} - -.container { - opacity: 0; - - display: flex; - flex-direction: row; - justify-content: space-evenly; - align-items: center; - min-height: 100vh; -} - -.signup-form { - background-color: #535C91; - opacity: 71; - padding: 8vh 3vh 5vh; - border-radius: 1rem; - box-shadow: 10px 10px 10px rgba(0, 0, 0, 0.9); - width: 25%; -} - -.signup-form-title{ - margin-bottom: 5vh; - color: #FFFFFF; - text-align: center; -} - -.signup-form-title h2 { - color: #FFFFFF; - margin: 0; - padding: 0; - font-size: 5vh; -} - -.signup-form-title hr{ - width:30%; -} - -.signup-form-content{ - display: flex; - margin-left: 2rem; - flex-direction: column; - align-items: start; - - font-size: 1.2rem; - color: white; - font-weight: bold; - - height: 17vh; /* Fixed height */ - width: 80%; /* Full width */ - overflow: auto; /* Enable scrolling */ -} - -.signup-form-content::-webkit-scrollbar { - width: 10px; /* Reduced width of the scrollbar by 2px */ -} - -.signup-form-content::-webkit-scrollbar-track { - background: #1B1A55; /* Updated track color */ -} - -.signup-form-content::-webkit-scrollbar-thumb { - background: #535C91; /* Updated handle color */ - border: 2px solid #1B1A55; /* Adding a border to effectively reduce the thumb size */ -} - -.signup-form-content::-webkit-scrollbar-thumb:hover { - background: #555; /* Updated handle color on hover */ -} - -.signup-form-content input{ - margin: 0.5rem; -} - -.signup-form-footer{ - margin-top: 7vh; - display: flex; - flex-wrap: wrap; - justify-content: space-between; - align-content: center; - align-items: center; -} - -button { - font-weight: bold; - width: 35%; - font-size: 1rem; - padding: 10px; - border: none; - border-radius: 5px; - margin-top: 5px; - cursor: pointer; -} - -button:hover{ - filter: brightness(85%); -} - -.signup-form button[name="delete"] { - background-color: #F44336; - color: white; -} - -.signup-form button[name="back"] { - background-color: #23BDEE; - color: white; -} \ No newline at end of file diff --git a/CEO/src/renderer/css/sending_file_confirmation.css b/CEO/src/renderer/css/sending_file_confirmation.css deleted file mode 100644 index ea7177f..0000000 --- a/CEO/src/renderer/css/sending_file_confirmation.css +++ /dev/null @@ -1,39 +0,0 @@ -body, html { - height: 100%; - width: 100%; - margin: 0; - padding: 0; - - font-family: "Roboto", sans-serif; - font-weight: 500; - font-style: normal; - - background-image: url('../assets/background.png'); - background-size: cover; - background-repeat: no-repeat; - -} - -.container { - opacity: 0; - - display: flex; - flex-direction: row; - justify-content: space-evenly; - align-items: center; - min-height: 100vh; - text-align: center; /* Center the text for all child elements */ -} - -.header{ - color: #1B1A55; - font-size: 4vh; - text-transform: uppercase; - line-height: 2.5rem; - margin-bottom: 10rem; -} - -img{ - width:20%; - height: 20%; -} diff --git a/CEO/src/renderer/html/alert_modal.html b/CEO/src/renderer/html/alert_modal.html deleted file mode 100644 index 340c97b..0000000 --- a/CEO/src/renderer/html/alert_modal.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - Alert Modal - - -
-
-
- -

-
- -
-
- - diff --git a/CEO/src/renderer/html/decrypting_backup.html b/CEO/src/renderer/html/decrypting_backup.html deleted file mode 100644 index fadaff9..0000000 --- a/CEO/src/renderer/html/decrypting_backup.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - Sending file - - -
-
-

DECRYPTING BACKUP!

-

PlEASE WAIT

- Description of GIF - -
-
- - diff --git a/CEO/src/renderer/html/ip_submit.html b/CEO/src/renderer/html/ip_submit.html deleted file mode 100644 index e4c7f0d..0000000 --- a/CEO/src/renderer/html/ip_submit.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - IP Submit - - -
-
-
-

IP Config

-
-
-
- -
- -
-
- - diff --git a/CEO/src/renderer/html/login.html b/CEO/src/renderer/html/login.html deleted file mode 100644 index 335c50d..0000000 --- a/CEO/src/renderer/html/login.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - Login - - -
-
-

DO WE KNOW

-

EACH OTHER?

-
- -
- - diff --git a/CEO/src/renderer/html/main_menu.html b/CEO/src/renderer/html/main_menu.html deleted file mode 100644 index 282b56d..0000000 --- a/CEO/src/renderer/html/main_menu.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - Main Page - - - -
-
-
-
-

WELCOME BACK,

-

-

Hope you have a productive day!

-
- -
-
-
- - - -
-
- - - - -
-
- - - -
-
- -
-
-
-

NOTIFICATIONS

-
-
-
- -
-
-
- - diff --git a/CEO/src/renderer/html/manage_departments.html b/CEO/src/renderer/html/manage_departments.html deleted file mode 100644 index b7915f9..0000000 --- a/CEO/src/renderer/html/manage_departments.html +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - Manage Departments - - -
-
-
-

CREATE NEW DEPARTMENT

-
-
-
- -
- -
-
-
-

SECURITY

-

LEVELS

-
-
-
    - -
- -
- -
-
-
- - diff --git a/CEO/src/renderer/html/manage_users.html b/CEO/src/renderer/html/manage_users.html deleted file mode 100644 index db47505..0000000 --- a/CEO/src/renderer/html/manage_users.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - Manage Users - - -
- -
- - diff --git a/CEO/src/renderer/html/profile.html b/CEO/src/renderer/html/profile.html deleted file mode 100644 index 72c9140..0000000 --- a/CEO/src/renderer/html/profile.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - Profile - - -
-
-
-

Profile

-
-
-
- - -
- -
-
- - diff --git a/CEO/src/renderer/html/sending_file_confirmation.html b/CEO/src/renderer/html/sending_file_confirmation.html deleted file mode 100644 index ca50d3a..0000000 --- a/CEO/src/renderer/html/sending_file_confirmation.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - ; - - Sending file - - -
-
-

SENDING THE FILE!

-

PlEASE WAIT

- Description of GIF - -
-
- - diff --git a/CEO/src/renderer/js/alert_modal.js b/CEO/src/renderer/js/alert_modal.js deleted file mode 100644 index 0367509..0000000 --- a/CEO/src/renderer/js/alert_modal.js +++ /dev/null @@ -1,11 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const closeButton = document.getElementById('closeButton'); - closeButton.addEventListener('click', () => { - window.electronAPI.closeAlertWindow(); - }); -}); - -function showAlert(message) { - const modalMessage = document.getElementById('modal-message'); - modalMessage.textContent = message; -} diff --git a/CEO/src/renderer/js/ip_submit.js b/CEO/src/renderer/js/ip_submit.js deleted file mode 100644 index bcc69e9..0000000 --- a/CEO/src/renderer/js/ip_submit.js +++ /dev/null @@ -1,34 +0,0 @@ -document.addEventListener('DOMContentLoaded', async function () { - const submitButton = document.getElementById('submit'); - const ipInput = document.getElementById('ipInput'); - - submitButton.addEventListener('click', async function (e) { - e.preventDefault(); - try { - const ipAddress = ipInput.value.trim(); - if (!ipAddress) { - throw new Error('Please enter an IP address.'); - } - - fetch(`http://${ipAddress}/heartbeat`) - .then(async response => { - if (!response.ok) { - throw new Error('Test failed. Check ip and server.'); - } - - await window.electronAPI.writeFile('ipConfig.json', JSON.stringify({ip: ipAddress})); - fadeOut('login.html'); - }) - .catch(async error => { - await window.electronAPI.showAlert('Can\'t reach server.') - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error)); - }); - } catch (error) { - console.error('Error:', error.message); - await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error)); - } - }); -}); diff --git a/CEO/src/renderer/js/login.js b/CEO/src/renderer/js/login.js deleted file mode 100644 index 2109265..0000000 --- a/CEO/src/renderer/js/login.js +++ /dev/null @@ -1,90 +0,0 @@ -document.addEventListener('DOMContentLoaded', async function () { - const submitButton = document.getElementById('submit'); - - const signupDataExists = await window.electronAPI.checkFileExists('signupData.json'); - if (signupDataExists) { - await window.electronAPI.deleteFile('signupData.json'); - } - - let ip = ''; - await window.electronAPI.readFile('ipConfig.json') - .then(result => { - const jsonData = JSON.parse(result.content); - ip = jsonData.ip; - }) - - const fileExists = await window.electronAPI.checkFileExists('loginData.json'); - if (fileExists) { - await window.electronAPI.readFile('loginData.json') - .then(async result => { - const loginData = JSON.parse(result.content); - const {email, password} = loginData; - - await fetch(`http://${ip}/ceo/login`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': 'uc_api' - }, - body: JSON.stringify({ - email: email, - password: password - }) - }).then(async response => { - if (response.ok) { - await window.electronAPI.killBeforeLogout(); - await window.electronAPI.startMainProcesses(); - fadeOut('main_menu.html'); - } else { - await window.electronAPI.deleteFile('loginData.json'); - } - }).catch(error => { - console.error(error); - }); - }) - .catch(async error => { - console.error('Can\'t read loginData'); - await window.electronAPI.deleteFile('loginData.json'); - }); - } - - submitButton.addEventListener('click', async function (e) { - e.preventDefault(); - console.log('Submit button clicked'); - - const form = document.getElementById('loginForm'); - const formData = new FormData(form); - - const email = formData.get('email'); - const password = formData.get('password'); - - await fetch(`http://${ip}/ceo/login`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': 'uc_api' - }, - body: JSON.stringify({ - email: email, - password: password - }) - }).then(async response => { - if (!response.ok) { - const data = await response.json(); - throw new Error(data.message); - } - - return response.json(); - }).then(async data => { - console.log(data); - await window.electronAPI.writeFile('loginData.json', JSON.stringify(data.data, null, 2)); - await window.electronAPI.startMainProcesses(); - fadeOut('main_menu.html'); - }) - .catch(async error => { - await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error)); - }) - }); -}); \ No newline at end of file diff --git a/CEO/src/renderer/js/main_menu.js b/CEO/src/renderer/js/main_menu.js deleted file mode 100644 index 75a3ee5..0000000 --- a/CEO/src/renderer/js/main_menu.js +++ /dev/null @@ -1,205 +0,0 @@ -document.addEventListener('DOMContentLoaded', async function () { - await insertUsername(); - setInterval(loadReceivedFiles, 3000); // Call loadReceivedFiles every 5000 ms (5 seconds) - - const backupButton = document.getElementById('backup'); - const shareButton = document.getElementById('share_dir'); - - const manageDepartmentButton = document.getElementById('manage_department'); - const manageUsersButton = document.getElementById('manage_users'); - const changeInfoButton = document.getElementById('change_info'); - const shareFileButton = document.getElementById('share_file'); - const decryptButton = document.getElementById('decrypt'); - const logoutButton = document.getElementById('logout'); - - async function loadReceivedFiles() { - const checkFileReceived = await window.electronAPI.checkFileExists('filesReceived.json'); - if (!checkFileReceived) { - return; - } - - try { - const fileData = await window.electronAPI.readFile('filesReceived.json'); - const filesJson = JSON.parse(fileData.content); - const receivedFiles = filesJson.receivedFiles; - - const notificationsDiv = document.getElementById('notifications'); - const existingButtons = Array.from(notificationsDiv.children).map(button => button.getAttribute('data-filepath')); - - receivedFiles.forEach(filePath => { - // Check if the button for this file path already exists - if (!existingButtons.includes(filePath)) { - const button = document.createElement('button'); - button.setAttribute('data-filepath', filePath); // Set a custom attribute to track the file path - button.name = 'notification'; - button.textContent = 'You received a file'; - button.addEventListener('click', () => handleFileReceivedButtonPressed(filePath, button)); - notificationsDiv.appendChild(button); - } - }); - } catch (error) { - console.error('Error loading received files:', error); - } - } - - async function handleFileReceivedButtonPressed(filePath, button) { - console.log('Notification button clicked!'); - - // Open file in file explorer - await window.electronAPI.showFileInExplorer(filePath) - .then(() => console.log('File explorer opened')) - .catch(error => console.error('Error opening file explorer:', error)); - - // Remove the button - button.remove(); - - // Call to remove the path from the JSON file - await window.electronAPI.removePathFromReceivedFiles(filePath) - .then(() => console.log('Path removed from received files')) - .catch(error => console.error('Error removing path:', error)); - } - - - checkDirBackupFileExists() - .then(() => console.log('verificare backupDir facuta')); - - checkShareDirFileExists() - .then(() => console.log('verificare ShareDir facuta')); - - backupButton.addEventListener('click', function () { - console.log('Set backup directory button clicked!'); - - window.electronAPI.openJsonDirConfigDialog('dirBackup.json') - .then(() => console.log('Back-up directory set')) - .catch(async error => await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error))); - }); - - shareButton.addEventListener('click', function () { - console.log('Set backup directory button clicked!'); - - window.electronAPI.openJsonDirConfigDialog('dirShare.json') - .then(() => console.log('Back-up directory set')) - .catch(async error => await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error))); - }); - - manageUsersButton.addEventListener('click', async function(){ - fadeOut('manage_users.html'); - }); - - changeInfoButton.addEventListener('click', function () { - console.log('Change your info button clicked!'); - fadeOut('profile.html'); - }); - - decryptButton.addEventListener('click', function () { - fadeOut('decrypting_backup.html'); - }); - - changeInfoButton.addEventListener('click', function () { - console.log('Change your info button clicked!'); - fadeOut('profile.html'); - }); - - shareFileButton.addEventListener('click', function () { - console.log('Share a file button clicked!'); - fadeOut('share_file.html'); - }); - - manageDepartmentButton.addEventListener('click', function() { - fadeOut('manage_departments.html'); - }) - - logoutButton.addEventListener('click', async function () { - console.log('Logout button clicked!'); - await window.electronAPI.killBeforeLogout(); - await window.electronAPI.deleteFile('loginData.json'); - fadeOut('login.html'); - }); - - decryptButton.addEventListener('click', async function (){ - - }); - async function checkDirBackupFileExists() { - try { - // Make an IPC call to check file existence - const fileExists = await window.electronAPI.checkFileExists('dirBackup.json'); - - if (!fileExists) { - const notificationsDiv = document.getElementById('notifications'); - const button = document.createElement('button'); - button.id = 'backup_alert'; - button.name = 'alert'; - button.textContent = 'Set your backup directory!'; - button.addEventListener('click', handleBackupButtonPressed); - notificationsDiv.appendChild(button); - } - } catch (error) { - console.error('Error checking file existence:', error); - } - } - - async function checkShareDirFileExists() { - try { - const fileExists = await window.electronAPI.checkFileExists('dirShare.json'); - - if (!fileExists) { - const notificationsDiv = document.getElementById('notifications'); - const button = document.createElement('button'); - button.id = 'share_file_alert'; - button.name = 'alert'; - button.textContent = 'Set your share directory!'; - button.addEventListener('click', handleShareButtonPressed); - notificationsDiv.appendChild(button); - } - } catch (error) { - console.error('Error checking file existence:', error); - } - } - - async function handleBackupButtonPressed() { - console.log('Button clicked!'); - - await window.electronAPI.openJsonDirConfigDialog('dirBackup.json') - .then(() => console.log('Back-up directory set')) - .catch(async error => await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error))); - - const button = document.getElementById('backup_alert'); - button.remove(); - } - - async function handleShareButtonPressed() { - console.log('Button clicked!'); - - await window.electronAPI.openJsonDirConfigDialog('dirShare.json') - .then(() => console.log('Share directory set')) - .catch(async error => await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error))); - - const button = document.getElementById('share_file_alert'); - button.remove(); - } - - async function insertUsername() { - try { - const userData = await window.electronAPI.readFile('loginData.json'); - const userJson = JSON.parse(userData.content); - const username = userJson.name; - - const usernameField = document.getElementById('username_field'); - if (usernameField) { - usernameField.textContent = username + '!'; - } - } catch (error) { - console.error('Error loading username:', error); - const usernameField = document.getElementById('username_field'); - usernameField.textContent = 'User!'; - } - } -}); diff --git a/CEO/src/renderer/js/manage_departments.js b/CEO/src/renderer/js/manage_departments.js deleted file mode 100644 index 4db7261..0000000 --- a/CEO/src/renderer/js/manage_departments.js +++ /dev/null @@ -1,175 +0,0 @@ -document.addEventListener('DOMContentLoaded', async function () { - let ip = ''; - await window.electronAPI.readFile('ipConfig.json') - .then(result => { - const jsonData = JSON.parse(result.content); - ip = jsonData.ip; - }) - - const result = await window.electronAPI.readFile('loginData.json'); - const loginData = JSON.parse(result.content); - const password = loginData.password; - - const backButton = document.querySelector('button[name="back"]'); - const createButton = document.querySelector('button[name="create"]'); - const submitButton = document.querySelector('button[name="submit"]'); - - backButton.addEventListener('click', function () { - fadeOut('main_menu.html'); - console.log('Back button clicked'); - }); - - createButton.addEventListener('click', async function (e) { - e.preventDefault(); - const departmentName = document.querySelector('input[name="text"]').value; - if (!departmentName) { - await window.electronAPI.showAlert('Department name is required.') - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error)); - return; // Exit if no name is provided - } - console.log(`Creating department: ${departmentName}`); - - const url = `http://${ip}/ceo/departments`; - const headers = { - 'Content-Type': 'application/json', - 'x-api-key': 'uc_api', - 'ceo_password': password - }; - console.log(headers); - console.log(password); - const body = JSON.stringify({ name: departmentName }); - - try { - const response = await fetch(url, { - method: 'POST', - headers: headers, - body: body - }); - - if (response.ok) { - const data = await response.json(); - await window.electronAPI.showAlert(data.message); - document.querySelector('input[name="text"]').value = ''; - fadeOut('manage_departments.html'); - } else { - console.log("eroare"); - const errorData = await response.json(); - await window.electronAPI.showAlert(errorData.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error)); - } - } catch (error) { - console.log("error"); - await window.electronAPI.showAlert(error) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error)); - } - }); - - submitButton.addEventListener('click', function () { - // Handle the "Submit" button functionality here - console.log('Submit button clicked'); - saveOrder(); - }); - - document.addEventListener('DOMContentLoaded', function () { - const list = document.getElementById('security_level_form_content'); - let draggedItem = null; - - for (const item of list.querySelectorAll('li')) { - item.setAttribute('draggable', true); - - item.addEventListener('dragstart', function (e) { - draggedItem = this; - setTimeout(() => this.classList.add('hide'), 0); - }); - - item.addEventListener('dragend', function (e) { - setTimeout(() => this.classList.remove('hide'), 0); - }); - - item.addEventListener('dragover', function (e) { - e.preventDefault(); - }); - - item.addEventListener('dragenter', function (e) { - e.preventDefault(); - this.classList.add('over'); - }); - - item.addEventListener('dragleave', function (e) { - this.classList.remove('over'); - }); - - item.addEventListener('drop', function (e) { - e.preventDefault(); - this.classList.remove('over'); - if (this !== draggedItem) { - const items = Array.from(list.querySelectorAll('li')); - const draggedIndex = items.indexOf(draggedItem); - const droppedIndex = items.indexOf(this); - - if (draggedIndex < droppedIndex) { - this.after(draggedItem); - } else { - this.before(draggedItem); - } - } - }); - } - }); - - function saveOrder() { - const listItems = document.querySelectorAll('#security_level_form_content li'); - const order = Array.from(listItems).map(item => item.textContent.trim()); - } - - async function loadOrder() { - const url = `http://${ip}/users/departments`; // Replace {serverip} with the actual IP address of the server - - try { - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': 'uc_api' - } - }); - - if (response.ok) { - let departments = await response.json(); - const list = document.getElementById('security_level_form_content'); - list.innerHTML = ''; // Clear existing items - departments = departments.data; - - // Check if there's a stored order in localStorage and reorder the departments array accordingly - const storedOrder = JSON.parse(localStorage.getItem('listOrder')); - if (storedOrder) { - storedOrder.forEach(itemKey => { - if (departments[itemKey]) { - appendDepartmentToList(departments[itemKey], list); - } - }); - } else { - Object.keys(departments).forEach(key => { - appendDepartmentToList(departments[key], list);}); - } - } else { - console.error('Failed to fetch departments:', response.status); - } - } catch (error) { - console.error('Error making the request:', error); - } - } - - function appendDepartmentToList(department, list) { - const li = document.createElement('li'); - li.textContent = department.name; - li.id = `department-${department.key}`; // Use a unique ID if possible, here it's prefixed with 'department-' - li.setAttribute('draggable', true); - list.appendChild(li); - } - - await loadOrder(); -}); diff --git a/CEO/src/renderer/js/manage_users.js b/CEO/src/renderer/js/manage_users.js deleted file mode 100644 index 72f49ca..0000000 --- a/CEO/src/renderer/js/manage_users.js +++ /dev/null @@ -1,94 +0,0 @@ -document.addEventListener('DOMContentLoaded', async function() { - let ip = ''; - await window.electronAPI.readFile('ipConfig.json') - .then(result => { - const jsonData = JSON.parse(result.content); - ip = jsonData.ip; - }) - - const result = await window.electronAPI.readFile('loginData.json'); - const loginData = JSON.parse(result.content); - const password = loginData.password; - const ceoId = loginData.id; - - const loadUsers = (async () => { - const url = `http://${ip}/users`; - - try { - const response = await fetch(url, { - method: 'GET', - headers: { - 'x-api-key': 'uc_api' - } - }); - if (response.ok) { - let users = await response.json(); - users = users.data; - const formContent = document.querySelector('.signup-form-content'); - formContent.innerHTML = ''; - - users.forEach(user => { - if(user.id !== ceoId) { - const label = document.createElement('label'); - const radioInput = document.createElement('input'); - radioInput.type = 'radio'; - radioInput.name = 'dept'; - radioInput.value = user.id; // Set user ID as value - label.appendChild(radioInput); - label.appendChild(document.createTextNode(user.name)); // User's name for display - formContent.appendChild(label); - } - }); - } else { - console.error('Failed to fetch users:', response.status); - } - } catch (error) { - console.error('Error making the request:', error); - } - }); - - await loadUsers(); - - const deleteButton = document.querySelector('button[name="delete"]'); - deleteButton.addEventListener('click', async function(event) { - event.preventDefault(); // Prevent the default form submission - const selectedUser = document.querySelector('input[type="radio"][name="dept"]:checked'); - - if (!selectedUser) { - await window.electronAPI.showAlert('Please select a user to delete.') - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error)); - return; - } - - const userId = selectedUser.value; - const deleteUrl = `http://${ip}/ceo/users/${userId}`; - - try { - const response = await fetch(deleteUrl, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': 'uc_api', - 'ceo_password': password - } - }); - const data = await response.json(); - - await window.electronAPI.showAlert(data.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error)); - fadeOut('manage_users.html'); - } catch (error) { - await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error)); - } - }); - - const backButton = document.querySelector('button[name="back"]'); - backButton.addEventListener('click', function() { - fadeOut('main_menu.html'); - console.log('Back button clicked'); - }); -}); diff --git a/CEO/src/renderer/js/profile.js b/CEO/src/renderer/js/profile.js deleted file mode 100644 index f679f7b..0000000 --- a/CEO/src/renderer/js/profile.js +++ /dev/null @@ -1,74 +0,0 @@ -document.addEventListener('DOMContentLoaded', async function () { - - let ip = ''; - await window.electronAPI.readFile('ipConfig.json') - .then(result => { - const jsonData = JSON.parse(result.content); - ip = jsonData.ip; - }) - - const result = await window.electronAPI.readFile('loginData.json'); - const loginData = JSON.parse(result.content); - - const emailInput = document.querySelector('input[name="email"]'); - const usernameInput = document.querySelector('input[name="username"]'); - - emailInput.value = loginData.email; - usernameInput.value = loginData.name; - const password = loginData.password; - - const backButton = document.querySelector('button[name="login"]'); - const submitButton = document.querySelector('button[name="submit"]'); - - backButton.addEventListener('click', function () { - console.log('Back button clicked!'); - fadeOut('main_menu.html') - }); - - submitButton.addEventListener('click', async function (e) { - e.preventDefault(); - console.log('Submit button clicked!'); - - const emailInput = document.querySelector('input[name="email"]'); - const usernameInput = document.querySelector('input[name="username"]'); - - const result = await window.electronAPI.readFile('loginData.json'); - const loginData = JSON.parse(result.content); - - const {id, department} = loginData; - const email = emailInput.value; - const name = usernameInput.value; - - await fetch(`http://${ip}/ceo`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': 'uc_api', - 'ceo_password': password - }, - body: JSON.stringify({ - name: name, - email: email, - }) - }).then(async result => { - const data = await result.json(); - if (!result.ok) { - throw new Error(data.message); - } - - await window.electronAPI.writeFile('loginData.json', JSON.stringify({ - id: id, - name: name, - email: email, - password: password, - department: department - })); - - fadeOut('main_menu.html'); - }).catch(async error => { - await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error)); - }) - }); -}); diff --git a/CEO/src/renderer/js/sending_file_confirmation.js b/CEO/src/renderer/js/sending_file_confirmation.js deleted file mode 100644 index 9fe8f75..0000000 --- a/CEO/src/renderer/js/sending_file_confirmation.js +++ /dev/null @@ -1,65 +0,0 @@ -document.addEventListener("DOMContentLoaded", async function () { - async function performUploads() { - try { - const uploadFile = await window.electronAPI.readFile('usersDestTemp.json'); - const uploadData = JSON.parse(uploadFile.content); - - const uploadPromises = uploadData.users.map(async (user) => { - try { - const fileResponse = await fetch(uploadData.filePath); - if (!fileResponse.ok) { - throw new Error(`HTTP error when trying to fetch the file: status ${fileResponse.statusText}`); - } - const fileBlob = await fileResponse.blob(); - - await timeout(5000); // Timeout to simulate delay or wait - const url = `http://${user.destIp}:${user.destPort}/share_file`; - - const uploadResponse = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/octet-stream', - 'X-IdUser': user.userId, - 'X-NameOfFile': user.fileName, - 'X-SizeOfFile': fileBlob.size - }, - body: fileBlob - }); - - if (!uploadResponse.ok) { - const response = await uploadResponse.json(); - throw new Error(`HTTP error during file upload to ${user.userId}: status ${response.message}`); - } - - return {userId: user.userId, success: true, message: `Upload successful for user ${user.userId}`}; - } catch (error) { - console.error(`Failed to upload for user ${user.userId}:`, error); - return {userId: user.userId, success: false, message: `Upload failed for user ${user.userId}`}; - } - }); - - const results = await Promise.all(uploadPromises); - results.forEach(result => { - if (result.success) { - console.log(result.message); - } else { - console.error(result.message); - } - }); - console.log('All files processed. Check the console for detailed results.'); - } catch (error) { - console.error('An error occurred during uploads:', error); - } finally { - await window.electronAPI.deleteFile('usersDestTemp.json'); - } - } - - function timeout(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - await performUploads(); - await timeout(3000); - fadeOut('main_menu.html'); // Make sure this function is properly defined or available in your context -}); - diff --git a/CEO/src/renderer/js/share_file.js b/CEO/src/renderer/js/share_file.js deleted file mode 100644 index bf2717f..0000000 --- a/CEO/src/renderer/js/share_file.js +++ /dev/null @@ -1,139 +0,0 @@ -document.addEventListener("DOMContentLoaded", async function () { - let pathToFile = ''; - let serverIp = ''; - await window.electronAPI.readFile('ipConfig.json') - .then(result => { - const jsonData = JSON.parse(result.content); - serverIp = jsonData.ip; - }) - - function updateFileName() { - const fileNameElement = document.getElementById('fileName'); - if (pathToFile.trim() === '') { - fileNameElement.textContent = 'No file chosen'; - } else { - fileNameElement.textContent = pathToFile.split('\\').pop().split('/').pop(); - } - } - - updateFileName(); - - async function fetchUsersAndCreateCheckboxes() { - const result = await window.electronAPI.readFile('loginData.json'); - const loginData = JSON.parse(result.content); - - const {id} = loginData; - - fetch(`http://${serverIp}/users`, { - method: 'GET', - headers: { - 'x-api-key': 'uc_api' - } - }) - .then(response => response.json()) - .then(data => { - const usersDiv = document.querySelector('.choose_user_form_content'); - usersDiv.innerHTML = ''; - data['data'].forEach(user => { - if (user.id !== id) { - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.name = 'users'; - checkbox.value = user.id; - - const label = document.createElement('label'); - label.innerHTML = `${user.name}`; - label.insertBefore(checkbox, label.firstChild); // Insert checkbox before the label's first child - - usersDiv.appendChild(label); - } - }); - }) - .catch(error => console.error('Error fetching users:', error)); - } - - document.getElementById('selectFile').addEventListener('click', async function () { - console.log('Select file button clicked'); - - try { - pathToFile = await window.electronAPI.openFileDialog(); - updateFileName(); - } catch (error) { - console.error('Error opening file dialog:', error); - } - }); - - document.getElementById('backButton').addEventListener('click', async function () { - console.log('Back button clicked'); - fadeOut('main_menu.html'); - }); - - document.getElementById('submitButton').addEventListener('click', async function (event) { - event.preventDefault(); - console.log('Submit button clicked'); - - // Check if pathToFile has content - if (!pathToFile.trim()) { - await window.electronAPI.showAlert('File not chosen!') - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error)); - return; - } - - const form = document.getElementById('userDestForm'); - const checkboxes = form.querySelectorAll('input[name="users"]'); - const selectedUserIds = Array.from(checkboxes) - .filter(checkbox => checkbox.checked) - .map(checkbox => checkbox.value); - - if (!selectedUserIds.length) { - await window.electronAPI.showAlert('No user selected!') - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error)); - return; - } - - const uploadData = { - filePath: pathToFile, // Use the pathToFile variable - users: [] - }; - - for (const userId of selectedUserIds) { - try { - const ipResponse = await fetch(`http://${serverIp}/backup_schemes/${userId}`,{ - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': 'uc_api', - } - }); - if (!ipResponse.ok) { - throw new Error(`Failed to fetch IP address for user ${userId}: status ${ipResponse.status}`); - } - const { data: destIp } = await ipResponse.json(); - uploadData.users.push({ - userId, - destIp, - destPort: 3000, // Static destination port - fileName: pathToFile.split('\\').pop().split('/').pop() - }); - } catch (error) { - console.error('Error fetching user data:', error); - } - } - - window.electronAPI.writeFile('usersDestTemp.json', JSON.stringify(uploadData, null, 2)) - .then(async () => { - console.log('File saved successfully'); - fadeOut('sending_file_confirmation.html'); - }) - .catch(error => console.error('Failed to save file:', error)); - }); - - function timeout(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - - fetchUsersAndCreateCheckboxes().then(() => console.log('Page rendered')) -}); diff --git a/CEO/src/renderer/js/transition.js b/CEO/src/renderer/js/transition.js deleted file mode 100644 index b5cb1c8..0000000 --- a/CEO/src/renderer/js/transition.js +++ /dev/null @@ -1,16 +0,0 @@ -function fadeIn() { - document.querySelector('.container').classList.remove('fade-out'); - document.querySelector('.container').classList.add('fade-in'); -} - -function fadeOut(destination) { - const container = document.querySelector('.container'); - container.classList.remove('fade-in'); - container.classList.add('fade-out'); - - container.addEventListener('animationend', async () => { - await window.electronAPI.changeContent(destination) - .then(() => console.log('Navigated to dashboard')) - .catch(error => console.error('Error navigating:', error)); - }); -} \ No newline at end of file diff --git a/CEO/src/workers/backup_retrieval_worker.ts b/CEO/src/workers/backup_retrieval_worker.ts new file mode 100644 index 0000000..6e55b4c --- /dev/null +++ b/CEO/src/workers/backup_retrieval_worker.ts @@ -0,0 +1,38 @@ +import { workerData, parentPort } from 'worker_threads'; +import { BackupRetrievalWorker} from '../helpers/backup_retrieval'; // Assuming the class is in the same folder + +// Destructure the data passed from the WorkerManager +const { + userConfigPath, + applicationInfoPath, + clientPort, + destinationPath +}: { + userConfigPath: string, + applicationInfoPath: string, + clientPort: number, + destinationPath: string +} = workerData; + +// Initialize the BackupRetrievalWorker +const backupRetrievalWorker = new BackupRetrievalWorker( + userConfigPath, + applicationInfoPath, + clientPort, + destinationPath +); + +// Start the backup retrieval process +backupRetrievalWorker.start() + .then(() => { + parentPort?.postMessage({ + success: true, + message: 'Backup retrieval completed successfully.' + }); + }) + .catch((error: any) => { + parentPort?.postMessage({ + success: false, + message: `Backup retrieval failed: ${error.message}` + }); + }); diff --git a/CEO/src/workers/resource_coordinator_worker.ts b/CEO/src/workers/resource_coordinator_worker.ts new file mode 100644 index 0000000..93d5856 --- /dev/null +++ b/CEO/src/workers/resource_coordinator_worker.ts @@ -0,0 +1,52 @@ +import { workerData } from 'worker_threads'; +import { UsersInfoFetcher } from '../helpers/users_info_fetcher'; +import {BackupManager} from "../helpers/backup_manager"; +import {FileSharer} from "../helpers/file_sharer"; +import {DepartmentSharer} from "../helpers/department_sharer"; + +// Destructure the required information from workerData +const { usersConfigPath, applicationInfoPath, memoryManagerPath, queueManagerPath, tcpPort } = workerData; + +const usersInfoFetcher = new UsersInfoFetcher(applicationInfoPath, memoryManagerPath, tcpPort); +usersInfoFetcher.start() + .then(() => { + console.log('Users Info Fetcher started successfully'); + }) + .catch((error: any) => { + console.error('Error starting Users Info Fetcher:', error); + }); + +const backupManager = new BackupManager( + usersConfigPath, + applicationInfoPath, + memoryManagerPath, + tcpPort +); + +backupManager.start() + .then(() => { + console.log('Backup Manager started successfully'); + }) + .catch((error: any) => { + console.error('Error starting Backup Manager:', error); + }); + +const fileSharer = new FileSharer(queueManagerPath, tcpPort); +fileSharer.start() + .then(() => { + console.log('File Sharer started successfully'); + }) + .catch((error: any) => { + console.error('Error starting File Sharer:', error); + }); + + +const departmentSharer = new DepartmentSharer(usersConfigPath, applicationInfoPath, memoryManagerPath, tcpPort); +departmentSharer.start() + .then(() => { + console.log('Department Sharer started successfully'); + }) + .catch((error: any) => { + console.error('Error starting Department Sharer:', error); + }); + diff --git a/CEO/src/workers/servers_worker.ts b/CEO/src/workers/servers_worker.ts new file mode 100644 index 0000000..4e6f4b6 --- /dev/null +++ b/CEO/src/workers/servers_worker.ts @@ -0,0 +1,14 @@ +import {UdpServer} from "../network/udp/udp_server"; +import {TcpServer} from "../network/tcp/tcp_server"; +import {workerData} from "worker_threads"; + +let udpServer: UdpServer | null = null; +let tcpServer: TcpServer | null = null; + +const {USER_UDP_PORT, USER_TCP_PORT, HOST} = workerData; + +udpServer = new UdpServer(HOST, USER_UDP_PORT); +udpServer.start(); + +tcpServer = new TcpServer(HOST, USER_TCP_PORT); +tcpServer.start(); \ No newline at end of file diff --git a/CEO/src/workers/watcher_worker.ts b/CEO/src/workers/watcher_worker.ts new file mode 100644 index 0000000..36a7262 --- /dev/null +++ b/CEO/src/workers/watcher_worker.ts @@ -0,0 +1,43 @@ +import {DirectoryWatcher} from "../helpers/directory_watcher"; +import {workerData} from "worker_threads"; + +const { + memoryManagerPath, + applicationInfoPath, +} = workerData; + +const backupDirectoryManager = new DirectoryWatcher(memoryManagerPath, applicationInfoPath, 'backupDirectory'); +const departmentShareManager = new DirectoryWatcher(memoryManagerPath, applicationInfoPath, 'departmentDirectory'); +const shareFileManager = new DirectoryWatcher(memoryManagerPath, applicationInfoPath, 'shareDirectory'); + +// Backup directory manager check loop +const backupIntervalId = setInterval(async () => { + if (await backupDirectoryManager.initialize()) { + clearInterval(backupIntervalId); // Stop the loop once initialized + console.log('BackupDirectoryManager successfully initialized.'); + } else { + console.log('Retrying BackupDirectoryManager initialization...'); + } +}, 60000); // Check every 60 seconds + + +// Department share manager check loop +const departmentIntervalId = setInterval(async () => { + if (await departmentShareManager.initialize()) { + clearInterval(departmentIntervalId); // Stop the loop once initialized + console.log('DepartmentShareManager successfully initialized.'); + } else { + console.log('Retrying DepartmentShareManager initialization...'); + } +}, 60000); // Check every 60 seconds + +// Department share manager check loop +const shareIntervalId = setInterval(async () => { + if (await shareFileManager.initialize()) { + clearInterval(shareIntervalId); // Stop the loop once initialized + console.log('DepartmentShareManager successfully initialized.'); + } else { + console.log('Retrying DepartmentShareManager initialization...'); + } +}, 60000); // Check every 60 seconds + diff --git a/CEO/tsconfig.json b/CEO/tsconfig.json new file mode 100644 index 0000000..a62789c --- /dev/null +++ b/CEO/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "module": "commonjs", + "target": "es6", + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "moduleResolution": "node" + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/CEO/usersInSystem.json b/CEO/usersInSystem.json deleted file mode 100644 index 9069ec4..0000000 Binary files a/CEO/usersInSystem.json and /dev/null differ diff --git a/UC/.env b/UC/.env new file mode 100644 index 0000000..1ec5645 --- /dev/null +++ b/UC/.env @@ -0,0 +1,6 @@ +HOST=0.0.0.0 +UDP_PORT=41234 +TCP_PORT=41233 + +CEO_EMAIL=ceo@domain.com +CEO_PASSWORD=ceo_password diff --git a/UC/backend/.idea/.gitignore b/UC/backend/.idea/.gitignore deleted file mode 100644 index b58b603..0000000 --- a/UC/backend/.idea/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/UC/backend/.idea/.name b/UC/backend/.idea/.name deleted file mode 100644 index 2ff8622..0000000 --- a/UC/backend/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -package.json \ No newline at end of file diff --git a/UC/backend/.idea/UC.iml b/UC/backend/.idea/UC.iml deleted file mode 100644 index 24643cc..0000000 --- a/UC/backend/.idea/UC.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/UC/backend/.idea/backend.iml b/UC/backend/.idea/backend.iml deleted file mode 100644 index 24643cc..0000000 --- a/UC/backend/.idea/backend.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/UC/backend/.idea/jsLibraryMappings.xml b/UC/backend/.idea/jsLibraryMappings.xml deleted file mode 100644 index d23208f..0000000 --- a/UC/backend/.idea/jsLibraryMappings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/UC/backend/.idea/misc.xml b/UC/backend/.idea/misc.xml deleted file mode 100644 index 3668dc8..0000000 --- a/UC/backend/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/UC/backend/.idea/modules.xml b/UC/backend/.idea/modules.xml deleted file mode 100644 index e066844..0000000 --- a/UC/backend/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/UC/backend/.idea/runConfigurations/bin_www.xml b/UC/backend/.idea/runConfigurations/bin_www.xml deleted file mode 100644 index b4001fc..0000000 --- a/UC/backend/.idea/runConfigurations/bin_www.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/UC/backend/.idea/vcs.xml b/UC/backend/.idea/vcs.xml deleted file mode 100644 index 6841f35..0000000 --- a/UC/backend/.idea/vcs.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/UC/backend/README.md b/UC/backend/README.md deleted file mode 100644 index 67665ec..0000000 --- a/UC/backend/README.md +++ /dev/null @@ -1,45 +0,0 @@ -General Overview -- - -The UC serves as the main logistic station for all the client/CEO applications which are used -inside the corporation. This UC stores -- credentials for the users and ceo; -- registered departments, as well security levels and the encrypting/decrypting keys for the files; -- backup schemes for each user, as well as the IP, as well as the IPs where each computer is located; - -The main endpoints of the application are: - -- Users: - - - Can register/login in the system; - - Update the information (username, email, password); - - Change the working department (the client app will prompt the CEO password request to proceed); - - Retrieve the full list of departments for the sign-up / 'change department' procedure; - - Retrieve the full list of users and their IPs for sharing a file - - -- CEO: - - - Can log in the system; - - Can change the email (the password can only be retrieved by the admin); - - Delete users from the system; - - Create / Delete departments, as well as managing the security levels of each department; - - Retrieve the full list of users and their IPs for sharing a file - - -- Admin: - - - Reset the server to default state; - - Retrieve the CEO login information, as well as a list of all users in the system; - - -- Backup Schemes: - - - At a request, will get the ID of a user, their IP and the backup directory structure to place it in the scheme, - as well as the data size of the backup - - -Other Features -- -The UC will also create a separate process which at a set interval of time will retrieve from the admin config, as -well as from the CEO config, the ID so that it will be used to authenticate the users at their endpoints. diff --git a/UC/backend/package-lock.json b/UC/backend/package-lock.json deleted file mode 100644 index 78308fa..0000000 --- a/UC/backend/package-lock.json +++ /dev/null @@ -1,1811 +0,0 @@ -{ - "name": "uc", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "uc", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "axios": "^1.6.8", - "body-parser": "^1.20.2", - "cheerio": "^1.0.0-rc.12", - "cors": "^2.8.5", - "cron": "^3.1.6", - "crypto": "^1.0.1", - "crypto-js": "^4.2.0", - "express": "^4.19.1", - "joi": "^17.12.2", - "json": "^11.0.0", - "uuid": "^9.0.1" - }, - "devDependencies": { - "concurrently": "^8.2.2", - "nodemon": "^3.1.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", - "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", - "dev": true, - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" - }, - "node_modules/@types/luxon": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.8.tgz", - "integrity": "sha512-jYvz8UMLDgy3a5SkGJne8H7VA7zPV2Lwohjx0V8V31+SqAjNmurWMkk9cQhfvlcnXWudBpK9xPM1n4rljOcHYQ==" - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/axios": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", - "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cheerio": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", - "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "htmlparser2": "^8.0.1", - "parse5": "^7.0.0", - "parse5-htmlparser2-tree-adapter": "^7.0.0" - }, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/cheerio-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "dependencies": { - "boolbase": "^1.0.0", - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/concurrently": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", - "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", - "dev": true, - "dependencies": { - "chalk": "^4.1.2", - "date-fns": "^2.30.0", - "lodash": "^4.17.21", - "rxjs": "^7.8.1", - "shell-quote": "^1.8.1", - "spawn-command": "0.0.2", - "supports-color": "^8.1.1", - "tree-kill": "^1.2.2", - "yargs": "^17.7.2" - }, - "bin": { - "conc": "dist/bin/concurrently.js", - "concurrently": "dist/bin/concurrently.js" - }, - "engines": { - "node": "^14.13.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" - } - }, - "node_modules/concurrently/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/concurrently/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/cron": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/cron/-/cron-3.1.6.tgz", - "integrity": "sha512-cvFiQCeVzsA+QPM6fhjBtlKGij7tLLISnTSvFxVdnFGLdz+ZdXN37kNe0i2gefmdD17XuZA6n2uPVwzl4FxW/w==", - "dependencies": { - "@types/luxon": "~3.3.0", - "luxon": "~3.4.0" - } - }, - "node_modules/crypto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", - "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", - "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in." - }, - "node_modules/crypto-js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" - }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/joi": { - "version": "17.12.2", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz", - "integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==", - "dependencies": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, - "node_modules/json": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/json/-/json-11.0.0.tgz", - "integrity": "sha512-N/ITv3Yw9Za8cGxuQqSqrq6RHnlaHWZkAFavcfpH/R52522c26EbihMxnY7A1chxfXJ4d+cEFIsyTgfi9GihrA==", - "bin": { - "json": "lib/json.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/nodemon": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", - "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==", - "dev": true, - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/nodemon/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", - "dev": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dependencies": { - "entities": "^4.4.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", - "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", - "dependencies": { - "domhandler": "^5.0.2", - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/shell-quote": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/spawn-command": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", - "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", - "dev": true - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dev": true, - "dependencies": { - "nopt": "~1.0.10" - }, - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - } - } -} diff --git a/UC/backend/package.json b/UC/backend/package.json deleted file mode 100644 index 45fb798..0000000 --- a/UC/backend/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "uc", - "version": "1.0.0", - "description": "", - "main": "app.js", - "scripts": { - "start": "node ./src/app.js", - "dev": "nodemon ./src/app.js" - }, - "keywords": [], - "author": "Cerbu Andrei- Mihnea", - "license": "ISC", - "dependencies": { - "axios": "^1.6.8", - "body-parser": "^1.20.2", - "cheerio": "^1.0.0-rc.12", - "cors": "^2.8.5", - "cron": "^3.1.6", - "crypto": "^1.0.1", - "crypto-js": "^4.2.0", - "express": "^4.19.1", - "joi": "^17.12.2", - "json": "^11.0.0", - "uuid": "^9.0.1" - }, - "devDependencies": { - "concurrently": "^8.2.2", - "nodemon": "^3.1.0" - } -} diff --git a/UC/backend/src/app.js b/UC/backend/src/app.js deleted file mode 100644 index ffccac2..0000000 --- a/UC/backend/src/app.js +++ /dev/null @@ -1,70 +0,0 @@ -const express = require('express'); -const {json} = require("body-parser"); -const cors = require('cors'); - -const app = express(); -const port = process.env.PORT || 5000; - -app.use(cors({ - origin: '*', // Allow all origins - methods: 'GET,POST,PUT,DELETE,PATCH', // Allow all methods - allowedHeaders: '*', // Allow all headers - credentials: true, // Enable credentials - preflightContinue: false, - optionsSuccessStatus: 204 // Some legacy browsers (IE11, various SmartTVs) choke on 204 -})); -app.use(json()); - -const {apiKey} = require('./config.json'); -app.locals.apiKey = apiKey; - -const { - usersDB, - departmentsDB, - backupSchemesDB, - ceoDB, - adminDB -} = require('./db/jsonDatabaseManager'); - -app.set('usersDB', usersDB); -app.set('departmentsDB', departmentsDB); -app.set('backupSchemesDB', backupSchemesDB); -app.set('ceoDB', ceoDB); -app.set('adminDB', adminDB); - -app.use('/heartbeat', (req, res) => { - return res.status(200).json({message: 'Server ap and running.'}); -}) - -const checkJson = require('./middlewares/checkJson'); -const apiKeyValidation = require('./middlewares/apiKeyValidation'); - -app.use(apiKeyValidation) -app.use(checkJson) - -const usersRouter = require('./routes/users'); -const adminRouter = require('./routes/admin'); -const ceoRouter = require('./routes/ceo'); -const backupSchemesRouter = require('./routes/backup_schemes'); - -app.use('/users', usersRouter); -app.use('/admin', adminRouter); -app.use('/ceo', ceoRouter); -app.use('/backup_schemes', backupSchemesRouter); - -app.use('/heartbeat', (req, res) => { - return res.status(200).json({message: 'Server ap and running.'}); -}) - -app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`); -}); - -// Graceful shutdown function -function gracefulShutdown() { - console.log('\nShutting down!'); - process.exit(0); -} - -process.on('SIGTERM', gracefulShutdown); -process.on('SIGINT', gracefulShutdown); \ No newline at end of file diff --git a/UC/backend/src/config.json b/UC/backend/src/config.json deleted file mode 100644 index ba0ff54..0000000 --- a/UC/backend/src/config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "apiKey": "uc_api" -} \ No newline at end of file diff --git a/UC/backend/src/db/backup_schemes.json b/UC/backend/src/db/backup_schemes.json deleted file mode 100644 index 43e145d..0000000 --- a/UC/backend/src/db/backup_schemes.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "7c0ef9d5-23ed-47a6-bfb4-fdcfce436904": { - "ip": "192.168.169.127", - "directoryStructure": "{\"backup_dir\":{\"files\":[],\"fisiere\":{\"files\":[\"New Microsoft Excel Worksheet.xlsx\",\"New Microsoft Word Document.docx\"]}}}", - "totalSize": 19363 - }, - "59c712d9-e6dd-438f-aec9-31ca2ae750e6": { - "ip": "192.168.169.19", - "directoryStructure": "{\"backup_dir\":{\"files\":[],\"fisiere\":{\"files\":[\"New Microsoft Excel Worksheet.xlsx\",\"New Microsoft Word Document.docx\"]}}}", - "totalSize": 19363 - } -} \ No newline at end of file diff --git a/UC/backend/src/db/ceo.json b/UC/backend/src/db/ceo.json deleted file mode 100644 index 199d171..0000000 --- a/UC/backend/src/db/ceo.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "id": "7c0ef9d5-23ed-47a6-bfb4-fdcfce436904", - "name": "CEO", - "email": "ceo@yourfirm.com", - "password": "b85cfc54e8dda76e72d7bde7b10ce30a", - "department": "CEO" -} \ No newline at end of file diff --git a/UC/backend/src/db/departments.json b/UC/backend/src/db/departments.json deleted file mode 100644 index 4a5578f..0000000 --- a/UC/backend/src/db/departments.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "1": { - "name": "CEO", - "key": "6f444243063271cc1ea3ccb3621dfebc352fd40e2f58eb6c97b26b7e34fde7a1" - }, - "2": { - "name": "HR", - "key": "2c480e5e3fdaca10d4d838b1b16fb44ce0f57238cb4343a839e065f8d6e41dc7" - } -} \ No newline at end of file diff --git a/UC/backend/src/db/jsonDatabaseManager.js b/UC/backend/src/db/jsonDatabaseManager.js deleted file mode 100644 index f96d424..0000000 --- a/UC/backend/src/db/jsonDatabaseManager.js +++ /dev/null @@ -1,91 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -class JSONDatabase { - constructor(filePath) { - this.filePath = filePath; - this.readFile = this.readFile.bind(this); // Bind readFile method to the instance - this.writeFile = this.writeFile.bind(this); // Bind writeFile method to the instance - - } - - readFile() { - if (!fs.existsSync(this.filePath)) { - console.log('File does not exist, returning null:', this.filePath); - return null; - } - try { - const data = fs.readFileSync(this.filePath, 'utf8'); - return JSON.parse(data); - } catch (error) { - console.error('Error reading file:', error); - return null; - } - } - - writeFile(data) { - try { - fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2), { flag: 'w' }); - console.log('File written successfully.'); - } catch (error) { - console.error('Error writing file:', error); - } - } - - isKeyValue(key, value) { - const data = this.readFile(); - if (!data) { - return false; - } - - for (const item of data) { - if (item[key] === value) { - return true; - } - } - return false; - } - - findIndexByKeyValueInArray(key, value) { - const data = this.readFile(); - if (!data || !Array.isArray(data)) { - return -1; - } - - for (let i = 0; i < data.length; i++) { - if (data[i][key] === value) { - return i; - } - } - return -1; - } - - findByKeyValue(key, value) { - const data = this.readFile(); - if (!data || !Array.isArray(data)) { - return null; // Database read failed or data is not an array - } - - for (const item of data) { - if (item[key] === value) { - return item; // Return the object containing the specified key-value pair - } - } - return null; // No matching key-value pair found - } -} - -const usersDB = new JSONDatabase(path.join(__dirname, 'users.json')); -const departmentsDB = new JSONDatabase(path.join(__dirname, 'departments.json')); -const backupSchemesDB = new JSONDatabase(path.join(__dirname, 'backup_schemes.json')); -const ceoDB = new JSONDatabase(path.join(__dirname, 'ceo.json')); -const adminDB = new JSONDatabase(path.join(__dirname + '..', '..', '..', '..', 'frontend', 'config', 'credentials.json')); - -module.exports = { - usersDB, - departmentsDB, - backupSchemesDB, - ceoDB, - adminDB, - JSONDatabase -}; diff --git a/UC/backend/src/db/users.json b/UC/backend/src/db/users.json deleted file mode 100644 index c4f4142..0000000 --- a/UC/backend/src/db/users.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - { - "id": "59c712d9-e6dd-438f-aec9-31ca2ae750e6", - "name": "Andrei Cerbu", - "email": "a@c.com", - "password": "5447045dc3cfaafdb49b365e31dad41eb88616b0b1449930e55fd3a70bd8cee4", - "department": "HR" - } -] \ No newline at end of file diff --git a/UC/backend/src/helpers/httpResponses.js b/UC/backend/src/helpers/httpResponses.js deleted file mode 100644 index 1519d88..0000000 --- a/UC/backend/src/helpers/httpResponses.js +++ /dev/null @@ -1,38 +0,0 @@ -const httpStatus = { - // Informational - CONTINUE: 100, - SWITCHING_PROTOCOLS: 101, - PROCESSING: 102, - - // Success - OK: 200, - CREATED: 201, - ACCEPTED: 202, - NO_CONTENT: 204, - - // Redirection - MOVED_PERMANENTLY: 301, - FOUND: 302, - SEE_OTHER: 303, - NOT_MODIFIED: 304, - TEMPORARY_REDIRECT: 307, - - // Client Error - BAD_REQUEST: 400, - UNAUTHORIZED: 401, - FORBIDDEN: 403, - NOT_FOUND: 404, - METHOD_NOT_ALLOWED: 405, - CONFLICT: 409, - GONE: 410, - UNSUPPORTED_MEDIA_TYPE: 415, - - // Server Error - INTERNAL_SERVER_ERROR: 500, - NOT_IMPLEMENTED: 501, - SERVICE_UNAVAILABLE: 503 -}; - - - -module.exports = {httpStatus}; \ No newline at end of file diff --git a/UC/backend/src/middlewares/apiKeyValidation.js b/UC/backend/src/middlewares/apiKeyValidation.js deleted file mode 100644 index cfa3748..0000000 --- a/UC/backend/src/middlewares/apiKeyValidation.js +++ /dev/null @@ -1,11 +0,0 @@ -const {httpStatus, httpStatusMessages} = require('../helpers/httpResponses') -function apiKeyValidation(req, res, next) { - const apiKey = req.headers['x-api-key']; - - if (apiKey !== req.app.locals.apiKey) { - return res.status(httpStatus.UNAUTHORIZED).json({message: 'Invalid API key'}); - } - next(); -} - -module.exports = apiKeyValidation; \ No newline at end of file diff --git a/UC/backend/src/middlewares/checkJson.js b/UC/backend/src/middlewares/checkJson.js deleted file mode 100644 index 1446c5f..0000000 --- a/UC/backend/src/middlewares/checkJson.js +++ /dev/null @@ -1,16 +0,0 @@ - -const {httpStatus} = require("../helpers/httpResponses"); - -function checkJson(req, res, next) { - if(req.method === 'GET'){ - next() - } - - if (['POST', 'PUT', 'PATCH'].includes(req.method) && req.headers['content-type'] !== 'application/json') { - return res.status(httpStatus.BAD_REQUEST).json({message: "Body missing in action."}); - } - - next(); -} - -module.exports = checkJson; \ No newline at end of file diff --git a/UC/backend/src/models/ceoModifyModel.js b/UC/backend/src/models/ceoModifyModel.js deleted file mode 100644 index d757447..0000000 --- a/UC/backend/src/models/ceoModifyModel.js +++ /dev/null @@ -1,15 +0,0 @@ -const Joi = require('joi'); - -const ceoModifyModel = Joi.object({ - name: Joi.string().required().messages({ - 'any.required': 'CEO name is required', - 'string.empty': 'CEO name must not be empty' - }), - email: Joi.string().email().required().messages({ - 'any.required': 'CEO email is required', - 'string.empty': 'CEO email must not be empty', - 'string.email': 'CEO email must be a valid email address' - }) -}); - -module.exports = ceoModifyModel; \ No newline at end of file diff --git a/UC/backend/src/models/ceoPasswordModel.js b/UC/backend/src/models/ceoPasswordModel.js deleted file mode 100644 index 9cbd3f1..0000000 --- a/UC/backend/src/models/ceoPasswordModel.js +++ /dev/null @@ -1,10 +0,0 @@ -const Joi = require('joi'); - -const ceoPasswordModel = Joi.object({ - password: Joi.string().required().messages({ - 'any.required': 'Password is required', - 'string.empty': 'Password must not be empty' - }) -}); - -module.exports = ceoPasswordModel; \ No newline at end of file diff --git a/UC/backend/src/models/departmentRegisterModel.js b/UC/backend/src/models/departmentRegisterModel.js deleted file mode 100644 index 2d45bad..0000000 --- a/UC/backend/src/models/departmentRegisterModel.js +++ /dev/null @@ -1,10 +0,0 @@ -const Joi = require('joi'); - -const departmentRegisterModel = Joi.object({ - name: Joi.string().required().messages({ - 'any.required': 'Department name is required', - 'string.empty': 'Department name must not be empty' - }) -}); - -module.exports = departmentRegisterModel \ No newline at end of file diff --git a/UC/backend/src/models/dirStructureModel.js b/UC/backend/src/models/dirStructureModel.js deleted file mode 100644 index 08625f7..0000000 --- a/UC/backend/src/models/dirStructureModel.js +++ /dev/null @@ -1,23 +0,0 @@ -const Joi = require('joi'); - -const dirStructureModel = Joi.object({ - id: Joi.string().guid().required().messages({ - 'any.required': 'ID is required', - 'string.empty': 'ID must not be empty', - 'string.guid': 'ID must be a valid GUID' - }), - ip: Joi.string().ip().required().messages({ - 'string.ip': 'The IP address "{{#value}}" is not valid.', - 'any.required': 'IP address is required.' - }), - directoryStructure: Joi.string().required().messages({ - 'any.required': 'Directory configuration is required', - 'string.empty': 'Directory configuration must not be empty' - }), - totalSize: Joi.number().required().messages({ - 'any.required': 'Total space is required', - 'number.base': 'Total space must be a number' - }) -}); - -module.exports = dirStructureModel \ No newline at end of file diff --git a/UC/backend/src/models/emailVerificationModel.js b/UC/backend/src/models/emailVerificationModel.js deleted file mode 100644 index 32ba392..0000000 --- a/UC/backend/src/models/emailVerificationModel.js +++ /dev/null @@ -1,11 +0,0 @@ -const Joi = require('joi'); - -const emailVerificationModel = Joi.object({ - email: Joi.string().email().required().messages({ - 'string.empty': 'Email can\'t be empty.', - 'string.email': 'Invalid email format.', - 'any.required': 'Email is required' - }) -}); - -module.exports = emailVerificationModel; \ No newline at end of file diff --git a/UC/backend/src/models/schemaMapper.js b/UC/backend/src/models/schemaMapper.js deleted file mode 100644 index 54eb32d..0000000 --- a/UC/backend/src/models/schemaMapper.js +++ /dev/null @@ -1,25 +0,0 @@ -const usersRegisterModelSchema = require('./usersRegisterModel'); -const usersLoginModelSchema = require('./usersLoginModel'); -const usersModifyModelSchema = require('./usersModifyModel'); -const dirStructureModelSchema = require('./dirStructureModel'); -const departmentRegisterModelSchema = require('./departmentRegisterModel'); -const ceoModifyModelSchema = require('./ceoModifyModel'); -const emailVerificationSchema = require('./emailVerificationModel'); -const userDepartmentPatchSchema = require('./userDepartmentPatchModel'); -const ceoPasswordModelSchema = require('./ceoPasswordModel'); -const securityLevelsModelSchema = require('./securityLevelsModel'); - -module.exports = { - schemas: { - dirStructureModelSchema, - usersRegisterModelSchema, - usersLoginModelSchema, - usersModifyModelSchema, - departmentRegisterModelSchema, - ceoModifyModelSchema, - emailVerificationSchema, - userDepartmentPatchSchema, - ceoPasswordModelSchema, - securityLevelsModelSchema - }, -}; \ No newline at end of file diff --git a/UC/backend/src/models/securityLevelsModel.js b/UC/backend/src/models/securityLevelsModel.js deleted file mode 100644 index 83feda6..0000000 --- a/UC/backend/src/models/securityLevelsModel.js +++ /dev/null @@ -1,19 +0,0 @@ -const Joi = require("joi"); - -// Define a schema for the string values (department names) -const nameSchema = Joi.string().required().messages({ - 'string.empty': 'Name can\'t be empty', - 'any.required': 'Name is required' -}); - -// Define the main schema for the security levels model -const securityLevelsModel = Joi.object().pattern( - Joi.number().integer().positive().messages({ - 'number.base': 'Keys must be integers', - 'number.integer': 'Keys must be integers', - 'number.positive': 'Keys must be positive integers' - }), - nameSchema -); - -module.exports = securityLevelsModel; diff --git a/UC/backend/src/models/userDepartmentPatchModel.js b/UC/backend/src/models/userDepartmentPatchModel.js deleted file mode 100644 index 077b078..0000000 --- a/UC/backend/src/models/userDepartmentPatchModel.js +++ /dev/null @@ -1,15 +0,0 @@ -const Joi = require("joi"); - -const UserDepartmentPatchModel = Joi.object({ - id: Joi.string().uuid().required().messages({ - 'any.required': 'User ID is required', - 'string.empty': 'User ID must not be empty', - 'string.uuid': 'User ID must be a valid UUID' - }), - department: Joi.string().required().messages({ - 'any.required': 'User ID is required', - 'string.empty': 'User ID must not be empty', - }) -}); - -module.exports = UserDepartmentPatchModel; diff --git a/UC/backend/src/models/usersLoginModel.js b/UC/backend/src/models/usersLoginModel.js deleted file mode 100644 index 880a0c0..0000000 --- a/UC/backend/src/models/usersLoginModel.js +++ /dev/null @@ -1,15 +0,0 @@ -const Joi = require('joi'); - -const usersLoginModel = Joi.object({ - email: Joi.string().email().required().messages({ - 'any.required': 'Email is required', - 'string.empty': 'Email must not be empty', - 'string.email': 'Email must be a valid email address' - }), - password: Joi.string().required().messages({ - 'any.required': 'Password is required', - 'string.empty': 'Password must not be empty' - }) -}); - -module.exports = usersLoginModel; \ No newline at end of file diff --git a/UC/backend/src/models/usersModifyModel.js b/UC/backend/src/models/usersModifyModel.js deleted file mode 100644 index 5820256..0000000 --- a/UC/backend/src/models/usersModifyModel.js +++ /dev/null @@ -1,21 +0,0 @@ -const Joi = require('joi'); - -const usersModifyModel = Joi.object({ - name: Joi.string().required().messages({ - 'any.required': 'User name is required', - 'string.empty': 'User name must not be empty' - }), - email: Joi.string().email().required().messages({ - 'any.required': 'User email is required', - 'string.empty': 'User email must not be empty', - 'string.email': 'User email must be a valid email address' - }), - password: Joi.string().min(8).max(20).required().messages({ - 'any.required': 'Password is required', - 'string.empty': 'Password must not be empty', - 'string.min': 'Password must be at least {#limit} characters long', - 'string.max': 'Password must be at most {#limit} characters long' - }) -}); - -module.exports = usersModifyModel; \ No newline at end of file diff --git a/UC/backend/src/models/usersRegisterModel.js b/UC/backend/src/models/usersRegisterModel.js deleted file mode 100644 index 0b49551..0000000 --- a/UC/backend/src/models/usersRegisterModel.js +++ /dev/null @@ -1,25 +0,0 @@ -const Joi = require('joi'); - -const usersRegisterModel = Joi.object({ - name: Joi.string().required().messages({ - 'any.required': 'User name is required', - 'string.empty': 'User name must not be empty' - }), - email: Joi.string().email().required().messages({ - 'any.required': 'Email is required', - 'string.empty': 'Email must not be empty', - 'string.email': 'Email must be a valid email address' - }), - password: Joi.string().min(8).max(20).required().messages({ - 'any.required': 'Password is required', - 'string.empty': 'Password must not be empty', - 'string.min': 'Password must be at least {#limit} characters long', - 'string.max': 'Password must be at most {#limit} characters long' - }), - department: Joi.string().required().messages({ - 'any.required': 'Department ID is required', - 'string.empty': 'Department ID must not be empty' - }) -}); - -module.exports = usersRegisterModel; \ No newline at end of file diff --git a/UC/backend/src/routes/admin.js b/UC/backend/src/routes/admin.js deleted file mode 100644 index e6643e0..0000000 --- a/UC/backend/src/routes/admin.js +++ /dev/null @@ -1,85 +0,0 @@ -const checkForServerConnection = async () => { - const pathToIpConfig = path.join(__dirname, '..', '..', 'ipConfig.json'); - await lockFile.lock(pathToIpConfig); - try { - await decryptFileInPlace(pathToIpConfig); - const ipConfig = await fs.readFile(pathToIpConfig, 'utf-8'); - const { ip } = JSON.parse(ipConfig); - const response = await fetch(`http://${ip}:5000/heartbeat`); - if (response.ok) { - return true; - } else { - return false; - } - } catch (error) { - console.error("Error:", error); - return false; - } finally { - await lockFile.unlock(pathToIpConfig); - } -};const express = require('express'); -const crypto = require('crypto'); -const { v4: uuidv4 } = require('uuid'); - -const {httpStatus} = require("../helpers/httpResponses"); - -const router = express.Router(); - -function validateBody(req, res, next) { - const apiKey = req.headers['authorization']; - const adminDB = req.app.get('adminDB'); - const {id} = adminDB.readFile(); - - if(apiKey !== id){ - return res.status(httpStatus.UNAUTHORIZED).json( - {message: "You are not authorized as an admin."}) - } - - next(); -} - -router.get('/reset_server', validateBody, (req, res) => { - const usersDB = req.app.get('usersDB'); - const departmentsDB = req.app.get('departmentsDB'); - const backupSchemesDB = req.app.get('backupSchemesDB'); - const ceoDB = req.app.get('ceoDB'); - - usersDB.writeFile([]); - departmentsDB.writeFile({}); - backupSchemesDB.writeFile({}); - ceoDB.writeFile({ - id: uuidv4(), - name: "CEO", - email: "ceo@yourfirm.com", - password: crypto.randomBytes(16).toString('hex'), - department: "CEO" - }); - - const ceoDepartment = { - name: "CEO", - key: crypto.randomBytes(32).toString('hex') - - } - - const departmentsJson = departmentsDB.readFile(); - departmentsJson[1] = ceoDepartment; - departmentsDB.writeFile(departmentsJson); - - res.status(httpStatus.OK).json({ message: "Resetting the server..." }); -}); - -router.get('/ceo', validateBody, (req, res) => { - const ceoDB = req.app.get('ceoDB'); - res.status(httpStatus.OK).json({ message: "Retrieving CEO info...", data: ceoDB.readFile() }) // Corrected to readFile -}); - -router.get('/users', validateBody, (req, res) => { - const usersDB = req.app.get('usersDB'); - res.status(httpStatus.OK).json({ message: "Retrieving users info...", data: usersDB.readFile() }) // Corrected to readFile -}); - -router.use((req, res) => { - return res.status(httpStatus.NOT_FOUND).json({message: "Endpoint not found"}); -}) - -module.exports = router; diff --git a/UC/backend/src/routes/backup_schemes.js b/UC/backend/src/routes/backup_schemes.js deleted file mode 100644 index ecf3c53..0000000 --- a/UC/backend/src/routes/backup_schemes.js +++ /dev/null @@ -1,63 +0,0 @@ -const express = require('express'); -const {httpStatus} = require("../helpers/httpResponses"); -const {schemas} = require("../models/schemaMapper"); -const router = express.Router(); - -function validateBody(req, res, next) { - let validationSchema = undefined; - - if(req.path === '/' && req.method === 'PATCH'){ - validationSchema = schemas.dirStructureModelSchema - } - - if(validationSchema !== undefined){ - console.log(req.body); - const {error} = validationSchema.validate(req.body); - if(error){ - const errorMessage = error.details.map(detail => detail.message).join(', '); - return res.status(httpStatus.BAD_REQUEST).json({ message: errorMessage }); - } - } - next(); -} - -router.patch('/', validateBody, (req, res) => { - const backupSchemesDB = req.app.get('backupSchemesDB'); - const { id, ip, directoryStructure, totalSize } = req.body; - - let backupSchemesJson = backupSchemesDB.readFile(); - backupSchemesJson[id] = { - ip: ip, - directoryStructure: directoryStructure, - totalSize: totalSize - } - - backupSchemesDB.writeFile(backupSchemesJson); - return res.status(httpStatus.OK).json({message: 'Backup schema updated'}); -}); - -router.get('/', validateBody, (req, res) => { - const backupSchemesDB = req.app.get('backupSchemesDB'); - return res.status(httpStatus.OK).json({ - message: 'Backup Schemes fetched.', - data: backupSchemesDB.readFile() - }); -}); - -router.get('/:userId', (req, res) => { - const userId = req.params.userId; - const backupSchemesDB = req.app.get('backupSchemesDB'); - let backupSchemesJson = backupSchemesDB.readFile(); - const ip = backupSchemesJson[userId].ip; - if (ip) { - res.status(httpStatus.OK).json({message: "IP found", data: ip}); - } else { - res.status(httpStatus.NOT_FOUND).send({message: "IP not found"}); - } -}); - -router.use((req, res) => { - return res.status(httpStatus.NOT_FOUND).json({message: "Endpoint not found"}); -}) - -module.exports = router; diff --git a/UC/backend/src/routes/ceo.js b/UC/backend/src/routes/ceo.js deleted file mode 100644 index a17b7f5..0000000 --- a/UC/backend/src/routes/ceo.js +++ /dev/null @@ -1,196 +0,0 @@ -const express = require('express'); -const { v4: uuidv4 } = require('uuid'); - -const {schemas} = require("../models/schemaMapper"); -const {httpStatus} = require("../helpers/httpResponses"); -const crypto = require("crypto"); -const {departmentsDB} = require("../db/jsonDatabaseManager"); - -const router = express.Router(); - -function validateBody(req, res, next) { - if(req.path !== '/login'){ - const ceoDB = req.app.get('ceoDB'); - const ceoPassword = req.headers['ceo_password']; - const {password} = ceoDB.readFile() - - if(ceoPassword !== password){ - return res.status(httpStatus.UNAUTHORIZED).json({message: 'Invalid CEO password.'}); - } - } - - let validationSchema = undefined; - - switch(req.path){ - case '/set_security_levels': - validationSchema = schemas.securityLevelsModelSchema; - break; - case '/login': - validationSchema = schemas.usersLoginModelSchema; - break; - case '/departments': - validationSchema = schemas.departmentRegisterModelSchema; - break; - case '/': - if(req.method === 'PUT'){ - validationSchema = schemas.ceoModifyModelSchema; - } - break; - default: - validationSchema = undefined; - } - - if(validationSchema !== undefined){ - const {error} = validationSchema.validate(req.body); - if(error){ - const errorMessage = error.details.map(detail => detail.message).join(', '); - return res.status(httpStatus.BAD_REQUEST).json({ message: errorMessage }); - } - } - - next(); -} - -router.post('/login', validateBody, (req, res) => { - const ceoDB = req.app.get('ceoDB'); - const { email, password } = req.body; - - if(ceoDB.findIndexByKeyValueInArray('email', email) !== ceoDB.findIndexByKeyValueInArray('password', password)){ - return res.status(httpStatus.UNAUTHORIZED).json({message: 'Invalid credentials.'}); - } - - return res.status(httpStatus.OK).json({message: 'Logged in.', data: ceoDB.readFile()}); -}); - -router.post('/set_security_levels', validateBody, (req, res) => { - const departmentsDB = req.app.get('departmentsDB'); - const departmentsJson = departmentsDB.readFile(); - - const levelsJson = req.body; - console.log(levelsJson); - - const newJson = {}; - - Object.entries(levelsJson).forEach(([levelKey, departmentName]) => { - let departmentId = -1; - for (let deptKey in departmentsJson) { - if (departmentsJson.hasOwnProperty(deptKey)) { - if (departmentsJson[deptKey].name === departmentName) { - departmentId = deptKey; - break; - } - } - } - - if (departmentId !== -1) { - newJson[levelKey] = { - name: departmentName, - key: departmentsJson[departmentId].key - }; - } - }); - - - departmentsDB.writeFile(newJson); - return res.status(httpStatus.OK).json({message: 'Departments levels updated.'}); -}); - -router.delete('/users/:id', validateBody, (req, res) => { - const usersDB = req.app.get('usersDB'); - const { id } = req.params; - - let usersJson = usersDB.readFile(); - - const userIndex = usersJson.findIndex(user => user.id === id); - if (userIndex === -1) { - return res.status(httpStatus.NOT_FOUND).json({message: 'User not found in system.'}); - } - - usersJson.splice(userIndex, 1); - - - usersDB.writeFile(usersJson); - - return res.status(httpStatus.OK).json({ message: 'User deleted successfully.' }); -}); - -router.post('/departments', validateBody, (req, res) => { - const departmentsDB = req.app.get('departmentsDB'); - const { name } = req.body; - console.log(name); - - let jsonDepartments = departmentsDB.readFile(); - const nameExists = Object.values(jsonDepartments).some(department => department.name === name); - if(nameExists){ - return res.status(httpStatus.CONFLICT).json({message: 'Department already in system.'}) - } - - const isObjectEmpty = !Object.keys(jsonDepartments).length; - const highestKey = isObjectEmpty ? 0 : Math.max(...Object.keys(jsonDepartments).map(Number)); - const nextKey = highestKey + 1; - - jsonDepartments[nextKey] = { - name: name, - key: crypto.randomBytes(32).toString('hex') - }; - departmentsDB.writeFile(jsonDepartments); - - return res.status(httpStatus.CREATED).json({message: 'Department successfully created'}); -}); - -router.delete('/departments/:name', validateBody, (req, res) => { - const departmentsDB = req.app.get('departmentsDB'); - let departmentsJson = departmentsDB.readFile(); - let foundName = false; - const { name } = req.params; - - console.log(name); - - for (let key in departmentsJson) { - if (departmentsJson.hasOwnProperty(key)) { - const department = departmentsJson[key]; - if (department.name === name) { - delete departmentsJson[key]; - foundName = true; - break; - } - } - } - - if(foundName !== true){ - return res.status(httpStatus.NOT_FOUND).json({ message: 'Department not found.' }); - } - - departmentsDB.writeFile(departmentsJson); - return res.status(httpStatus.OK).json({ message: 'Department deleted successfully.' }); -}); - -router.put('/', validateBody, (req, res) => { - const ceoDB = req.app.get('ceoDB'); - const { name, email } = req.body; - const {id, department} = ceoDB.readFile() - - const newCeo = { - id: id, - name: name, - email: email, - password: crypto.randomBytes(16).toString('hex'), - department: department - } - - ceoDB.writeFile(newCeo); - return res.status(httpStatus.OK).json({message: 'Information modified.', data: newCeo}); -}); - -router.get('/get_decrypt_keys', (req, res) => { - return res.status(httpStatus.OK).json({ - message: 'Departments fetched.', - data: departmentsDB.readFile() - }); -}); - -router.use((req, res) => { - return res.status(httpStatus.NOT_FOUND).json({message: "Endpoint not found"}); -}) - -module.exports = router; diff --git a/UC/backend/src/routes/users.js b/UC/backend/src/routes/users.js deleted file mode 100644 index 354d7c8..0000000 --- a/UC/backend/src/routes/users.js +++ /dev/null @@ -1,210 +0,0 @@ -const express = require('express'); -const { v4: uuidv4 } = require('uuid'); -const crypto = require('crypto'); - -const {schemas} = require('../models/schemaMapper'); -const {httpStatus} = require("../helpers/httpResponses"); - - -const router = express.Router(); - -function validateBody(req, res, next) { - let validationSchema = undefined; - - switch(req.path){ - case '/register': - validationSchema = schemas.usersRegisterModelSchema; - break; - case '/login': - validationSchema = schemas.usersLoginModelSchema; - break; - case '/validate_ceo_password': - validationSchema = schemas.ceoPasswordModelSchema; - break; - case '/validate_email': - validationSchema = schemas.emailVerificationSchema; - break; - case '/': - if(req.method === 'PUT'){ - validationSchema = schemas.usersModifyModelSchema; - } - break; - case '/change_department': - validationSchema = schemas.userDepartmentPatchSchema; - break; - default: - validationSchema = undefined; - } - - if(validationSchema !== undefined){ - console.log(req.body); - const {error} = validationSchema.validate(req.body); - if(error){ - const errorMessage = error.details.map(detail => detail.message).join(', '); - return res.status(httpStatus.BAD_REQUEST).json({ message: errorMessage }); - } - } - next(); -} - -router.post('/register', validateBody, (req, res) => { - const usersDB = req.app.get('usersDB'); - const usersJson = usersDB.readFile(); - - const { name, email, password, department } = req.body; - const hashedPassword = crypto.createHash('sha256').update(password).digest('hex'); - - const newUser = { - id: uuidv4(), - name: name, - email: email, - password: hashedPassword, - department: department - } - - usersJson.push(newUser); - usersDB.writeFile(usersJson); - - return res.status(httpStatus.CREATED).json({message: "User successfully created.", data: newUser}); -}); - -router.post('/login', validateBody, (req, res) => { - const usersDB = req.app.get('usersDB'); - const { email, password } = req.body; - const hashedPassword = crypto.createHash('sha256').update(password).digest('hex'); - - if(usersDB.readFile().length === 0){ - return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({message: 'Internal server error.'}); - } - - if(usersDB.findIndexByKeyValueInArray('email', email) !== - usersDB.findIndexByKeyValueInArray('password', hashedPassword)){ - return res.status(httpStatus.UNAUTHORIZED).json({message: 'Invalid credentials.'}); - } - - const userIndex = usersDB.findIndexByKeyValueInArray('email', email); - - return res.status(httpStatus.OK).json({ - message: 'Logged in.', - data: usersDB.readFile()[userIndex] - }); -}); - -router.post('/validate_ceo_password', validateBody, (req, res) => { - const ceoDB = req.app.get('ceoDB'); - const { password } = req.body; - - const ceoJson = ceoDB.readFile(); - if(ceoJson.password !== password){ - return res.status(httpStatus.UNAUTHORIZED).json({message: 'Incorrect CEO password'}); - } - - return res.status(httpStatus.OK).json({message: 'Password verified.'}) -}) - -router.post('/validate_email', validateBody, (req, res) => { - const usersDB = req.app.get('usersDB'); - const { email } = req.body; - - if(usersDB.isKeyValue("email", email)){ - return res.status(httpStatus.CONFLICT).json({"message": "Email already in system."}); - } - - return res.status(httpStatus.OK).json({"message": "Email is valid."}); -}); - -router.get('/', (req, res) => { - const usersDB = req.app.get('usersDB'); - const ceoDB = req.app.get('ceoDB'); - - const ceoJson = ceoDB.readFile(); - let usersJson = usersDB.readFile(); - usersJson.push(ceoJson); - - return res.status(httpStatus.OK).json({ - message: "Fetched users", - data: usersJson - }) -}); - -router.get('/departments', (req, res) => { - const departmentsDB = req.app.get('departmentsDB'); - return res.status(httpStatus.OK).json({ - message: 'Departments fetched.', - data: departmentsDB.readFile() - }); -}) - -router.put('/', validateBody, (req, res) => { - const usersDB = req.app.get('usersDB'); - let usersJson = usersDB.readFile(); - - const { name, email, password } = req.body; - const userIndex = usersDB.findIndexByKeyValueInArray('email', email); - if(userIndex === -1){ - return res.status(httpStatus.NOT_FOUND).json({message: 'User not found.'}); - } - - const hashedPassword = crypto.createHash('sha256').update(password).digest('hex'); - let userInfo = usersJson[userIndex]; - userInfo = { - id: userInfo.id, - name: name, - email: email, - password: hashedPassword, - department: userInfo.department - } - - usersJson[userIndex] = userInfo; - usersDB.writeFile(usersJson); - - return res.status(httpStatus.OK).json({message: 'Information modified'}); -}); - -router.patch('/change_department', validateBody, (req, res) => { - const usersDB = req.app.get('usersDB'); - const { id, department } = req.body; - - const userIndex = usersDB.findIndexByKeyValueInArray('id', id); - if(userIndex === -1){ - return res.status(httpStatus.NOT_FOUND).json({message: "User not found."}); - } - - let usersJson = usersDB.readFile(); - let userInfo = usersJson[userIndex]; - - userInfo = { - id: userInfo.id, - name: userInfo.name, - email: userInfo.email, - password: userInfo.password, - department: department - } - - usersJson[userIndex] = userInfo - usersDB.writeFile(usersJson); - - return res.status(httpStatus.OK).json({message: "Department modified."}); -}); - -router.get('/get_decrypt_keys', (req, res) => { - const ceoDB = req.app.get('ceoDB'); - const departmentsDB = req.app.get('departmentsDB'); - const ceoPassword = req.headers['ceo_password']; - const {password} = ceoDB.readFile() - - if(ceoPassword !== password){ - return res.status(httpStatus.UNAUTHORIZED).json({message: 'Invalid CEO password.'}); - } - - return res.status(httpStatus.OK).json({ - message: 'Departments fetched.', - data: departmentsDB.readFile() - }); -}); - -router.use((req, res) => { - return res.status(httpStatus.NOT_FOUND).json({message: "Endpoint not found"}); -}) - -module.exports = router; diff --git a/UC/backend/task_uri.md b/UC/backend/task_uri.md deleted file mode 100644 index 475f640..0000000 --- a/UC/backend/task_uri.md +++ /dev/null @@ -1,21 +0,0 @@ -Taskuri -- -- create form which upon submission set's the ip location (also create a button which can change this one and have to -create a IPC for retrieving the ip when making the requests). -- create the encryption process when writing a file on disk / decryption process when reading a file form disk -- configure endpoint for 'backup_schemes' completely -- refactor the JS code from all the html to make it with .then statements and show the messages -from the UC -- finish the CEO interface -- create process which will update the backup scheme with: - - - IP - - user_id - - dir_schema - - size - -- create process which will fetch the large schema and encryption keys and compare it with the one on local; -if is the same, hibernate; else, make requests for the files -- create process which will get the user database and combine it with the ip and at the request will share a -specific file -- create process which will decrypt all the files from the backup scheme locally \ No newline at end of file diff --git a/UC/db/database.db b/UC/db/database.db new file mode 100644 index 0000000..4296ffb Binary files /dev/null and b/UC/db/database.db differ diff --git a/UC/frontend/.idea/.gitignore b/UC/frontend/.idea/.gitignore deleted file mode 100644 index b58b603..0000000 --- a/UC/frontend/.idea/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/UC/frontend/.idea/frontend.iml b/UC/frontend/.idea/frontend.iml deleted file mode 100644 index 24643cc..0000000 --- a/UC/frontend/.idea/frontend.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/UC/frontend/.idea/jsLibraryMappings.xml b/UC/frontend/.idea/jsLibraryMappings.xml deleted file mode 100644 index d23208f..0000000 --- a/UC/frontend/.idea/jsLibraryMappings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/UC/frontend/.idea/modules.xml b/UC/frontend/.idea/modules.xml deleted file mode 100644 index f3d93d7..0000000 --- a/UC/frontend/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/UC/frontend/.idea/runConfigurations/bin_www.xml b/UC/frontend/.idea/runConfigurations/bin_www.xml deleted file mode 100644 index 9fe0f59..0000000 --- a/UC/frontend/.idea/runConfigurations/bin_www.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/UC/frontend/.idea/vcs.xml b/UC/frontend/.idea/vcs.xml deleted file mode 100644 index b2bdec2..0000000 --- a/UC/frontend/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/UC/frontend/README.md b/UC/frontend/README.md deleted file mode 100644 index 5b6df04..0000000 --- a/UC/frontend/README.md +++ /dev/null @@ -1,32 +0,0 @@ -General overview -- - -For the use of the admin console, start with 'npm run start' the UI frontend. -To see/modify the initial login credentials check the /config/config.json file. - -WITHOUT SETTING THE CONFIG FILE, THE SERVER WILL NOT WORK AS INTENDED! -(click on the 'Reset UC' when first configuring the server to bring the -UC at the initial state) - -Because the client and CEO apps are dependent on the Central Unit (UC), we strongly -suggest you to make backup at a regular interval in case of hardware failing you will -only have to deploy the server with the configurations already existing. - -Important aspects: -- - -- The starting/stopping of the UC is handled by this 'Admin Console'. The server will not -work if the server which servers as the 'Admin Console' is not running. -- The main server will run at the port 5000, so make sure to configure your routers to -enable routing to the server; -- The client and CEO backup system of their applications will need access at ports 5001 and 5002, -so also make sure to open those ports as well; -- If the CEO wants to change the email address linked to the account, his password will be changed -to a low-risk password, so make sure after any release to specify the account information; -- Make sure that all the ports configurations are respected to have all the applications work properly - -Closing thoughts -- - -We really hope that you will enjoy our application and for any issues, feel free to contact us at -'username@ourdomain.com' for troubleshooting and other problems. \ No newline at end of file diff --git a/UC/frontend/app.js b/UC/frontend/app.js deleted file mode 100644 index 62fe764..0000000 --- a/UC/frontend/app.js +++ /dev/null @@ -1,55 +0,0 @@ -const express = require('express'); -const bodyParser = require('body-parser'); -const loginRouter = require('./routers/login'); -const menuRouter = require('./routers/mainMenu'); -const path = require('path'); -const cookieParser = require('cookie-parser'); - -const checkSession = require('./middlewares/checkSession'); - -process.on('SIGINT', shutdown); -process.on('SIGTERM', shutdown); - -function shutdown() { - console.log('Received kill signal, shutting down gracefully.'); - const serverProcess = app.get('serverProcess'); - if (serverProcess && typeof serverProcess.kill === 'function') { - console.log('Shutting down server process...'); - serverProcess.kill(); // Send SIGTERM to server process - - serverProcess.on('exit', () => { - console.log('Server process terminated.'); - process.exit(0); // Exit main process cleanly - }); - } else { - console.log('No server process or cannot be killed, exiting.'); - process.exit(0); // Exit main process cleanly - } -} - -const app = express() - -app.set('serverProcess', null); - -app.use(cookieParser()); -app.use(bodyParser.urlencoded({ extended: true })); -app.use(bodyParser.json()); - -app.use(express.static(path.join(__dirname, 'public', 'html'))); -app.use('/css', express.static(path.join(__dirname, 'public', 'css'))); -app.use('/js', express.static(path.join(__dirname, 'public', 'js'))); -app.use('/images', express.static(path.join(__dirname, 'public', 'images'))); - -app.use(checkSession); - -// Mount login and menu routers -app.use('/login', loginRouter); -app.use('/', menuRouter); - -// Start the server -const PORT = process.env.PORT || 80; -app.listen(PORT, () => { - console.log(`Server is running on port ${PORT}`); -}); - - diff --git a/UC/frontend/config/credentials.json b/UC/frontend/config/credentials.json deleted file mode 100644 index 5e615f0..0000000 --- a/UC/frontend/config/credentials.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "id": "deee8837-3549-44d5-af56-fb7532505574", - "name": "Andrei", - "email": "admin@yourfirm.com", - "password": "password" -} \ No newline at end of file diff --git a/UC/frontend/helpers/status_codes.js b/UC/frontend/helpers/status_codes.js deleted file mode 100644 index 96e9b84..0000000 --- a/UC/frontend/helpers/status_codes.js +++ /dev/null @@ -1,11 +0,0 @@ -const statusCodes = { - OK: 200, - CREATED: 201, - BAD_REQUEST: 400, - UNAUTHORIZED: 401, - FORBIDDEN: 403, - NOT_FOUND: 404, - INTERNAL_SERVER_ERROR: 500 -}; - -module.exports = statusCodes; \ No newline at end of file diff --git a/UC/frontend/middlewares/checkSession.js b/UC/frontend/middlewares/checkSession.js deleted file mode 100644 index 97d55b9..0000000 --- a/UC/frontend/middlewares/checkSession.js +++ /dev/null @@ -1,22 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const checkSession = (req, res, next) => { - const configPath = path.join(__dirname, '..', 'config', 'credentials.json'); - const configFile = fs.readFileSync(configPath); - const config = JSON.parse(configFile); - - const userEmail = req.cookies.email; - - if (req.path === '/login') { - next(); - } else { - if (userEmail && decodeURIComponent(userEmail) === config.email) { - next(); - } else { - res.redirect('/login'); - } - } -} - -module.exports = checkSession diff --git a/UC/frontend/models/login.js b/UC/frontend/models/login.js deleted file mode 100644 index 9166b15..0000000 --- a/UC/frontend/models/login.js +++ /dev/null @@ -1,8 +0,0 @@ -const Joi = require('joi'); - -const loginModel = Joi.object({ - email: Joi.string().email().required(), - password: Joi.string().required() -}); - -module.exports = loginModel; diff --git a/UC/frontend/models/schemas.js b/UC/frontend/models/schemas.js deleted file mode 100644 index 2edde1b..0000000 --- a/UC/frontend/models/schemas.js +++ /dev/null @@ -1,7 +0,0 @@ -const loginModelSchema = require('./login'); - -const schemas = { - loginModelSchema -} - -module.exports = schemas; \ No newline at end of file diff --git a/UC/frontend/package-lock.json b/UC/frontend/package-lock.json deleted file mode 100644 index e0e1c94..0000000 --- a/UC/frontend/package-lock.json +++ /dev/null @@ -1,1151 +0,0 @@ -{ - "name": "frontend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "frontend", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "body-parser": "^1.20.2", - "cookie-parser": "^1.4.6", - "express": "^4.19.2", - "joi": "^17.12.2" - }, - "devDependencies": { - "nodemon": "^3.1.0" - } - }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", - "dependencies": { - "cookie": "0.4.1", - "cookie-signature": "1.0.6" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/cookie-parser/node_modules/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/joi": { - "version": "17.12.2", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz", - "integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==", - "dependencies": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/nodemon": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", - "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==", - "dev": true, - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/nodemon/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", - "dev": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dev": true, - "dependencies": { - "nopt": "~1.0.10" - }, - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } -} diff --git a/UC/frontend/package.json b/UC/frontend/package.json deleted file mode 100644 index e0a8b58..0000000 --- a/UC/frontend/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "frontend", - "version": "1.0.0", - "description": "UI for the UC", - "main": "app.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node app.js", - "dev": "nodemon app.js" - }, - "author": "Andrei Cerbu", - "license": "ISC", - "dependencies": { - "body-parser": "^1.20.2", - "cookie-parser": "^1.4.6", - "express": "^4.19.2", - "joi": "^17.12.2" - }, - "devDependencies": { - "nodemon": "^3.1.0" - } -} diff --git a/UC/frontend/public/css/main_menu.css b/UC/frontend/public/css/main_menu.css deleted file mode 100644 index 942ccf5..0000000 --- a/UC/frontend/public/css/main_menu.css +++ /dev/null @@ -1,118 +0,0 @@ -body, html { - height: 100%; - width: 100%; - margin: 0; - padding: 0; - - font-family: "Roboto", sans-serif; - font-weight: 500; - font-style: normal; - - background-image: url('/images/background.png'); - background-size: cover; - background-repeat: no-repeat; - -} -.container { - opacity: 0; - - display: flex; - flex-direction: row; - justify-content: space-evenly; - align-items: center; - min-height: 100vh; - text-align: center; -} - -.main_block{ - display: flex; - flex-direction: column; - background-color: #535C91; - opacity: 71; - padding: 2rem; - border-radius: 1rem; - box-shadow: 10px 10px 10px rgba(0, 0, 0, 0.9); - color: white; -} - -.header{ - display: flex; - flex-direction: row; - justify-content: space-between; - text-align: left; -} - -.header h1{ - padding: 0; - margin: 0; -} - -.header img{ - margin: 0 3vw 0 5vw; - width: 8vw; - height: 8vw; -} - -.content{ - margin: 2rem 0 2rem 0; -} - -.content_buttons{ - display: flex; - flex-direction: row; - align-content: center; - align-items: center; - justify-content: center; -} - -button{ - margin: 0 1rem 0 1rem; - width: 15vw; - height: 10vh; - - border: none; - border-radius: 10px; - - color: white; - font-size: 1rem; - font-weight: bold; - - text-transform: uppercase; - cursor: pointer; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); - outline: none; - transition: background-color 0.3s ease; -} - -button:hover{ - filter: brightness(85%); -} - -.footer{ - display: flex; - align-items: center; - justify-content: end; -} - -button[name="logout"]{ - margin: 0; - padding: 0; - width: 10vw; - height: 5vh; - background-color: #F44336; -} - -button[name="alert"]{ - margin: 0.7rem; - background-color: #F44336; -} - -button[name="notification"]{ - margin: 0.7rem; - background-color: #23BDEE; -} - -button[name="menu_button"]{ - margin: 0.7rem; - background-color: #1B1A55; -} \ No newline at end of file diff --git a/UC/frontend/public/html/login.html b/UC/frontend/public/html/login.html deleted file mode 100644 index a7fad83..0000000 --- a/UC/frontend/public/html/login.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - Login - - -
-
-

DO WE KNOW

-

EACH OTHER?

-
- -
- - diff --git a/UC/frontend/public/html/main_menu.html b/UC/frontend/public/html/main_menu.html deleted file mode 100644 index 1df8f5f..0000000 --- a/UC/frontend/public/html/main_menu.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - Main Page - - -
-
-
-
-

WELCOME BACK,

-

-

Hope you have a productive day!

-
- profile image -
-
-
- - -
-
- - -
-
- -
-
- -
-
- - diff --git a/UC/frontend/public/js/login.js b/UC/frontend/public/js/login.js deleted file mode 100644 index 7db51ed..0000000 --- a/UC/frontend/public/js/login.js +++ /dev/null @@ -1,38 +0,0 @@ -document.addEventListener('DOMContentLoaded', function () { - const loginForm = document.getElementById('loginForm'); - - loginForm.addEventListener('submit', async function (e) { - e.preventDefault(); - - const formData = new FormData(loginForm); - const email = formData.get('email'); - const password = formData.get('password'); - - const data = { email, password }; - - try { - const response = await fetch('/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }); - - const result = await response.json(); - - if (response.ok && result.success) { - document.cookie = `id=${result.user.id};`; - document.cookie = `email=${result.user.email};`; - document.cookie = `name=${result.user.name};`; - - fadeOut('/'); - } else { - alert('Incorrect credentials. Please try again.'); - } - } catch (error) { - console.error('Error during fetch:', error); - alert('An error occurred. Please try again.'); - } - }); -}); diff --git a/UC/frontend/public/js/main_menu.js b/UC/frontend/public/js/main_menu.js deleted file mode 100644 index 70dff71..0000000 --- a/UC/frontend/public/js/main_menu.js +++ /dev/null @@ -1,199 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - function getCookie(name) { - const cookies = document.cookie.split(';'); - for (let i = 0; i < cookies.length; i++) { - const cookie = cookies[i].trim(); - if (cookie.startsWith(name + '=')) { - return cookie.substring(name.length + 1, cookie.length); - } - } - return null; - } - - const username = getCookie('name'); // Using 'name' to match your cookie structure - const usernameField = document.getElementById('username_field'); - if (usernameField && username) { - usernameField.textContent = username; - }else{ - usernameField.textContent = 'Admin'; - } - - // Define handler function - function handleButtonClick(event) { - console.log(`${event.target.id} button was clicked`); - // Implement specific logic for each button here - } - - // Attach event handlers to specific buttons - const startUcButton = document.getElementById('start_uc'); - const stopUcButton = document.getElementById('stop_uc'); - const ceoInfoButton = document.getElementById('ceo_info'); - const usersInfoButton = document.getElementById('users_info'); - const resetUcButton = document.getElementById('reset_uc'); - const logoutButton = document.getElementById('logout'); - - startUcButton.addEventListener('click', () => { - console.log('Start UC Button pressed'); - fetch('/server', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ action: 'start' }), - }) - .then(response => response.json()) - .then(data => alert(data.message)) - .catch(error => console.error('Error:', error)); - }); - - stopUcButton.addEventListener('click', () => { - console.log('Stop UC Button pressed'); - fetch('/server', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ action: 'stop' }), - }) - .then(response => response.json()) - .then(data => alert(data.message)) - .catch(error => console.error('Error:', error)); - }); - - ceoInfoButton.addEventListener('click', () => { - console.log('Ceo Info Button pressed'); - - const domain = window.location.hostname; - const endpoint = 'http://' + domain + ':5000/admin/ceo'; - const id = getCookie('id'); - - fetch(endpoint, { - method: 'GET', - headers: { - 'Authorization': id, - 'x-api-key': "uc_api" - }, - }) - .then(async response => { - if (!response.ok) { - const responseData = await response.json(); - throw new Error(responseData.message); - } - - return await response.json() - }) - .then(data => { - alert(data.message); - - // Assuming 'data' is the property you want to download - const toDownload = data.data; - const jsonStr = JSON.stringify(toDownload, null, 2); - const blob = new Blob([jsonStr], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - - const a = document.createElement('a'); - a.href = url; - a.download = 'ceo.json'; - document.body.appendChild(a); - a.click(); - - // Cleanup: remove the link and revoke the URL - document.body.removeChild(a); - URL.revokeObjectURL(url); - }) - .catch(error => { - alert(error) - console.error('Error: '+ error) - }); - }) - - usersInfoButton.addEventListener('click', () => { - console.log('Users Info Button pressed'); - - const domain = window.location.hostname; - const endpoint = 'http://' + domain + ':5000/admin/users'; - const id = getCookie('id'); - - fetch(endpoint, { - method: 'GET', - headers: { - 'Authorization': id, - 'x-api-key': "uc_api" - }, - }) - .then(async response => { - if (!response.ok) { - const responseData = await response.json(); - throw new Error(responseData.message); - } - - return await response.json() - }) - .then(data => { - alert(data.message); - - const toDownload = data.data; - const jsonStr = JSON.stringify(toDownload, null, 2); - const blob = new Blob([jsonStr], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - - // Create a temporary link to trigger the download - const a = document.createElement('a'); - a.href = url; - a.download = 'users.json'; - document.body.appendChild(a); - a.click(); - - // Cleanup: remove the link and revoke the URL - document.body.removeChild(a); - URL.revokeObjectURL(url); - }) - .catch(error => { - alert(error) - console.error('Error: '+ error) - }); - }) - - resetUcButton.addEventListener('click', () => { - console.log('Reset UC Button pressed'); - - const domain = window.location.hostname; - const endpoint = 'http://' + domain + ':5000/admin/reset_server'; - const id = getCookie('id'); - - fetch(endpoint, { - method: 'GET', - headers: { - 'Authorization': id, - 'x-api-key': "uc_api" - }, - }) - .then(async response => { - if (!response.ok) { - const responseData = await response.json(); - throw new Error(responseData.message); - } - - return await response.json() - }) - .then(data => alert(data.message)) - .catch(error => { - alert(error) - console.error('Error: '+ error) - }); - }) - - logoutButton.addEventListener('click', () => { - console.log('Logout button was clicked'); - - const deleteCookie = (name) => { - document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; Secure`; - } - - deleteCookie('email'); - deleteCookie('name'); - deleteCookie('id'); - - fadeOut('/login'); - }); -}); diff --git a/UC/frontend/routers/login.js b/UC/frontend/routers/login.js deleted file mode 100644 index 460db13..0000000 --- a/UC/frontend/routers/login.js +++ /dev/null @@ -1,41 +0,0 @@ -const express = require('express'); -const path = require('path'); -const schemas = require('../models/schemas'); -const statusCodes = require('../helpers/status_codes'); - -const config = require('../config/credentials.json'); - -const router = express.Router(); - -router.get('/', (req, res) => { - res.sendFile('login.html', {root: path.join(__dirname, '..', 'public', 'html')}); -}); - -function validateLogin(req, res, next) { - const {error, value} = schemas.loginModelSchema.validate(req.body); - - if (error) { - res.status(statusCodes.BAD_REQUEST).json({error: error.details}); - } else { - next(); - } -} - -router.post('/', validateLogin, (req, res) => { - const { email, password } = req.body; - - if (email === config.email && password === config.password) { - res.status(statusCodes.OK).json({ - success: true, - message: 'Login successful', - user: { - id: config.id, - name: config.name, - email: config.email - } - }); - } else { - res.status(statusCodes.UNAUTHORIZED).json({message: 'Invalid email or password' }); - } -}); -module.exports = router; diff --git a/UC/frontend/routers/mainMenu.js b/UC/frontend/routers/mainMenu.js deleted file mode 100644 index 4b86403..0000000 --- a/UC/frontend/routers/mainMenu.js +++ /dev/null @@ -1,46 +0,0 @@ -const express = require('express'); -const path = require('path'); -const { fork } = require('child_process'); -const httpStatus = require('../helpers/status_codes'); - -const router = express.Router(); -const backendPath = path.join(__dirname, '..', '..', 'backend'); - -router.get('/', (req, res) => { - res.sendFile('main_menu.html', {root: path.join(__dirname, '..', 'public', 'html')}); -}); - -router.post('/server', async (req, res) => { - const action = req.body.action; - let serverProcess = req.app.get('serverProcess'); - - if (action === 'start') { - if (serverProcess) { - return res.status(httpStatus.BAD_REQUEST).send({message: 'Server is already running.'}); - } - const serverPath = path.join(backendPath, 'src', 'app.js'); - serverProcess = fork(serverPath, [], { stdio: 'inherit' }); // Add stdio: 'inherit' to see child process logs in the parent process console - - serverProcess.on('message', (msg) => { - console.log('Message from server:', msg); - }); - - serverProcess.on('close', (code, signal) => { - console.log(`Server process exited with code ${code} and signal ${signal}`); - req.app.set('serverProcess', null); - }); - - req.app.set('serverProcess', serverProcess); // Update the serverProcess in the app - res.status(httpStatus.OK).json({message: 'Server starting...'}); - } else if (action === 'stop') { - if (!serverProcess) { - return res.status(httpStatus.BAD_REQUEST).json({message: 'Server is not running.'}); - } - serverProcess.kill('SIGTERM'); - res.status(httpStatus.OK).json({message: 'Server stopping...'}); - } else { - res.status(httpStatus.BAD_REQUEST).json({message: 'Invalid action.'}); - } -}); - -module.exports = router; diff --git a/UC/package-lock.json b/UC/package-lock.json new file mode 100644 index 0000000..0a7a350 --- /dev/null +++ b/UC/package-lock.json @@ -0,0 +1,2088 @@ +{ + "name": "uc", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "uc", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "async-mutex": "^0.5.0", + "better-sqlite3": "^11.3.0", + "dotenv": "^16.0.3", + "lokijs": "^1.5.12", + "ping": "^0.4.4", + "sqlite3": "^5.1.7", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.11", + "@types/lokijs": "^1.5.14", + "@types/node": "^22.6.1", + "@types/sqlite3": "^3.1.11", + "@types/uuid": "^10.0.0", + "concurrently": "^8.2.2", + "ts-node": "^10.9.2", + "typescript": "^5.6.2" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", + "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.11", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.11.tgz", + "integrity": "sha512-i8KcD3PgGtGBLl3+mMYA8PdKkButvPyARxA7IQAd6qeslht13qxb1zzO8dRCtE7U3IoJS782zDBAeoKiM695kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/lokijs": { + "version": "1.5.14", + "resolved": "https://registry.npmjs.org/@types/lokijs/-/lokijs-1.5.14.tgz", + "integrity": "sha512-4Fic47BX3Qxr8pd12KT6/T1XWU8dOlJBIp1jGoMbaDbiEvdv50rAii+B3z1b/J2pvMywcVP+DBPGP5/lgLOKGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.6.1.tgz", + "integrity": "sha512-V48tCfcKb/e6cVUigLAaJDAILdMP0fUW6BidkPK4GpGjXcfbnoHasCZDwz3N3yVt5we2RHm4XTQCpv0KJz9zqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/sqlite3": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/@types/sqlite3/-/sqlite3-3.1.11.tgz", + "integrity": "sha512-KYF+QgxAnnAh7DWPdNDroxkDI3/MspH1NMx6m/N/6fT1G6+jvsw4/ZePt8R8cr7ta58aboeTfYFBDxTJ5yv15w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.3.0.tgz", + "integrity": "sha512-iHt9j8NPYF3oKCNOO5ZI4JwThjt3Z6J6XrcwG85VNMVzv1ByqrHWv5VILEbCMFWDsoHhXvQ7oC8vgRXFAKgl9w==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT", + "optional": true + }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC", + "optional": true + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "optional": true + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "optional": true + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lokijs": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/lokijs/-/lokijs-1.5.12.tgz", + "integrity": "sha512-Q5ALD6JiS6xAUWCwX3taQmgwxyveCtIIuL08+ml0nHwT3k0S/GIFJN+Hd38b1qYIMaE5X++iqsqWVksz7SYW+Q==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.68.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.68.0.tgz", + "integrity": "sha512-7vbj10trelExNjFSBm5kTvZXXa7pZyKWx9RCKIyqe6I9Ev3IzGpQoqBP3a+cOdxY+pWj6VkP28n/2wWysBHD/A==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ping": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/ping/-/ping-0.4.4.tgz", + "integrity": "sha512-56ZMC0j7SCsMMLdOoUg12VZCfj/+ZO+yfOSjaNCRrmZZr6GLbN2X/Ui56T15dI8NhiHckaw5X2pvyfAomanwqQ==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "optional": true + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "devOptional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/UC/package.json b/UC/package.json new file mode 100644 index 0000000..6a45059 --- /dev/null +++ b/UC/package.json @@ -0,0 +1,31 @@ +{ + "name": "uc", + "version": "1.0.0", + "description": "A backend service with UDP heartbeat and TCP communication secured using Diffie-Hellman key exchange.", + "main": "index.js", + "scripts": { + "start": "ts-node src/index.ts" + }, + "keywords": [], + "author": "Cerbu Andrei-Mihnea", + "license": "ISC", + "dependencies": { + "async-mutex": "^0.5.0", + "better-sqlite3": "^11.3.0", + "dotenv": "^16.0.3", + "lokijs": "^1.5.12", + "ping": "^0.4.4", + "sqlite3": "^5.1.7", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.11", + "@types/lokijs": "^1.5.14", + "@types/node": "^22.6.1", + "@types/sqlite3": "^3.1.11", + "@types/uuid": "^10.0.0", + "concurrently": "^8.2.2", + "ts-node": "^10.9.2", + "typescript": "^5.6.2" + } +} diff --git a/UC/src/db_managers/db.ts b/UC/src/db_managers/db.ts new file mode 100644 index 0000000..66e53a9 --- /dev/null +++ b/UC/src/db_managers/db.ts @@ -0,0 +1,15 @@ +import path from "path"; + +import { UserDatabase } from "./user_database"; +import { DepartmentDatabase } from "./department_database"; +import { KeyDatabase } from "./key_database"; + +const departmentDatabase = new DepartmentDatabase(); +const userDatabase = new UserDatabase(); +const keyDatabase = new KeyDatabase(); + +export { + keyDatabase, + userDatabase, + departmentDatabase +} \ No newline at end of file diff --git a/UC/src/db_managers/department_database.ts b/UC/src/db_managers/department_database.ts new file mode 100644 index 0000000..fb3474c --- /dev/null +++ b/UC/src/db_managers/department_database.ts @@ -0,0 +1,116 @@ +import SQLiteDatabase from './sql_lite_database'; // Singleton instance of SQLite DB +import { v4 as uuidv4 } from 'uuid'; + +export interface Department { + id: string; + name: string; +} + +export class DepartmentDatabase { + private db: any; + + constructor() { + this.db = SQLiteDatabase.getInstance(); + this.createTableIfNotExists(); + } + + // Create the table if it doesn't exist + private createTableIfNotExists() { + const createTableQuery = ` + CREATE TABLE IF NOT EXISTS departments ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL + ); + `; + this.db.exec(createTableQuery); + + const ceoDepartment = this.db.prepare('SELECT COUNT(*) as count FROM departments WHERE name = ?').get('CEO').count; + const defaultDepartment = this.db.prepare('SELECT COUNT(*) as count FROM departments WHERE name = ?').get('Worker').count; + + // Create CEO department if it doesn't exist + if (ceoDepartment === 0) { + this.createDepartment('CEO'); + console.log('Created CEO department.'); + } else { + console.log('CEO department already exists.'); + } + + if (defaultDepartment === 0) { + this.createDepartment('Worker'); + console.log('Created Worker department.'); + } else { + console.log('Worker department already exists.'); + } + + console.log('Departments table checked/created.'); + } + + + // Find a department by name + public findByName(name: string): Department | null { + const stmt = this.db.prepare('SELECT * FROM departments WHERE name = ?'); + const department = stmt.get(name); + return department || null; + } + + // Create a new department + public createDepartment(name: string): Department { + const department: Department = { + id: uuidv4(), + name, + }; + + const stmt = this.db.prepare('INSERT INTO departments (id, name) VALUES (?, ?)'); + stmt.run(department.id, department.name); + return department; + } + + // Find a department by ID + public findById(id: string): Department | null { + const stmt = this.db.prepare('SELECT * FROM departments WHERE id = ?'); + const department = stmt.get(id); + return department || null; + } + + // Modify an existing department by ID + public modifyDepartment(departmentId: string, newName: string): boolean { + const stmt = this.db.prepare('UPDATE departments SET name = ? WHERE id = ?'); + const result = stmt.run(newName, departmentId); + return result.changes > 0; + } + + // Delete a department by ID + public deleteDepartment(departmentId: string): boolean { + const stmt = this.db.prepare('DELETE FROM departments WHERE id = ?'); + const result = stmt.run(departmentId); + return result.changes > 0; + } + + // Get all departments + public getAllDepartments(): Department[] { + const stmt = this.db.prepare('SELECT * FROM departments'); + return stmt.all(); + } + + public cleanTable(): { success: boolean; message: string } { + try { + // Fetch the CEO department ID + const ceoDepartmentRow = this.db.prepare(`SELECT id FROM departments WHERE LOWER(name) = 'ceo'`).get(); + if (!ceoDepartmentRow) { + console.error('CEO department not found in the database.'); + return { success: false, message: 'Failed to find CEO department.' }; + } + + const ceoDepartmentId = ceoDepartmentRow.id; + + // Delete all departments except the CEO department + const deleteDepartmentsStmt = this.db.prepare(`DELETE FROM departments WHERE id != ?`); + deleteDepartmentsStmt.run(ceoDepartmentId); + + return { success: true, message: 'All non-CEO departments have been deleted.' }; + } catch (error) { + console.error('Error while cleaning the departments table:', error); + return { success: false, message: 'Failed to clean departments table.' }; + } + } +} diff --git a/UC/src/db_managers/key_database.ts b/UC/src/db_managers/key_database.ts new file mode 100644 index 0000000..ceeeba8 --- /dev/null +++ b/UC/src/db_managers/key_database.ts @@ -0,0 +1,164 @@ +import { randomBytes } from 'crypto'; +import { v4 as uuidv4 } from 'uuid'; +import SQLiteDatabase from './sql_lite_database'; +import { userDatabase, departmentDatabase } from './db'; + +export interface UserKey { + id: string; // Unique ID for the key entry + userId: string; // ID of the user this key belongs to + key: string; // AES key in Base64 format + iv: string; // Initialization Vector (IV) in Base64 format +} + +export class KeyDatabase { + private db: any; + + constructor() { + // Get the singleton instance of the database + this.db = SQLiteDatabase.getInstance(); + + // Create the keys table if it doesn't exist + this.createTableIfNotExists(); + } + + // Create the table if it doesn't exist + private createTableIfNotExists() { + const createTableQuery = ` + CREATE TABLE IF NOT EXISTS user_keys ( + id TEXT PRIMARY KEY, + userId TEXT UNIQUE NOT NULL, -- Ensure one key per user + key TEXT NOT NULL, + iv TEXT NOT NULL, + FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE + ); + `; + this.db.exec(createTableQuery); + + // Fetch the CEO department and create keys for all users in the CEO department + this.createKeysForCeoUsers(); + } + + // Generate AES key and IV + private generateAESKeyAndIV(): { key: string, iv: string } { + const key = randomBytes(32).toString('base64'); // AES-256 key (32 bytes) + const iv = randomBytes(16).toString('base64'); // IV for AES (16 bytes) + return { key, iv }; + } + + // Create a key for a specific user + private createKeyForUser(userId: string): UserKey { + const { key, iv } = this.generateAESKeyAndIV(); + + // Delete any existing key for the user + this.deleteKeysByUserId(userId); + + // Insert the new key + const newKey: UserKey = { + id: uuidv4(), + userId: userId, + key: key, + iv: iv + }; + + const stmt = this.db.prepare('INSERT INTO user_keys (id, userId, key, iv) VALUES (?, ?, ?, ?)'); + stmt.run(newKey.id, newKey.userId, newKey.key, newKey.iv); + + return newKey; + } + + // Create keys for all users in the CEO department + private createKeysForCeoUsers() { + const departments = departmentDatabase.getAllDepartments(); + + // Find the CEO department by name + const ceoDepartment = departments.find(dept => dept.name.toLowerCase() === 'ceo'); + + if (!ceoDepartment) { + console.error('CEO department not found in the database.'); + return; + } + + // Fetch all users in the CEO department + const ceoUsers = userDatabase.findByDepartmentId(ceoDepartment.id); + + // Create keys for each user in the CEO department + ceoUsers.forEach(user => { + console.log(`Creating key for user ${user.name} (ID: ${user.id})`); + this.createKeyForUser(user.id); + }); + + console.log('Keys created for all users in the CEO department.'); + } + + public createKey(userId: string): UserKey { + const { key, iv } = this.generateAESKeyAndIV(); + + // Check if a key for this user already exists + const existingKey = this.findByUserId(userId); + if (existingKey) { + // Delete the existing key if present + this.deleteKeysByUserId(userId); + } + + // Create the new key + const newKey: UserKey = { + id: uuidv4(), + userId: userId, + key: key, + iv: iv + }; + + const stmt = this.db.prepare('INSERT INTO user_keys (id, userId, key, iv) VALUES (?, ?, ?, ?)'); + stmt.run(newKey.id, newKey.userId, newKey.key, newKey.iv); + + return newKey; + } + + // Find a key by userId (returns null if no key is found) + public findByUserId(userId: string): UserKey | null { + const stmt = this.db.prepare('SELECT * FROM user_keys WHERE userId = ?'); + const row = stmt.get(userId); + return row || null; + } + + // Delete a key by userId (only one key per user is allowed) + public deleteKeysByUserId(userId: string): boolean { + const stmt = this.db.prepare('DELETE FROM user_keys WHERE userId = ?'); + const result = stmt.run(userId); + return result.changes > 0; + } + + // Get all keys (for potential admin purposes) + public getAllKeys(): UserKey[] { + const stmt = this.db.prepare('SELECT * FROM user_keys'); + return stmt.all(); + } + + // Clean the table but keep CEO keys + public cleanTable(): { success: boolean; message: string } { + try { + const departments = departmentDatabase.getAllDepartments(); + const ceoDepartment = departments.find(dept => dept.name.toLowerCase() === 'ceo'); + + if (!ceoDepartment) { + console.error('CEO department not found in the database.'); + return { success: false, message: 'Failed to find CEO department.' }; + } + + // Fetch all users in the CEO department + const ceoUsers = userDatabase.findByDepartmentId(ceoDepartment.id); + const ceoUserIds = ceoUsers.map(user => user.id); + + // Delete all keys except for the CEO users + const stmt = this.db.prepare(` + DELETE FROM user_keys WHERE userId NOT IN (${ceoUserIds.map(() => '?').join(', ')}) + `); + stmt.run(...ceoUserIds); + + return { success: true, message: 'All non-CEO keys have been deleted.' }; + } catch (error) { + console.error('Error while cleaning the keys table:', error); + return { success: false, message: 'Failed to clean keys table.' }; + } + } +} diff --git a/UC/src/db_managers/sql_lite_database.ts b/UC/src/db_managers/sql_lite_database.ts new file mode 100644 index 0000000..620a49b --- /dev/null +++ b/UC/src/db_managers/sql_lite_database.ts @@ -0,0 +1,36 @@ +import Database from 'better-sqlite3'; +import path from "path"; + +class SQLiteDatabase { + // Static variable to hold the single instance + private static instance: Database.Database | null = null; + private static readonly dbFilePath = path.join(__dirname, '..', '..', 'db', 'database.db'); + + // Private constructor prevents direct instantiation + private constructor() {} + + // Static method to get the instance of the database + public static getInstance(): Database.Database { + if (!SQLiteDatabase.instance) { + // If no instance exists, create it + SQLiteDatabase.instance = new Database(SQLiteDatabase.dbFilePath, { + verbose: console.log, // Log queries (optional) + }); + console.log('Database initialized'); + } + + // Return the existing instance + return SQLiteDatabase.instance; + } + + // Optional method to close the database connection + public static closeDatabase(): void { + if (SQLiteDatabase.instance) { + SQLiteDatabase.instance.close(); + SQLiteDatabase.instance = null; + console.log('Database connection closed'); + } + } +} + +export default SQLiteDatabase; diff --git a/UC/src/db_managers/user_database.ts b/UC/src/db_managers/user_database.ts new file mode 100644 index 0000000..7c8ac70 --- /dev/null +++ b/UC/src/db_managers/user_database.ts @@ -0,0 +1,235 @@ +import { pbkdf2Sync, randomBytes } from 'crypto'; +import { v4 as uuidv4 } from 'uuid'; +import SQLiteDatabase from './sql_lite_database'; +import { departmentDatabase} from "./db"; +import * as dotenv from 'dotenv'; + +dotenv.config(); // Load environment variables from the .env file + +export interface User { + id: string; + name: string; + email: string; + hashedPassword: string, + salt: string, + departmentId: string; + app_type: string; +} + +export class UserDatabase { + private db: any; + + constructor() { + // Get the singleton instance of the database + this.db = SQLiteDatabase.getInstance(); + + // Check if the table exists and create it if not + this.createTableIfNotExists(); + } + + // Create the table if it doesn't exist + private createTableIfNotExists() { + const createTableQuery = ` + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + salt TEXT NOT NULL, + hashedPassword TEXT NOT NULL, + departmentId TEXT NOT NULL, + app_type TEXT NOT NULL + ); + `; + this.db.exec(createTableQuery); + + // Check if any users already exist + const userCount = this.db.prepare('SELECT COUNT(*) as count FROM users').get().count; + + if (userCount === 0) { + console.log('No users found, creating default CEO user...'); + + // Fetch departments + const departments = departmentDatabase.getAllDepartments(); + + // Find department ID for CEO + const ceoDepartment = departments.find(dept => dept.name.toLowerCase() === 'ceo'); + + if (!ceoDepartment) { + console.error('CEO department not found in the database.'); + return; + } + + // Get CEO credentials from the .env file + const ceoEmail = process.env.CEO_EMAIL || 'ceo@yourfirm.com'; + const ceoPassword = process.env.CEO_PASSWORD || 'Password123!'; + + // Create the CEO user with the credentials from .env + this.createUser('CEO', ceoEmail, ceoPassword, ceoDepartment.id, 'ceo'); + + console.log('Default CEO user created.'); + } else { + console.log('Users already exist, skipping creation of default CEO user.'); + } + } + + // Find a user by email + public findByEmail(email: string): User | null { + const stmt = this.db.prepare('SELECT * FROM users WHERE email = ?'); + const row = stmt.get(email); + return row || null; + } + + // Find a user by ID + public findById(id: string): User | null { + const stmt = this.db.prepare('SELECT * FROM users WHERE id = ?'); + const row = stmt.get(id); + return row || null; + } + + // Create a new user + public createUser(name: string, email: string, password: string, departmentId: string, app_type: string): User { + const salt = randomBytes(16).toString('hex'); + const hashedPassword = this.hashPassword(password, salt); + + const user: User = { + id: uuidv4(), + name, + email, + salt, + hashedPassword, + departmentId: departmentId, + app_type: app_type + }; + + const stmt = this.db.prepare('INSERT INTO users (id, name, email, salt, hashedPassword, departmentId, app_type) VALUES (?, ?, ?, ?, ?, ?, ?)'); + stmt.run(user.id, user.name, user.email, user.salt, user.hashedPassword, user.departmentId, user.app_type); + return user; + } + + // Modify an existing user + public modifyUser(id: string, name: string, email: string, password: string, departmentId: string, app_type: string): boolean { + // Find the user by ID + const user = this.findById(id); + if (!user) return false; + + // Hash the new password using the existing salt + const hashedPassword = this.hashPassword(password, user.salt); + + // Prepare the updated user data + const updatedUser: User = { + ...user, + name: name, + email: email, + salt: user.salt, // Use the same salt + hashedPassword: hashedPassword, // Use the newly hashed password + departmentId: departmentId, + app_type: app_type, + }; + + // Prepare and execute the SQL query to update the user + const stmt = this.db.prepare(` + UPDATE users + SET name = ?, email = ?, salt = ?, hashedPassword = ?, departmentId = ?, app_type = ? + WHERE id = ? + `); + + stmt.run(updatedUser.name, updatedUser.email, updatedUser.salt, updatedUser.hashedPassword, updatedUser.departmentId, updatedUser.app_type, updatedUser.id); + + return true; + } + + // Delete a user by ID + public deleteUser(id: string): boolean { + const stmt = this.db.prepare('DELETE FROM users WHERE id = ?'); + const result = stmt.run(id); + return result.changes > 0; + } + + public findByDepartmentId(departmentId: string): User[] { + const stmt = this.db.prepare('SELECT * FROM users WHERE departmentId = ?'); + return stmt.all(departmentId); + } + + // Get all users + public getAllUsers(): User[] { + const stmt = this.db.prepare('SELECT * FROM users'); + return stmt.all(); + } + + // Verify email, password, and app_type for authentication + public verifyCredentials(email: string, password: string, app_type: string): { success: boolean; message: string } { + const user = this.findByEmail(email); + + if (!user) { + return { success: false, message: 'User not found.' }; + } + + // Check if app_type matches + if (user.app_type !== app_type) { + return { success: false, message: `Access denied for app type: ${app_type}.` }; + } + + // Check if password matches + const hashedPassword = this.hashPassword(password, user.salt); + if (hashedPassword === user.hashedPassword) { + return { success: true, message: 'Authentication successful.' }; + } else { + return { success: false, message: 'Incorrect password.' }; + } + } + + // Reset password by user email and new password + public resetPassword(email: string, newPassword: string, app_type: string): { success: boolean; message: string } { + const user = this.findByEmail(email); + if (!user) { + return { success: false, message: 'User not found.' }; + } + + if(user.app_type !== app_type){ + return { success: false, message: 'Invalid app type for operation,' }; + } + + // Generate a new salt for the new password + const newSalt = randomBytes(16).toString('hex'); + const newHashedPassword = this.hashPassword(newPassword, newSalt); + + const stmt = this.db.prepare('UPDATE users SET salt = ?, hashedPassword = ? WHERE email = ?'); + const result = stmt.run(newSalt, newHashedPassword, email); + + if (result.changes > 0) { + return { success: true, message: 'Password reset successfully.' }; + } else { + return { success: false, message: 'Failed to reset password.' }; + } + } + + // Helper method to hash the password with the salt + private hashPassword(password: string, salt: string): string { + return pbkdf2Sync(password, salt, 1000, 64, 'sha256').toString('hex'); + } + + public cleanTable(): { success: boolean; message: string } { + try { + // Fetch departments + const departments = departmentDatabase.getAllDepartments(); + + const ceoDepartment = departments.find(dept => dept.name.toLowerCase() === 'ceo'); + + if (!ceoDepartment) { + console.error('Admin or CEO department not found in the database.'); + return { success: false, message: 'Failed to find Admin or CEO department.' }; + } + + // Delete users from the table except for those in the Admin and CEO departments + const stmt = this.db.prepare(` + DELETE FROM users WHERE departmentId NOT IN (?, ?) + `); + stmt.run(ceoDepartment.id); + + return { success: true, message: 'All users except Admin and CEO have been deleted.' }; + } catch (error) { + console.error('Error while cleaning the users table:', error); + return { success: false, message: 'Failed to clean users table.' }; + } + } +} diff --git a/UC/src/index.ts b/UC/src/index.ts new file mode 100644 index 0000000..8a4ee03 --- /dev/null +++ b/UC/src/index.ts @@ -0,0 +1,25 @@ +import {TcpServer} from "./tcp_server"; +import {UdpServer} from "./udp_server"; +import dotenv from "dotenv"; +import path from "path"; + +dotenv.config({ path: path.join('..', '.env') }); + +const UDP_PORT: number = process.env.UDP_PORT ? parseInt(process.env.UDP_PORT) : 41234; +const TCP_PORT: number = process.env.TCP_PORT? parseInt(process.env.TCP_PORT): 41233 +const HOST: string = process.env.HOST || '0.0.0.0'; + +// Function to handle server logs (not needed when starting directly) +const handleServerLogs = (serverName: string): void => { + console.log(`${serverName} started successfully.`); +}; + +// Start the UDP server +const udpServer = new UdpServer(HOST, UDP_PORT); +udpServer.start(); +handleServerLogs('UDP Server'); + +// Start the TCP server +const tcpServer = new TcpServer(HOST, TCP_PORT); +tcpServer.start(); +handleServerLogs('TCP Server'); diff --git a/UC/src/network/connection_manager.ts b/UC/src/network/connection_manager.ts new file mode 100644 index 0000000..f7f29f8 --- /dev/null +++ b/UC/src/network/connection_manager.ts @@ -0,0 +1,46 @@ +import { SocketCommunicatorBase } from "./socket_communicator/socket_communicator_base"; + +interface Connection { + communicator: SocketCommunicatorBase; +} + +export class ConnectionManager { + private readonly connections: { [key: string]: Connection }; + + constructor() { + this.connections = {}; + } + + // Adds a new communicator, keyed by both IP and port + addConnection(ip: string, port: number, communicator: SocketCommunicatorBase): void { + const key = `${ip}:${port}`; + + // Store the communicator along with the client's public and private keys + this.connections[key] = { + communicator + }; + + console.log(`Added communicator for ${ip}:${port}, generated public and private keys.`); + } + + // Removes a communicator based on IP and port + removeCommunicator(ip: string, port: number): void { + const key = `${ip}:${port}`; + if (this.connections[key]) { + delete this.connections[key]; + console.log(`Removed communicator for ${ip}:${port}`); + } + } + + // Retrieves a communicator based on IP and port + getCommunicator(ip: string, port: number): SocketCommunicatorBase | null { + const key = `${ip}:${port}`; + return this.connections[key] ? this.connections[key].communicator : null; + } + + // Checks if a communicator exists for a given IP and port + communicatorExists(ip: string, port: number): boolean { + const key = `${ip}:${port}`; + return this.connections[key] !== undefined; + } +} diff --git a/UC/src/network/message_handler.ts b/UC/src/network/message_handler.ts new file mode 100644 index 0000000..6583a1d --- /dev/null +++ b/UC/src/network/message_handler.ts @@ -0,0 +1,65 @@ +export interface ParsedMessage { + operationCode: string; + metaInfo?: { [key: string]: any }; + fileContent?: Buffer; +} + +export class MessageHandler { + // Format the message with operationCode, guid, metaInfo, and fileContent (Base64 for fileContent) + static formatMessage( + operationCode: string, + metaInfo?: { [key: string]: any }, + fileContent?: Buffer + ): string { + let message = `${operationCode}\n`; // First part: operationCode and guid + + if (metaInfo && Object.keys(metaInfo).length > 0) { + message += `${JSON.stringify(metaInfo)}\n`; // Add metaInfo + } + + if (fileContent && fileContent.length > 0) { + message += fileContent.toString('base64'); // Convert buffer to Base64 for fileContent + } + + return message; + } + + // Parse the incoming message (convert Base64 back to Buffer if fileContent is present) + static parseMessage(msg: string): ParsedMessage { + const parts = msg.split('\n'); // Split by \n (operationCode, metaInfo, and fileContent are on separate lines) + + // First part should always be the operation code + const operationCode = parts[0]?.trim(); + if (!operationCode) { + throw new Error('Missing operation code in the message'); + } + + let metaInfo: { [key: string]: any } | undefined = undefined; + let fileContent: Buffer | undefined = undefined; + + // Parse the metaInfo (JSON object) if present + if (parts[1]) { + try { + metaInfo = JSON.parse(parts[1].trim()); + } catch (err) { + console.error('Invalid metaInfo JSON format:', err); + } + } + + // Convert Base64 string back to Buffer for fileContent if present + if (parts[2]) { + fileContent = Buffer.from(parts[2].trim(), 'base64'); + } + + return { + operationCode, + metaInfo, + fileContent, + }; + } + + // Validate if the parsed message contains an operation code + static validateMessage(parsedMessage: ParsedMessage | null): boolean { + return !!parsedMessage?.operationCode; + } +} \ No newline at end of file diff --git a/UC/src/network/operations_base/operation_base.ts b/UC/src/network/operations_base/operation_base.ts new file mode 100644 index 0000000..d0d62da --- /dev/null +++ b/UC/src/network/operations_base/operation_base.ts @@ -0,0 +1,43 @@ +import { ParsedMessage } from '../message_handler'; +import { OperationHandler } from './operation_handler'; +import { OperationPlugin } from './operation_plugin'; + +export abstract class OperationBase implements OperationPlugin { + // Shared operation codes + public static readonly operationCodes = { + OK: 'OK', + ERR: 'ERR', + END: 'END', + }; + + // Default handler for OK operation + public static handleOk(parsedMessage: ParsedMessage): ParsedMessage { + console.log('OK operation received'); + return parsedMessage; // Typically, you would just acknowledge with OK, returning as-is + } + + // Default handler for ERR operation + public static handleErr(parsedMessage: ParsedMessage): ParsedMessage { + console.log('ERR operation received: ', parsedMessage.metaInfo?.message || 'No error details provided'); + return parsedMessage; // Typically, you would log the error and return + } + + // Default handler for END operation + public static handleEnd(parsedMessage: ParsedMessage): ParsedMessage { + console.log('END operation received'); + return { + operationCode: OperationBase.operationCodes.END, + metaInfo: { message: 'Connection ended.' }, + }; + } + + // Register the common OK, ERR, and END handlers + public static registerCommonOperations(operationHandler: OperationHandler): void { + operationHandler.registerHandler(OperationBase.operationCodes.OK, OperationBase.handleOk); + operationHandler.registerHandler(OperationBase.operationCodes.ERR, OperationBase.handleErr); + operationHandler.registerHandler(OperationBase.operationCodes.END, OperationBase.handleEnd); + } + + // Abstract register method that will be implemented by subclasses + public abstract register(operationHandler: OperationHandler): void; +} diff --git a/UC/src/network/operations_base/operation_handler.ts b/UC/src/network/operations_base/operation_handler.ts new file mode 100644 index 0000000..0f0b10f --- /dev/null +++ b/UC/src/network/operations_base/operation_handler.ts @@ -0,0 +1,63 @@ +// operation_handler.ts +import { ParsedMessage, MessageHandler } from '../message_handler'; +import { OperationPlugin } from './operation_plugin'; + +type OperationHandlerFunction = (parsedMessage: ParsedMessage) => ParsedMessage; + +export class OperationHandler { + private static instance: OperationHandler; + private handlers: { [operationCode: string]: OperationHandlerFunction } = {}; + + private constructor() { + // Register only the unknown command handler on initialization + this.registerHandler('UNKNOWN_COMMAND', this.handleUnknownCommand); + } + + // Singleton instance + public static getInstance(): OperationHandler { + if (!OperationHandler.instance) { + OperationHandler.instance = new OperationHandler(); + } + return OperationHandler.instance; + } + + // Register a handler for a specific operation code + public registerHandler(operationCode: string, handler: OperationHandlerFunction): void { + this.handlers[operationCode] = handler; + } + + // Handle operation request + public handleOperation(rawMessage: string): ParsedMessage { + // Parse and validate the message + const parsedMessage = MessageHandler.parseMessage(rawMessage); + if (!parsedMessage || !MessageHandler.validateMessage(parsedMessage)) { + return this.handleUnknownCommand(parsedMessage); + } + + // Dispatch the handler for the given operation code + const handler = this.handlers[parsedMessage.operationCode]; + if (handler) { + return handler(parsedMessage); + } else { + return this.handleUnknownCommand(parsedMessage); + } + } + + // Get all registered operation codes + public getAvailableOperationCodes(): string[] { + return Object.keys(this.handlers); + } + + // Default handler for unknown commands + private handleUnknownCommand(parsedMessage?: ParsedMessage): ParsedMessage { + return { + operationCode: 'UNKNOWN_COMMAND', + metaInfo: { message: 'Unknown command received.' }, + }; + } + + // Plugin system: Load plugins to register handlers + public loadPlugin(plugin: OperationPlugin): void { + plugin.register(this); + } +} diff --git a/UC/src/network/operations_base/operation_plugin.ts b/UC/src/network/operations_base/operation_plugin.ts new file mode 100644 index 0000000..6b11a29 --- /dev/null +++ b/UC/src/network/operations_base/operation_plugin.ts @@ -0,0 +1,6 @@ +// operation_plugin.ts +import { OperationHandler } from './operation_handler'; + +export interface OperationPlugin { + register(operationHandler: OperationHandler): void; +} diff --git a/UC/src/network/operations_custom/auth_operations.ts b/UC/src/network/operations_custom/auth_operations.ts new file mode 100644 index 0000000..580fe35 --- /dev/null +++ b/UC/src/network/operations_custom/auth_operations.ts @@ -0,0 +1,183 @@ +import { ParsedMessage } from '../message_handler'; +import {userDatabase, departmentDatabase, keyDatabase} from '../../db_managers/db'; +import { OperationBase } from '../operations_base/operation_base'; +import { OperationHandler } from '../operations_base/operation_handler'; + +export class AuthOperations extends OperationBase { + public static readonly operationCodes = { + ...OperationBase.operationCodes, + LOGIN: 'LOGIN', + SIGN_UP: 'SIGN_UP', + RESET_PASSWORD: 'RESET_PASSWORD' + }; + + // Utility function to validate email format + private static isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + } + + // Utility function to validate password strength (min 8 chars, at least 1 number and 1 special char) + private static isStrongPassword(password: string): boolean { + const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/; + return passwordRegex.test(password); + } + + // Utility function to check name is not empty + private static isValidName(name: string): boolean { + return name.trim().length > 0; + } + + private static isValidAppType(app_type: string){ + const app_types = ['client', 'ceo', 'admin']; + return app_types.includes(app_type); + } + + // Handle Login operation with validation + public static handleLogin(parsedMessage: ParsedMessage): ParsedMessage { + const { email, password, app_type } = parsedMessage.metaInfo || {}; + + // Check if email and password are provided + if (!email || !password || !app_type) { + return { + operationCode: OperationBase.operationCodes.ERR, + metaInfo: { message: 'Both email and password are required.' }, + }; + } + + // Verify email and password with the database + const result = userDatabase.verifyCredentials(email, password, app_type); + + // Handle the case where the credentials are incorrect + if (!result.success) { + return { + operationCode: OperationBase.operationCodes.ERR, + metaInfo: { message: result.message }, + }; + } + + return { + operationCode: OperationBase.operationCodes.OK, + metaInfo: { message: 'Login successful.' }, + }; + } + + // Handle SignUp operation with validation + public static handleSignUp(parsedMessage: ParsedMessage): ParsedMessage { + const { name, email, password, departmentId, app_type } = parsedMessage.metaInfo || {}; + + // Validate name + if (!AuthOperations.isValidName(name)) { + return { + operationCode: OperationBase.operationCodes.ERR, + metaInfo: { message: 'Invalid name. Please provide a valid name.' }, + }; + } + + // Validate email format + if (!AuthOperations.isValidEmail(email)) { + return { + operationCode: OperationBase.operationCodes.ERR, + metaInfo: { message: 'Invalid email format.' }, + }; + } + + // Validate password strength + if (!AuthOperations.isStrongPassword(password)) { + return { + operationCode: OperationBase.operationCodes.ERR, + metaInfo: { + message: 'Weak password. It must be at least 8 characters long, contain at least 1 letter, 1 number, and 1 special character.', + }, + }; + } + + if (!AuthOperations.isValidAppType(app_type)) { + return { + operationCode: OperationBase.operationCodes.ERR, + metaInfo: { + message: 'Not valid app_type.', + }, + }; + } + + // Check if user with this email already exists + const existingUser = userDatabase.findByEmail(email); + if (existingUser) { + return { + operationCode: OperationBase.operationCodes.ERR, + metaInfo: { message: 'Email already in use.' }, + }; + } + + const existingUserByName = userDatabase.getAllUsers().find((user) => user.name === name); + if (existingUserByName) { + return { + operationCode: OperationBase.operationCodes.ERR, + metaInfo: { message: 'Name already in use.' }, + }; + } + + // Validate department ID + const departmentEntry = departmentDatabase.findById(departmentId); + if (!departmentEntry) { + return { + operationCode: OperationBase.operationCodes.ERR, + metaInfo: { message: 'Invalid department.' }, + }; + } + + // Create new user in the database + const newUser = userDatabase.createUser(name, email, password, departmentEntry.id, app_type); + + // Create a key for the new user + keyDatabase.createKey(newUser.id); + + return { + operationCode: OperationBase.operationCodes.OK, + metaInfo: { + message: 'User created successfully.', + userId: newUser.id, + }, + }; + } + + // Handle Reset Password operation + public static handleResetPassword(parsedMessage: ParsedMessage): ParsedMessage { + const { email, newPassword, app_type } = parsedMessage.metaInfo || {}; + + if (!AuthOperations.isStrongPassword(newPassword)) { + return { + operationCode: OperationBase.operationCodes.ERR, + metaInfo: { + message: 'Weak password. It must be at least 8 characters long, contain at least 1 letter, 1 number, and 1 special character.', + }, + }; + } + + // Logic to reset password (could be sending a reset link, or generating a temp password) + const resetResult = userDatabase.resetPassword(email, newPassword, app_type); + + if (resetResult.success) { + return { + operationCode: OperationBase.operationCodes.OK, + metaInfo: { message: 'Password reset successfully. Please check your email for instructions.' }, + }; + } else { + return { + operationCode: OperationBase.operationCodes.ERR, + metaInfo: { message: 'Failed to reset password.' }, + }; + } + } + + // Register specific operations for AuthOperations + public register(operationHandler: OperationHandler): void { + operationHandler.registerHandler(AuthOperations.operationCodes.LOGIN, AuthOperations.handleLogin); + operationHandler.registerHandler(AuthOperations.operationCodes.SIGN_UP, AuthOperations.handleSignUp); + operationHandler.registerHandler(AuthOperations.operationCodes.RESET_PASSWORD, AuthOperations.handleResetPassword) + + // Register common OK, ERR, and END operations from the base class + OperationBase.registerCommonOperations(operationHandler); + } +} diff --git a/UC/src/network/operations_custom/ceo_operations.ts b/UC/src/network/operations_custom/ceo_operations.ts new file mode 100644 index 0000000..c912961 --- /dev/null +++ b/UC/src/network/operations_custom/ceo_operations.ts @@ -0,0 +1,42 @@ +import { ParsedMessage } from '../message_handler'; +import { OperationBase } from '../operations_base/operation_base'; +import { + departmentDatabase, + keyDatabase, + userDatabase +} from '../../db_managers/db'; +import { OperationHandler } from '../operations_base/operation_handler'; + +export class CeoOperations extends OperationBase { + public static readonly operationCodes = { + ...OperationBase.operationCodes, // Inherit common operation codes (OK, ERR, END) + RESET_DATABASE: 'RESET_DATABASE', + }; + + // Handle reset database operation + public static handleResetDatabase(): ParsedMessage { + console.log('Resetting the database...'); + + departmentDatabase.cleanTable(); + userDatabase.cleanTable(); + keyDatabase.cleanTable(); + + // Create the departments + departmentDatabase.createDepartment('Worker'); + + return { + operationCode: CeoOperations.operationCodes.OK, + metaInfo: { + message: 'Database reset successfully.', + }, + }; + } + + // Register general operations with the OperationHandler + public register(operationHandler: OperationHandler): void { + operationHandler.registerHandler(CeoOperations.operationCodes.RESET_DATABASE, CeoOperations.handleResetDatabase); + + // Register common operations inherited from the base class + OperationBase.registerCommonOperations(operationHandler); + } +} diff --git a/UC/src/network/operations_custom/department_operations.ts b/UC/src/network/operations_custom/department_operations.ts new file mode 100644 index 0000000..de28189 --- /dev/null +++ b/UC/src/network/operations_custom/department_operations.ts @@ -0,0 +1,129 @@ +import { ParsedMessage } from '../message_handler'; +import {departmentDatabase} from '../../db_managers/db'; +import { OperationBase } from '../operations_base/operation_base'; +import { OperationHandler } from '../operations_base/operation_handler'; + +export class DepartmentOperations extends OperationBase { + public static readonly operationCodes = { + ...OperationBase.operationCodes, + GET_DEPARTMENTS: 'GET_DEPARTMENTS', + CREATE_DEPARTMENT: 'CREATE_DEPARTMENT', + MODIFY_DEPARTMENT: 'MODIFY_DEPARTMENT', + DELETE_DEPARTMENT: 'DELETE_DEPARTMENT', + FIND_DEPARTMENT_BY_ID: 'FIND_DEPARTMENT_BY_ID' + }; + + // Get all departments + public static handleGetDepartments(): ParsedMessage { + const departments = departmentDatabase.getAllDepartments(); + return { + operationCode: DepartmentOperations.operationCodes.OK, + metaInfo: { departments }, + }; + } + + public static handleGetDepartmentById(parsedMessage: ParsedMessage): ParsedMessage { + const { id } = parsedMessage.metaInfo || {}; + + if(!id){ + return { + operationCode: DepartmentOperations.operationCodes.ERR, + metaInfo: { message: 'Id is required.' }, + }; + } + + const user = departmentDatabase.findById(id); + return { + operationCode: user === null ? DepartmentOperations.operationCodes.ERR : DepartmentOperations.operationCodes.OK, + metaInfo: user === null ? { message: 'Department not found.' } : { message: user } + } + } + + // Create a new department + public static handleCreateDepartment(parsedMessage: ParsedMessage): ParsedMessage { + // @ts-ignore + const { departmentName } = parsedMessage.metaInfo; + + // Check if department name is provided + if (!departmentName) { + return { + operationCode: DepartmentOperations.operationCodes.ERR, + metaInfo: { message: 'Department name is required.' }, + }; + } + + // Check if the department already exists + const existingDepartment = departmentDatabase.findByName(departmentName); + if (existingDepartment) { + return { + operationCode: DepartmentOperations.operationCodes.ERR, + metaInfo: { message: 'Department already exists.' }, + }; + } + + // Create the new department + const newDepartment = departmentDatabase.createDepartment(departmentName); + + return { + operationCode: DepartmentOperations.operationCodes.OK, + metaInfo: { message: 'Department created successfully.', departmentId: newDepartment.id }, + }; + } + + // Modify an existing department + public static handleModifyDepartment(parsedMessage: ParsedMessage): ParsedMessage { + // @ts-ignore + const { departmentId, newDepartmentName } = parsedMessage.metaInfo; + + // Validate input + if (!departmentId || !newDepartmentName) { + return { + operationCode: DepartmentOperations.operationCodes.ERR, + metaInfo: { message: 'Department ID and new department name are required.' }, + }; + } + + // Modify the department + const isSuccess = departmentDatabase.modifyDepartment(departmentId, newDepartmentName); + + return { + operationCode: isSuccess ? DepartmentOperations.operationCodes.OK : DepartmentOperations.operationCodes.ERR, + metaInfo: { message: isSuccess ? 'Department modified successfully.' : 'Failed to modify department.' }, + }; + } + + // Delete an existing department + public static handleDeleteDepartment(parsedMessage: ParsedMessage): ParsedMessage { + // @ts-ignore + const { departmentId } = parsedMessage.metaInfo; + + // Validate input + if (!departmentId) { + return { + operationCode: DepartmentOperations.operationCodes.ERR, + metaInfo: { message: 'Department ID is required.' }, + }; + } + + // Delete the department + const isSuccess = departmentDatabase.deleteDepartment(departmentId); + + return { + operationCode: isSuccess ? DepartmentOperations.operationCodes.OK : DepartmentOperations.operationCodes.ERR, + metaInfo: { message: isSuccess ? 'Department deleted successfully.' : 'Failed to delete department.' }, + }; + } + + // Register department operations_base with the OperationHandler + public register(operationHandler: OperationHandler): void { + // Register the department operation handlers + operationHandler.registerHandler(DepartmentOperations.operationCodes.GET_DEPARTMENTS, DepartmentOperations.handleGetDepartments); + operationHandler.registerHandler(DepartmentOperations.operationCodes.CREATE_DEPARTMENT, DepartmentOperations.handleCreateDepartment); + operationHandler.registerHandler(DepartmentOperations.operationCodes.MODIFY_DEPARTMENT, DepartmentOperations.handleModifyDepartment); + operationHandler.registerHandler(DepartmentOperations.operationCodes.DELETE_DEPARTMENT, DepartmentOperations.handleDeleteDepartment); + operationHandler.registerHandler(DepartmentOperations.operationCodes.FIND_DEPARTMENT_BY_ID, DepartmentOperations.handleGetDepartmentById); + + // Register common operations_base inherited from the base class + OperationBase.registerCommonOperations(operationHandler); + } +} diff --git a/UC/src/network/operations_custom/general_operations.ts b/UC/src/network/operations_custom/general_operations.ts new file mode 100644 index 0000000..a9a448f --- /dev/null +++ b/UC/src/network/operations_custom/general_operations.ts @@ -0,0 +1,79 @@ +import { ParsedMessage } from '../message_handler'; +import { OperationBase } from '../operations_base/operation_base'; +import { OperationHandler } from '../operations_base/operation_handler'; +import os from 'node:os'; + +export class GeneralOperations extends OperationBase { + public static readonly operationCodes = { + ...OperationBase.operationCodes, // Inherit common operation codes (OK, ERR, END) + HEARTBEAT: 'HEARTBEAT', + ALIVE: 'ALIVE', + SET_PUBLIC_KEY: 'SET_PUBLIC_KEY', + SET_AES_KEY: 'SET_AES_KEY', // New operation for AES key + }; + + // Handle heartbeat operation + public static handleHeartbeat(): ParsedMessage { + const networkInterfaces = os.networkInterfaces(); + let ipAddress = 'Unknown'; + + for (const iface of Object.values(networkInterfaces)) { + // @ts-ignore + for (const address of iface) { + if (address.family === 'IPv4' && !address.internal) { + ipAddress = address.address; + break; + } + } + if (ipAddress !== 'Unknown') break; + } + + return { + operationCode: GeneralOperations.operationCodes.ALIVE, + metaInfo: { ipAddress }, + }; + } + + // Handle public key exchange + public static handlePublicKey(parsedMessage: ParsedMessage): ParsedMessage { + const clientPublicKey = parsedMessage.metaInfo?.publicKey; + if (clientPublicKey) { + return { + operationCode: GeneralOperations.operationCodes.SET_PUBLIC_KEY, + metaInfo: { message: clientPublicKey }, + }; + } else { + return { + operationCode: GeneralOperations.operationCodes.ERR, + metaInfo: { message: 'No public key provided.' }, + }; + } + } + + // Handle AES key exchange + public static handleAESKey(parsedMessage: ParsedMessage): ParsedMessage { + const aesKey = parsedMessage.metaInfo?.aesKey; + if (aesKey) { + return { + operationCode: GeneralOperations.operationCodes.SET_AES_KEY, + metaInfo: { message: aesKey }, + }; + } else { + return { + operationCode: GeneralOperations.operationCodes.ERR, + metaInfo: { message: 'No AES key provided.' }, + }; + } + } + + // Register general operations with the OperationHandler + public register(operationHandler: OperationHandler): void { + // Register specific handlers for the general operations + operationHandler.registerHandler(GeneralOperations.operationCodes.HEARTBEAT, GeneralOperations.handleHeartbeat); + operationHandler.registerHandler(GeneralOperations.operationCodes.SET_PUBLIC_KEY, GeneralOperations.handlePublicKey); + operationHandler.registerHandler(GeneralOperations.operationCodes.SET_AES_KEY, GeneralOperations.handleAESKey); // Register AES key handler + + // Register common operations inherited from the base class + OperationBase.registerCommonOperations(operationHandler); + } +} diff --git a/UC/src/network/operations_custom/key_operations.ts b/UC/src/network/operations_custom/key_operations.ts new file mode 100644 index 0000000..a7997ea --- /dev/null +++ b/UC/src/network/operations_custom/key_operations.ts @@ -0,0 +1,90 @@ +import { ParsedMessage } from '../message_handler'; +import { keyDatabase } from '../../db_managers/db'; +import { OperationBase } from '../operations_base/operation_base'; +import { OperationHandler } from '../operations_base/operation_handler'; + +export class KeyOperations extends OperationBase { + public static readonly operationCodes = { + ...OperationBase.operationCodes, + GET_KEYS: 'GET_KEYS', + CREATE_KEY: 'CREATE_KEY', + DELETE_KEY: 'DELETE_KEY', + FIND_KEY_BY_USER_ID: 'FIND_KEY_BY_USER_ID', + }; + + // Get all keys + public static handleGetKeys(): ParsedMessage { + const keys = keyDatabase.getAllKeys(); + console.log(keys); + return { + operationCode: KeyOperations.operationCodes.OK, + metaInfo: { keys }, + }; + } + + // Create a key for a user + public static handleCreateKey(parsedMessage: ParsedMessage): ParsedMessage { + const { userId } = parsedMessage.metaInfo || {}; + + if (!userId) { + return { + operationCode: KeyOperations.operationCodes.ERR, + metaInfo: { message: 'User ID is required to create a key.' }, + }; + } + + // Create the key for the user + const key = keyDatabase.createKey(userId); + return { + operationCode: KeyOperations.operationCodes.OK, + metaInfo: { key }, + }; + } + + // Find a key by user ID + public static handleFindKeyByUserId(parsedMessage: ParsedMessage): ParsedMessage { + const { userId } = parsedMessage.metaInfo || {}; + + if (!userId) { + return { + operationCode: KeyOperations.operationCodes.ERR, + metaInfo: { message: 'User ID is required.' }, + }; + } + + const key = keyDatabase.findByUserId(userId); + return { + operationCode: key === null ? KeyOperations.operationCodes.ERR : KeyOperations.operationCodes.OK, + metaInfo: key === null ? { message: 'Key not found.' } : { key }, + }; + } + + // Delete a key by user ID + public static handleDeleteKey(parsedMessage: ParsedMessage): ParsedMessage { + const { userId } = parsedMessage.metaInfo || {}; + + if (!userId) { + return { + operationCode: KeyOperations.operationCodes.ERR, + metaInfo: { message: 'User ID is required for deletion.' }, + }; + } + + const isSuccess = keyDatabase.deleteKeysByUserId(userId); + return { + operationCode: isSuccess ? KeyOperations.operationCodes.OK : KeyOperations.operationCodes.ERR, + metaInfo: { message: isSuccess ? 'Key deleted successfully.' : 'Failed to delete key.' }, + }; + } + + // Register key operations with the OperationHandler + public register(operationHandler: OperationHandler): void { + operationHandler.registerHandler(KeyOperations.operationCodes.GET_KEYS, KeyOperations.handleGetKeys); + operationHandler.registerHandler(KeyOperations.operationCodes.CREATE_KEY, KeyOperations.handleCreateKey); + operationHandler.registerHandler(KeyOperations.operationCodes.DELETE_KEY, KeyOperations.handleDeleteKey); + operationHandler.registerHandler(KeyOperations.operationCodes.FIND_KEY_BY_USER_ID, KeyOperations.handleFindKeyByUserId); + + // Register common operations inherited from OperationBase + OperationBase.registerCommonOperations(operationHandler); + } +} diff --git a/UC/src/network/operations_custom/user_operations.ts b/UC/src/network/operations_custom/user_operations.ts new file mode 100644 index 0000000..8a53903 --- /dev/null +++ b/UC/src/network/operations_custom/user_operations.ts @@ -0,0 +1,136 @@ +import { ParsedMessage } from '../message_handler'; +import {userDatabase} from '../../db_managers/db'; +import { OperationBase } from '../operations_base/operation_base'; +import { OperationHandler } from '../operations_base/operation_handler'; + +export class UserOperations extends OperationBase { + public static readonly operationCodes = { + ...OperationBase.operationCodes, + GET_USERS: 'GET_USERS', + CREATE_USER: 'CREATE_USER', + MODIFY_USER: 'MODIFY_USER', + DELETE_USER: 'DELETE_USER', + FIND_BY_ID: 'FIND_BY_ID', + FIND_BY_EMAIL: 'FIND_BY_EMAIL', + }; + + // Get all users + public static handleGetUsers(): ParsedMessage { + const users = userDatabase.getAllUsers(); + console.log(users); + return { + operationCode: UserOperations.operationCodes.OK, + metaInfo: { users }, + }; + } + + public static handleGetUserById(parsedMessage: ParsedMessage): ParsedMessage { + const { id } = parsedMessage.metaInfo || {}; + + if(!id){ + return { + operationCode: UserOperations.operationCodes.ERR, + metaInfo: { message: 'Id is required.' }, + }; + } + + const user = userDatabase.findById(id); + return { + operationCode: user === null ? UserOperations.operationCodes.ERR : UserOperations.operationCodes.OK, + metaInfo: user === null ? { message: 'User not found.' } : { message: user } + } + } + + public static handleGetUserByEmail(parsedMessage: ParsedMessage) : ParsedMessage{ + const { email } = parsedMessage.metaInfo || {}; + if(!email){ + return { + operationCode: UserOperations.operationCodes.ERR, + metaInfo: { message: 'Email is required.' }, + }; + } + + const user = userDatabase.findByEmail(email); + const response = { + operationCode: user === null ? UserOperations.operationCodes.ERR : UserOperations.operationCodes.OK, + metaInfo: user === null ? { message: 'User not found.' } : user + } + + console.log(response); + return response; + } + + // Modify an existing user + public static handleModifyUser(parsedMessage: ParsedMessage): ParsedMessage { + const { id, name, email, password, departmentId, app_type } = parsedMessage.metaInfo || {}; + + if (!id || !name || !email || !password || !departmentId || !app_type) { + return { + operationCode: UserOperations.operationCodes.ERR, + metaInfo: { message: 'Some information are missing.' }, + }; + } + + const user = userDatabase.findById(id); + if (!user) return { + operationCode: UserOperations.operationCodes.ERR, + metaInfo: { message: 'User not found in the system.' }, + }; + + const listOfUsers = userDatabase.getAllUsers() + + // Check if another user already exists with the same email or name (excluding the current user) + const existingUserByEmail = listOfUsers.find((existingUser) => existingUser.email === email && existingUser.id !== id); + const existingUserByName = listOfUsers.find((existingUser) => existingUser.name === name && existingUser.id !== id); + + if (existingUserByEmail) { + return { + operationCode: UserOperations.operationCodes.ERR, + metaInfo: { message: `Email ${email} is already in use by another user.` }, + }; + } + + if (existingUserByName) { + return { + operationCode: UserOperations.operationCodes.ERR, + metaInfo: { message: `Name ${name} is already in use by another user.` }, + }; + } + + const isSuccess = userDatabase.modifyUser(id, name, email, password, departmentId, app_type); + return { + operationCode: isSuccess ? UserOperations.operationCodes.OK : UserOperations.operationCodes.ERR, + metaInfo: { message: isSuccess ? 'User modified successfully.' : 'Failed to modify user.' }, + }; + } + + // Delete a user by ID + public static handleDeleteUser(parsedMessage: ParsedMessage): ParsedMessage { + const { id } = parsedMessage.metaInfo || {}; + + if (!id) { + return { + operationCode: UserOperations.operationCodes.ERR, + metaInfo: { message: 'User ID is required for deletion.' }, + }; + } + + const isSuccess = userDatabase.deleteUser(id); + return { + operationCode: isSuccess ? UserOperations.operationCodes.OK : UserOperations.operationCodes.ERR, + metaInfo: { message: isSuccess ? 'User deleted successfully.' : 'Failed to delete user.' }, + }; + } + + // Register user operations with the OperationHandler + public register(operationHandler: OperationHandler): void { + operationHandler.registerHandler(UserOperations.operationCodes.GET_USERS, UserOperations.handleGetUsers); + operationHandler.registerHandler(UserOperations.operationCodes.MODIFY_USER, UserOperations.handleModifyUser); + operationHandler.registerHandler(UserOperations.operationCodes.DELETE_USER, UserOperations.handleDeleteUser); + operationHandler.registerHandler(UserOperations.operationCodes.FIND_BY_ID, UserOperations.handleGetUserById); + operationHandler.registerHandler(UserOperations.operationCodes.FIND_BY_EMAIL, UserOperations.handleGetUserByEmail); + + // Register common operations inherited from OperationBase + OperationBase.registerCommonOperations(operationHandler); + } +} diff --git a/UC/src/network/socket_communicator/socket_communicator_base.ts b/UC/src/network/socket_communicator/socket_communicator_base.ts new file mode 100644 index 0000000..cbe8e7d --- /dev/null +++ b/UC/src/network/socket_communicator/socket_communicator_base.ts @@ -0,0 +1,21 @@ +import { ParsedMessage } from '../message_handler'; +import { OperationHandler } from '../operations_base/operation_handler'; + +export abstract class SocketCommunicatorBase { + protected readonly ip: string; + protected readonly port: number; + protected readonly operationHandler: OperationHandler; + protected handlerResult: ParsedMessage | null; + + protected constructor(ip: string, port: number, operationHandler: OperationHandler) { + this.ip = ip; + this.port = port; + this.operationHandler = operationHandler + this.handlerResult = null; + } + + // Getter for the handler result + getHandlerResult(): ParsedMessage | null { + return this.handlerResult; + } +} diff --git a/UC/src/network/socket_communicator/tcp_server_communicator.ts b/UC/src/network/socket_communicator/tcp_server_communicator.ts new file mode 100644 index 0000000..8f57dba --- /dev/null +++ b/UC/src/network/socket_communicator/tcp_server_communicator.ts @@ -0,0 +1,161 @@ +import { Socket } from 'net'; +import { privateEncrypt, privateDecrypt, generateKeyPairSync, createCipheriv, createDecipheriv, randomBytes } from 'crypto'; +import { MessageHandler, ParsedMessage } from '../message_handler'; +import { SocketCommunicatorBase } from './socket_communicator_base'; +import { OperationHandler } from '../operations_base/operation_handler'; +import {constants} from "node:crypto"; + +const END_OF_MESSAGE = ''; // Define a unique marker for end of message + +export class TcpServerCommunicator extends SocketCommunicatorBase { + private readonly socket: Socket; + private privateKey: string | null; + private publicKey: string | null; + private aesKey: Buffer | null; + private aesIv: Buffer | null; + private messageBuffer: string; + + constructor(socket: Socket, ip: string, port: number, operationHandler: OperationHandler) { + super(ip, port, operationHandler); + this.socket = socket; + this.privateKey = null; + this.publicKey = null; // Client public key will be set later + this.aesKey = null; + this.aesIv = null; + this.messageBuffer = ''; // Initialize the message buffer + this.generateKeyPair(); // Generate RSA key pair for encryption + } + + // Generate RSA key pair (public and private keys) + generateKeyPair(): void { + const { privateKey, publicKey } = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + this.privateKey = privateKey; + this.publicKey = publicKey; + console.log('RSA key pair generated.'); + } + + // Send the server's public key to the client + async sendPublicKey(): Promise { + if (!this.publicKey) { + throw new Error('Public key is not available. Please generate RSA key pair.'); + } + + const message = MessageHandler.formatMessage('SET_PUBLIC_KEY', { publicKey: this.publicKey }); + await this.writeToSocket(message + END_OF_MESSAGE); + console.log('Public key sent to client.'); + } + + // Generate AES key and IV, then send them to the client + async sendAesKey(): Promise { + this.aesKey = randomBytes(32); // 256-bit AES key + this.aesIv = randomBytes(16); // AES IV + + const aesKeyBase64 = this.aesKey.toString('base64'); + const aesIvBase64 = this.aesIv.toString('base64'); + + const formattedMessage = MessageHandler.formatMessage('SET_AES_KEY', { aesKey: aesKeyBase64, aesIv: aesIvBase64 }); + const encryptedMessage = this.encryptWithRsa(Buffer.from(formattedMessage)).toString('base64'); + + await this.writeToSocket(encryptedMessage + END_OF_MESSAGE); + console.log('AES key and IV sent to client.'); + } + + // Encrypt a message with the server's private key (RSA encryption) + private encryptWithRsa(message: Buffer): Buffer { + const bufferMessage = Buffer.isBuffer(message) ? message : Buffer.from(message); + if (!this.privateKey) throw new Error('Server private key not set.'); + + return privateEncrypt( + { + key: this.privateKey, + padding: constants.RSA_PKCS1_PADDING, // PKCS1 padding + }, + bufferMessage + ); + } + + // Decrypt AES-encrypted messages + private decryptWithAes(encryptedMessage: string): string { + if (!this.aesKey || !this.aesIv) { + throw new Error('AES key not set.'); + } + + const decipher = createDecipheriv('aes-256-cbc', this.aesKey, this.aesIv); + let decrypted = decipher.update(Buffer.from(encryptedMessage, 'base64')); + decrypted = Buffer.concat([decrypted, decipher.final()]); + return decrypted.toString('utf-8'); + } + + // Encrypt a message with AES + private encryptWithAes(message: string): string { + if (!this.aesKey || !this.aesIv) { + throw new Error('AES key or IV is not set.'); + } + const cipher = createCipheriv('aes-256-cbc', this.aesKey, this.aesIv); + let encrypted = cipher.update(message, 'utf-8'); + encrypted = Buffer.concat([encrypted, cipher.final()]); + return encrypted.toString('base64'); + } + + // Handle incoming chunks of data + async handleIncomingChunk(data: Buffer): Promise { + const incomingMessage = data.toString(); + this.messageBuffer += incomingMessage; + + // Check if the message ends with END_OF_MESSAGE + if (this.messageBuffer.endsWith(END_OF_MESSAGE)) { + // Remove the END_OF_MESSAGE marker and process the message + const completeMessage = this.messageBuffer.slice(0, -END_OF_MESSAGE.length); + + console.log(`\n\nComplete Message:\n${completeMessage}\n\n`); + + this.handleIncomingMessage(completeMessage); + + // Clear the message buffer after processing + this.messageBuffer = ''; + } + } + + // Send chunked message + async sendChunkedMessage(operationCode: string, metaInfo?: { [key: string]: any }, fileContent?: Buffer): Promise { + const message = MessageHandler.formatMessage(operationCode, metaInfo, fileContent); + let outgoingMessage: string; + + if (this.aesKey && this.aesIv) { + outgoingMessage = this.encryptWithAes(message); + } else { + outgoingMessage = message; + } + + outgoingMessage += END_OF_MESSAGE; // Append end-of-message marker + await this.writeToSocket(outgoingMessage); + } + + // Handle incoming message (decrypt with AES if available) + handleIncomingMessage(incomingMessage: string): void { + let messageToProcess = incomingMessage; + + if (this.aesKey && this.aesIv) { + messageToProcess = this.decryptWithAes(incomingMessage); + } + + this.handlerResult = this.operationHandler.handleOperation(messageToProcess); + } + + // Write message to socket + private writeToSocket(message: string): Promise { + return new Promise((resolve, reject) => { + this.socket.write(message, (err: any) => { + if (err) { + console.error('Error sending message over TCP:', err); + return reject(err); + } + resolve(); + }); + }); + } +} diff --git a/UC/src/network/socket_communicator/udp_socket_communicator.ts b/UC/src/network/socket_communicator/udp_socket_communicator.ts new file mode 100644 index 0000000..82b2b50 --- /dev/null +++ b/UC/src/network/socket_communicator/udp_socket_communicator.ts @@ -0,0 +1,39 @@ +import { Socket as UdpSocket } from 'dgram'; +import { MessageHandler } from '../message_handler'; +import { SocketCommunicatorBase } from './socket_communicator_base'; +import {OperationHandler} from "../operations_base/operation_handler"; + +export class UdpSocketCommunicator extends SocketCommunicatorBase { + private readonly socket: UdpSocket; + + constructor(socket: UdpSocket, ip: string, port: number, operationHandler: OperationHandler) { + super(ip, port, operationHandler); + this.socket = socket; + } + + // Handle incoming message (no decryption needed for UDP) + handleIncomingMessage(incomingMessage: string): void { + this.handlerResult = this.operationHandler.handleOperation(incomingMessage); + } + + // Send plain message over UDP (metaInfo as JSON and fileContent as Buffer) + async sendMessage(operationCode: string, metaInfo?: any, fileContent?: Buffer): Promise { + const message = MessageHandler.formatMessage(operationCode, metaInfo, fileContent); + + await this.sendUdpMessage(message); + } + + // Helper method to wrap socket.send in a Promise for async/await support + private sendUdpMessage(message: string): Promise { + return new Promise((resolve, reject) => { + this.socket.send(message, this.port, this.ip, (err: any) => { + if (err) { + console.error('Error sending UDP message:', err); + return reject(err); + } + console.log(`Plain message sent to ${this.ip}:${this.port} (UDP)`); + resolve(); + }); + }); + } +} diff --git a/UC/src/tcp_server.ts b/UC/src/tcp_server.ts new file mode 100644 index 0000000..a67bc75 --- /dev/null +++ b/UC/src/tcp_server.ts @@ -0,0 +1,121 @@ +import net, { Socket } from 'net'; +import path from 'path'; +import dotenv from 'dotenv'; + +import { ConnectionManager } from './network/connection_manager'; +import {OperationHandler} from "./network/operations_base/operation_handler"; +import {GeneralOperations} from "./network/operations_custom/general_operations"; +import {TcpServerCommunicator} from "./network/socket_communicator/tcp_server_communicator"; +import {CeoOperations} from "./network/operations_custom/ceo_operations"; +import {AuthOperations} from "./network/operations_custom/auth_operations"; +import {DepartmentOperations} from "./network/operations_custom/department_operations"; +import {KeyOperations} from "./network/operations_custom/key_operations"; +import {UserOperations} from "./network/operations_custom/user_operations"; + +dotenv.config({ path: path.resolve(__dirname, './config/.env') }); + +export class TcpServer { + private readonly connectionManager: ConnectionManager; + private readonly operationHandler: OperationHandler; + private readonly port: number; + private readonly host: string; + + constructor(host: string, port: number) { + this.connectionManager = new ConnectionManager(); + this.operationHandler = OperationHandler.getInstance(); + this.host = host; + this.port = port; + + this.operationHandler.loadPlugin(new GeneralOperations()); + this.operationHandler.loadPlugin(new CeoOperations()); + this.operationHandler.loadPlugin(new AuthOperations()); + this.operationHandler.loadPlugin(new DepartmentOperations()); + this.operationHandler.loadPlugin(new KeyOperations()); + this.operationHandler.loadPlugin(new UserOperations()); + + } + + // Start the TCP server + public start(): void { + const tcpServer = net.createServer(); + + // Handle incoming connections + tcpServer.on('connection', (socket: Socket) => { + const ip = socket.remoteAddress || 'unknown'; + const port = socket.remotePort || 0; + + const clientId = `${ip}:${port}`; // Use IP and port to identify the client + console.log(`Client connected: ${clientId}`); + + const tcpCommunicator = new TcpServerCommunicator(socket, ip, port, this.operationHandler); + this.connectionManager.addConnection(ip, port, tcpCommunicator); + + // Generate RSA key pair and start key exchange + tcpCommunicator.generateKeyPair(); + tcpCommunicator.sendPublicKey() + .then(() => tcpCommunicator.sendAesKey()) + .then(() => console.log('Public key and AES key sent successfully.')) + .catch(err => { + console.error(`Error during key exchange with client ${clientId}:`, err); + socket.end(); // Close the connection in case of any error + }); + + // Handle incoming data in chunks + socket.on('data', async (data: Buffer) => { + await this.handleData(data, ip, port); + }); + + // Handle client disconnect + socket.on('end', () => { + console.log(`Client disconnected: ${clientId}`); + this.connectionManager.removeCommunicator(ip, port); + }); + + // Handle socket errors + socket.on('error', (err: Error) => { + console.error(`Error from client ${clientId}: ${err.message}`); + this.connectionManager.removeCommunicator(ip, port); + }); + }); + + // Handle server errors + tcpServer.on('error', (err: Error) => { + console.error(`TCP server error: ${err.message}`); + }); + + // Start listening for connections + tcpServer.listen(this.port, this.host, () => { + console.log(`TCP server listening on ${this.host}:${this.port}`); + }); + } + + // Handle incoming data from a client + private async handleData(data: Buffer, ip: string, port: number): Promise { + const clientId = `${ip}:${port}`; + + // Retrieve the communicator associated with this connection + const communicator = this.connectionManager.getCommunicator(ip, port) as TcpServerCommunicator; + if (!communicator) { + console.error(`No communicator found for ${clientId}`); + return; + } + + // Handle incoming chunked message via communicator + await communicator.handleIncomingChunk(data); + + // Fetch and process result if available + const handlerResult = communicator.getHandlerResult(); + if (handlerResult) { + try { + await communicator.sendChunkedMessage( + handlerResult.operationCode, + handlerResult.metaInfo, + handlerResult.fileContent + ); + console.log(`Response sent to ${clientId}`); + } catch (err) { + console.error(`Failed to send response to ${clientId}:`, err); + } + } + } +} diff --git a/UC/src/udp_server.ts b/UC/src/udp_server.ts new file mode 100644 index 0000000..87e32e9 --- /dev/null +++ b/UC/src/udp_server.ts @@ -0,0 +1,67 @@ +import dgram, { RemoteInfo } from 'dgram'; +import path from 'path'; +import dotenv from 'dotenv'; +import { UdpSocketCommunicator } from "./network/socket_communicator/udp_socket_communicator"; +import { OperationHandler } from "./network/operations_base/operation_handler"; +import { GeneralOperations } from "./network/operations_custom/general_operations"; + +export class UdpServer { + private readonly udpServer: dgram.Socket; + private readonly operationHandler: OperationHandler; + private readonly host: string; + private readonly port: number + + constructor(host: string, port: number) { + this.udpServer = dgram.createSocket('udp4'); + this.operationHandler = OperationHandler.getInstance(); + + this.host = host; + this.port = port; + + // Load only the GeneralOperations into the operation handler + this.operationHandler.loadPlugin(new GeneralOperations()); + } + + // Start the UDP server + public start(): void { + this.udpServer.on('message', this.handleUdpMessages.bind(this)); + this.udpServer.on('error', this.handleError); + this.udpServer.on('listening', this.handleListening.bind(this)); + + // Bind the server to the UDP port and host + this.udpServer.bind(this.port, this.host); + } + + // Handle incoming UDP messages + private async handleUdpMessages(msg: Buffer, rinfo: RemoteInfo): Promise { + const ip = rinfo.address; + const port = rinfo.port; + + console.log(`Received message from ${ip}:${port}`); + + // Create a temporary communicator for the incoming message + const communicator = new UdpSocketCommunicator(this.udpServer, ip, port, this.operationHandler); + + // Process the incoming message using the communicator + communicator.handleIncomingMessage(msg.toString()); + + const communicatorResult = communicator.getHandlerResult(); + if (communicatorResult) { + // Send response back to the client using the temporary communicator + await communicator.sendMessage(communicatorResult.operationCode, communicatorResult.metaInfo); + } else { + console.error(`No handler result for ${ip}:${port}`); + } + } + + // Handle UDP server errors + private handleError(err: Error): void { + console.error(`UDP server error:\n${err.stack}`); + } + + // Handle when the UDP server starts listening + private handleListening(): void { + const address = this.udpServer.address(); + console.log(`UDP server listening on ${address.address}:${address.port}`); + } +} diff --git a/UC/tsconfig.json b/UC/tsconfig.json new file mode 100644 index 0000000..56a8ab8 --- /dev/null +++ b/UC/tsconfig.json @@ -0,0 +1,110 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/User/.env b/User/.env new file mode 100644 index 0000000..c411426 --- /dev/null +++ b/User/.env @@ -0,0 +1,4 @@ +UDP_PORT=41234 +TCP_PORT=41233 + +HOST=0.0.0.0 \ No newline at end of file diff --git a/User/.idea/.name b/User/.idea/.name index 12fdd11..0be4d80 100644 --- a/User/.idea/.name +++ b/User/.idea/.name @@ -1 +1 @@ -sending_file_confirmation.js \ No newline at end of file +login.html \ No newline at end of file diff --git a/User/backupSchemes.json b/User/backupSchemes.json deleted file mode 100644 index 43e145d..0000000 --- a/User/backupSchemes.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "7c0ef9d5-23ed-47a6-bfb4-fdcfce436904": { - "ip": "192.168.169.127", - "directoryStructure": "{\"backup_dir\":{\"files\":[],\"fisiere\":{\"files\":[\"New Microsoft Excel Worksheet.xlsx\",\"New Microsoft Word Document.docx\"]}}}", - "totalSize": 19363 - }, - "59c712d9-e6dd-438f-aec9-31ca2ae750e6": { - "ip": "192.168.169.19", - "directoryStructure": "{\"backup_dir\":{\"files\":[],\"fisiere\":{\"files\":[\"New Microsoft Excel Worksheet.xlsx\",\"New Microsoft Word Document.docx\"]}}}", - "totalSize": 19363 - } -} \ No newline at end of file diff --git a/User/dirBackup.json b/User/dirBackup.json deleted file mode 100644 index 3b51572..0000000 --- a/User/dirBackup.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "path": "/home/andrei/Desktop/proiect-licenta/dirs/user/backup_dir", - "structure": { - "backup_dir": { - "files": [], - "fisiere": { - "files": [ - "New Microsoft Excel Worksheet.xlsx", - "New Microsoft Word Document.docx" - ] - } - } - }, - "size": 19363 -} \ No newline at end of file diff --git a/User/dirDepartment.json b/User/dirDepartment.json deleted file mode 100644 index bb4392f..0000000 --- a/User/dirDepartment.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "path": "/home/andrei/Desktop/proiect-licenta/dirs/user/department_dir" -} \ No newline at end of file diff --git a/User/dirShare.json b/User/dirShare.json deleted file mode 100644 index df67c5e..0000000 --- a/User/dirShare.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "path": "/home/andrei/Desktop/proiect-licenta/dirs/user/share_dir" -} \ No newline at end of file diff --git a/User/helpers/http_status.js b/User/helpers/http_status.js deleted file mode 100644 index b03796e..0000000 --- a/User/helpers/http_status.js +++ /dev/null @@ -1,36 +0,0 @@ -const httpStatus = { - // Informational - CONTINUE: 100, - SWITCHING_PROTOCOLS: 101, - PROCESSING: 102, - - // Success - OK: 200, - CREATED: 201, - ACCEPTED: 202, - NO_CONTENT: 204, - - // Redirection - MOVED_PERMANENTLY: 301, - FOUND: 302, - SEE_OTHER: 303, - NOT_MODIFIED: 304, - TEMPORARY_REDIRECT: 307, - - // Client Error - BAD_REQUEST: 400, - UNAUTHORIZED: 401, - FORBIDDEN: 403, - NOT_FOUND: 404, - METHOD_NOT_ALLOWED: 405, - CONFLICT: 409, - GONE: 410, - UNSUPPORTED_MEDIA_TYPE: 415, - - // Server Error - INTERNAL_SERVER_ERROR: 500, - NOT_IMPLEMENTED: 501, - SERVICE_UNAVAILABLE: 503 -}; - -module.exports = { httpStatus }; diff --git a/User/helpers/lock_mechanism.js b/User/helpers/lock_mechanism.js deleted file mode 100644 index 2d55fc5..0000000 --- a/User/helpers/lock_mechanism.js +++ /dev/null @@ -1,33 +0,0 @@ -const lockfile = require('proper-lockfile'); - -async function lockFile(filePath) { - try { - await lockfile.lock(filePath, { - realpath: false, - retries: { - retries: 10, // Number of retries - factor: 2, // The exponential factor - minTimeout: 1000, // The number of milliseconds before starting the first retry - maxTimeout: 5000, // The maximum number of milliseconds between two retries - randomize: true, // Randomizes the timeouts by multiplying with a factor between 1 to 2 - } - }); - console.log(`File locked: ${filePath}`); - } catch (error) { - console.error(`Error locking file ${filePath}: ${error.message}`); - throw error; // Propagate the error if unable to lock after retries - } -} - -async function unlockFile(filePath) { - try { - await lockfile.unlock(filePath); - console.log(`File unlocked: ${filePath}`); - } catch (error) { - console.error(`Error unlocking file ${filePath}: ${error.message}`); - // Decide whether to throw the error or not, based on your error handling strategy - throw error; - } -} - -module.exports = {lockFile, unlockFile} \ No newline at end of file diff --git a/User/ipConfig.json b/User/ipConfig.json deleted file mode 100644 index 51f378a..0000000 --- a/User/ipConfig.json +++ /dev/null @@ -1 +0,0 @@ -);'F w-`e/dK \ No newline at end of file diff --git a/User/iv.key b/User/iv.key deleted file mode 100644 index 0d7c548..0000000 --- a/User/iv.key +++ /dev/null @@ -1 +0,0 @@ -i60@SLi \ No newline at end of file diff --git a/User/jobs/backup.js b/User/jobs/backup.js deleted file mode 100644 index f206d9e..0000000 --- a/User/jobs/backup.js +++ /dev/null @@ -1,365 +0,0 @@ -const fsPromises = require('fs').promises; // Ensure you use the promise-based API -const fs = require('fs'); -const path = require('path'); -const { decryptFileInPlace, encryptFileInPlace, encryptFileWithKey, decryptFileWithKey} = require("../src/main/aes_encrypt"); -const {lockFile, unlockFile} = require("../helpers/lock_mechanism"); - -async function decryptAndGetServerIP(filePath) { - try { - await lockFile(filePath); - await decryptFileInPlace(filePath); - - const fileContent = await fs.promises.readFile(filePath, 'utf8'); - const jsonData = JSON.parse(fileContent); - const serverIP = jsonData.ip; - - await encryptFileInPlace(filePath) - await unlockFile(filePath); - return serverIP; - } catch (error) { - console.error('An error occurred:', error); - return null; - } -} - -function extractFilePaths(directoryStructure) { - let paths = []; - const traverse = (dir, currentPath) => { - Object.keys(dir).forEach(key => { - if (key === 'files') { - dir[key].forEach(file => paths.push(path.join(currentPath, file))); - } else { - traverse(dir[key], path.join(currentPath, key)); - } - }); - }; - traverse(directoryStructure, ''); - return paths; -} - -async function fetchFiles(ip, filePaths) { - let results = {}; - - for (const filePath of filePaths) { - const url = `http://${ip}:3000/file_path`; - try { - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ filePath }) - }); - - if (!response.ok) throw new Error(`HTTP status ${response.status}`); - - const arrayBuffer = await response.arrayBuffer(); // Fetch the response as an ArrayBuffer - results[filePath] = Buffer.from(arrayBuffer); - } catch (error) { - console.error(`Error fetching ${filePath}:`, error); - results[filePath] = null; - } - } - - return results; -} - -async function removeDirectory(directoryPath) { - try { - // Check if the directory exists - const stats = await fsPromises.stat(directoryPath); - if (!stats.isDirectory()) { - console.log('The specified path is not a directory.'); - return; - } - - // Read all the contents of the directory - const files = await fsPromises.readdir(directoryPath); - - // Loop through each file/directory and delete them - for (const file of files) { - const currentPath = path.join(directoryPath, file); - const currentStats = await fsPromises.stat(currentPath); - - if (currentStats.isDirectory()) { - // Recursive call for directories - await removeDirectory(currentPath); - } else { - await lockFile(currentPath); - await unlockFile(currentPath); - await fsPromises.unlink(currentPath); - } - } - - // Finally, delete the directory itself - await fsPromises.rmdir(directoryPath); - //console.log(`Directory removed: ${directoryPath}`); - } catch (error) { - console.error(`Error removing directory: ${error.message}`); - } -} - -async function decryptUserSystemConfig() { - const configPath = path.join(__dirname, '..', 'usersInSystem.json'); - await lockFile(configPath); - await decryptFileInPlace(configPath); -} - -async function encryptUserSystemConfig(){ - const configPath = path.join(__dirname, '..', 'usersInSystem.json'); - await encryptFileInPlace(configPath); - await unlockFile(configPath); -} - -async function processBackup() { - const usersConfigPath = path.join(__dirname, '..', 'usersInSystem.json'); - const backupConfigPath = path.join(__dirname, '..', 'backupSchemes.json'); - const baseBackupDirPath = path.join(__dirname, '..', 'backup_directories'); - if(!fs.existsSync(backupConfigPath) || !fs.existsSync(usersConfigPath)){ - console.log('Can\'t start backup system: necessary files missing!'); - return; - } - - await decryptUserSystemConfig(); - - try { - // Read and parse the users configuration - const usersData = await fsPromises.readFile(usersConfigPath, 'utf8'); - const usersConfig = JSON.parse(usersData); - - // Read and parse the backup configuration - const backupData = await fsPromises.readFile(backupConfigPath, 'utf8'); - const jsonData = JSON.parse(backupData); - - for (const key in jsonData) { - const userBackupDir = path.join(baseBackupDirPath, key); - await removeDirectory(userBackupDir); - - const node = jsonData[key]; - const directoryStructure = JSON.parse(node.directoryStructure); - const filePaths = extractFilePaths(directoryStructure); - const files = await fetchFiles(node.ip, filePaths); - - await fsPromises.mkdir(userBackupDir, { recursive: true }); - - // Find the encryption key for the user's department - let encryptionKey = ''; - for (const dept in usersConfig) { - if (usersConfig[dept].users.includes(key)) { - encryptionKey = usersConfig[dept].key; - break; - } - } - - for (const file in files) { - if (files[file]) { - const fullPath = path.join(userBackupDir, file); - await fsPromises.mkdir(path.dirname(fullPath), { recursive: true }); - await fsPromises.writeFile(fullPath, files[file]); // Write the binary data - - // Encrypt the file in place after writing it - if (encryptionKey) { - await encryptFileWithKey(fullPath, encryptionKey); - } - } - } - } - } catch (error) { - console.error('Failed to read or process the JSON file:', error); - } - - await encryptUserSystemConfig(); -} - -async function getAllFilePaths(dirPath) { - let filePaths = []; - async function recurse(currentPath) { - const entries = await fsPromises.readdir(currentPath, { withFileTypes: true }); - // Create promises for each entry and process them in parallel - const entryPromises = entries.map(async (entry) => { - const resolvedPath = path.join(currentPath, entry.name); - if (entry.isDirectory()) { - // If it's a directory, recurse into it - await recurse(resolvedPath); - } else { - // If it's a file, add it to the file paths array - filePaths.push(resolvedPath); - } - }); - // Wait for all promises to complete - await Promise.all(entryPromises); - } - await recurse(dirPath); - return filePaths; -} - -async function decryptBackupFilesToDirectory(destinationDir) { - await decryptUserSystemConfig(); - const usersConfigPath = path.join(__dirname, '..', 'usersInSystem.json'); - - if(!fs.existsSync(usersConfigPath)){ - return false; - } - - const usersConfig = JSON.parse(await fsPromises.readFile(usersConfigPath, 'utf8')); - const sourceDir = path.join(__dirname, '..', 'backup_directories'); - - await removeDirectory(destinationDir); - - try { - for (const department in usersConfig) { - const { key, users } = usersConfig[department]; - for (const userId of users) { - const userDir = path.join(sourceDir, userId); - try { - const files = await getAllFilePaths(userDir); - for (const filePath of files) { - const stats = await fsPromises.stat(filePath); - if (stats.isFile()) { - const relativePath = path.relative(userDir, filePath); - - // Construct the destination file path - const destinationFilePath = path.join(destinationDir, userId, relativePath); - await fsPromises.mkdir(path.dirname(destinationFilePath), { recursive: true }); - await lockFile(filePath); - - // Decrypt the file in its original location - await decryptFileWithKey(filePath, key); - - // Copy the decrypted file to the destination directory - await fsPromises.copyFile(filePath, destinationFilePath); - console.log(`Decrypted file copied to: ${destinationFilePath}`); - - // Re-encrypt the file in its original location (optional) - await encryptFileWithKey(filePath, key); - await unlockFile(filePath); - } - } - } catch (error) { - console.error(`Error processing files for user ${userId}: ${error.message}`); - await encryptUserSystemConfig(); - return false; - } - } - } - } catch (error) { - console.error(`Error processing decryption: ${error.message}`); - await encryptUserSystemConfig(); - return false; - } - - await encryptUserSystemConfig(); - return true; -} - -async function decryptDepartmentFilesToDirectory() { - const dirDepartmentPath = path.join(__dirname, '..', 'dirDepartment.json'); - const loginDataPath = path.join(__dirname, '..', 'loginData.json'); - const usersConfigPath = path.join(__dirname, '..', 'usersInSystem.json'); - const sourceDir = path.join(__dirname, '..', 'backup_directories'); - - if (!fs.existsSync(usersConfigPath) || !fs.existsSync(loginDataPath) || !fs.existsSync(dirDepartmentPath)) { - console.error("Missing one or more essential configuration files."); - return false; - } - - await lockFile(loginDataPath); - await lockFile(usersConfigPath); - await lockFile(dirDepartmentPath); - - await decryptFileInPlace(loginDataPath); - await decryptFileInPlace(usersConfigPath); - - const { path: destinationDir } = JSON.parse(await fsPromises.readFile(dirDepartmentPath, 'utf8')); - const { department } = JSON.parse(await fsPromises.readFile(loginDataPath, 'utf8')); - const usersConfig = JSON.parse(await fsPromises.readFile(usersConfigPath, 'utf8')); - - // Check if department exists in users configuration - if (!usersConfig[department]) { - console.error(`No configuration found for department: ${department}`); - return false; - } - - const { key, users } = usersConfig[department]; - - await encryptFileInPlace(loginDataPath); - await encryptFileInPlace(usersConfigPath); - - await unlockFile(loginDataPath); - await unlockFile(usersConfigPath); - await unlockFile(dirDepartmentPath); - - for (const userId of users) { - const userDir = path.join(sourceDir, userId); - - try { - const files = await getAllFilePaths(userDir); - console.log(`Processing files for user ${userId}: ${files}`); - - for (const filePath of files) { - const stats = await fsPromises.stat(filePath); - if (stats.isFile()) { - const relativePath = path.relative(userDir, filePath); - - // Construct the destination file path - const destinationFilePath = path.join(destinationDir, userId, relativePath); - await fsPromises.mkdir(path.dirname(destinationFilePath), { recursive: true }); - - // Decrypt the file in its original location - await decryptFileWithKey(filePath, key); - - // Copy the decrypted file to the destination directory - await fsPromises.copyFile(filePath, destinationFilePath); - console.log(`Decrypted file copied to: ${destinationFilePath}`); - - // Re-encrypt the file in its original location (optional) - await encryptFileWithKey(filePath, key); - } - } - } catch (error) { - console.error(`Error processing files for user ${userId}: ${error.message}`); - return false; - } - } - - console.log("All files processed successfully."); - return true; -} - -let serverIp = null; -decryptAndGetServerIP(path.join(__dirname, '..', 'ipConfig.json')).then(ip => { - serverIp = ip; -}); - -const timeoutInterval = 1 * 10 * 1000 //minutes * seconds * miliseconds - -setInterval(async () => { - await decryptDepartmentFilesToDirectory(); -}, timeoutInterval); - -console.log('backup.js process started!'); -process.on('SIGINT', async () => { - console.log('Shutdown signal received in \'backup.js\'. Cleaning up...'); - - try { - console.log('Performing cleanup tasks...'); - console.log('Cleanup completed successfully.'); - } catch (error) { - console.error('An error occurred during cleanup:', error); - } finally { - console.log('Process terminated'); - process.exit(0); - } -}); - -process.on('message', async (message) => { - if (message.type === 'startBackup') { - console.log('BackupProcess: Received command to start backup.'); - await processBackup() - } - if(message.type === 'decryptBackup'){ - console.log(`BackupProcess: Received command to decrypt backup to ${message.decryptDestPath}`); - console.log(message.decryptDestPath); - await decryptBackupFilesToDirectory(message.decryptDestPath); - } -}); \ No newline at end of file diff --git a/User/jobs/clean.js b/User/jobs/clean.js deleted file mode 100644 index 236f48a..0000000 --- a/User/jobs/clean.js +++ /dev/null @@ -1,28 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -// List of files you want to check and delete -const filesToDelete = [ - path.join(__dirname, '..', 'backupSchemes.json'), - path.join(__dirname, '..', 'dirBackup.json'), - path.join(__dirname, '..', 'dirShare.json'), - path.join(__dirname, '..', 'usersInSystem.json'), - path.join(__dirname, '..', 'loginData.json'), - path.join(__dirname, '..', 'ipConfig.json') -]; - -filesToDelete.forEach(file => { - fs.access(file, fs.constants.F_OK, (err) => { - if (!err) { - fs.unlink(file, err => { - if (err) { - console.error(`Failed to delete ${file}: ${err}`); - } else { - console.log(`${file} was deleted successfully.`); - } - }); - } else { - console.log(`${file} does not exist.`); - } - }); -}); diff --git a/User/jobs/external_endpoints.js b/User/jobs/external_endpoints.js deleted file mode 100644 index 73d3dc4..0000000 --- a/User/jobs/external_endpoints.js +++ /dev/null @@ -1,192 +0,0 @@ -const express = require('express'); -const fs = require('fs'); -const fsExtra = require('fs-extra'); -const path = require('path'); -const Joi = require('joi'); -const { httpStatus } = require('../helpers/http_status'); -const {lockFile, unlockFile} = require("../helpers/lock_mechanism"); -const {decryptFileInPlace, encryptFileInPlace} = require("../src/main/aes_encrypt"); - -const app = express(); -app.use(express.json()); - - -const server = require('http').createServer(app); -const connections = []; - -// Store active connections to close them on shutdown -server.on('connection', (conn) => { - connections.push(conn); - conn.on('close', () => { - connections.splice(connections.indexOf(conn), 1); - }); -}); - -const filePathSchema = Joi.object({ - filePath: Joi.string().required() -}); - -async function getBaseDirectory() { - const filePath = path.join(__dirname, '..', 'dirBackup.json'); - try { - const data = await fs.promises.readFile(filePath, 'utf8'); - const jsonData = JSON.parse(data); - return jsonData.path; - } catch (error) { - console.error('Error reading the base directory:', error); - throw error; - } -} - -app.post('/file_path', async (req, res) => { - const { error, value } = filePathSchema.validate(req.body); - if (error) { - return res.status(httpStatus.BAD_REQUEST).json({ - message: 'Invalid input', - data: error.details - }); - } - - try { - const basePath = await getBaseDirectory(); - let { filePath } = value; - const baseDirName = path.basename(basePath); - if (filePath.startsWith(baseDirName + path.sep)) { - filePath = filePath.slice(baseDirName.length + 1); - } - const fullPath = path.resolve(basePath, filePath); - - fs.stat(fullPath, (err, stats) => { - if (err) { - if (err.code === 'ENOENT') { - return res.status(httpStatus.NOT_FOUND).json({ - message: 'File not found' - }); - } - return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ - message: 'Error accessing the file' - }); - } - - if (!stats.isFile()) { - return res.status(httpStatus.BAD_REQUEST).json({ - message: 'Path is not a file' - }); - } - - // Stream the file as binary data - res.writeHead(httpStatus.OK, { - 'Content-Type': 'application/octet-stream', - 'Content-Disposition': `attachment; filename="${path.basename(fullPath)}"` - }); - const fileStream = fs.createReadStream(fullPath); - fileStream.pipe(res); - }); - } catch (error) { - return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ - message: 'Error processing request' - }); - } -}); - - - -async function addFilePathToJson(filePath) { - const jsonFilePath = path.join(__dirname, '..', 'filesReceived.json'); - let data = { receivedFiles: [] }; - - try { - // Check if the JSON file exists - const fileExists = await fsExtra.pathExists(jsonFilePath); - - if (fileExists) { - await lockFile(jsonFilePath); - await decryptFileInPlace(jsonFilePath); - data = await fsExtra.readJson(jsonFilePath); - } else { - await fsExtra.writeJson(jsonFilePath, data, { spaces: 2 }); - await lockFile(jsonFilePath); - } - - if (data.receivedFiles.findIndex(existingFilePath => existingFilePath === filePath) === -1) { - data.receivedFiles.push(filePath); - } - - // Write the updated data back to the file - await fsExtra.writeJson(jsonFilePath, data, { spaces: 2 }); - // Unlock the file after updating - await unlockFile(jsonFilePath); - await encryptFileInPlace(jsonFilePath); - console.log('File path added successfully.'); - } catch (error) { - console.error('Error updating JSON file:', error); - await encryptFileInPlace(jsonFilePath); - await unlockFile(jsonFilePath); - } -} - -app.use('/share_file', express.raw({ - type: 'application/octet-stream', - limit: '50mb' -})); - -async function getShareDir(filePath) { - try { - await lockFile(filePath); - - const fileContent = await fs.promises.readFile(filePath, 'utf8'); - const jsonData = JSON.parse(fileContent); - const path = jsonData.path; - - await unlockFile(filePath); - return path; - } catch (error) { - console.error('An error occurred:', error); - return null; - } -} - - -app.post('/share_file', async (req, res) => { - // Extract metadata from headers - const idUser = req.headers['x-iduser']; - const nameOfFile = req.headers['x-nameoffile']; - const sizeOfFile = req.headers['x-sizeoffile']; - - // You might want to validate the metadata here - if (!idUser || !nameOfFile || !sizeOfFile) { - return res.status(httpStatus.BAD_REQUEST).json({message: 'Missing metadata headers'}); - } - - // Construct the file path using the metadata - const shareDirPath = await getShareDir(path.join(__dirname, '..', 'dirShare.json')); - const baseDir = path.join(shareDirPath, idUser); - fsExtra.ensureDirSync(baseDir); - - const filePath = path.join(baseDir, nameOfFile); - - try { - fs.writeFileSync(filePath, req.body); - await addFilePathToJson(filePath); - res.status(httpStatus.OK).json({ message: 'Upload successful.' }); - } catch (error) { - console.error('Error during file upload:', error); - res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ message: 'Internal server error.' }); - } -}); - - -console.log('external_endpoints.json started.'); -const PORT = process.env.PORT || 3000; -server.listen(PORT, () => console.log(`Server running on port ${PORT}`)); - -process.on('SIGINT', () => { - console.log('SIGINT signal received. Shutting down gracefully.'); - server.close(() => { - console.log('Server closed.'); - // Ensure all connections are closed - connections.forEach(conn => conn.end()); - setTimeout(() => connections.forEach(conn => conn.destroy()), 5000); - process.exit(0); - }); -}); diff --git a/User/jobs/fetcher.js b/User/jobs/fetcher.js deleted file mode 100644 index e6ef1d1..0000000 --- a/User/jobs/fetcher.js +++ /dev/null @@ -1,233 +0,0 @@ -const fsPromises = require('fs').promises; -const fs = require('fs'); -const path = require('path'); -const ip = require('ip'); -const {encryptFileInPlace, decryptFileInPlace} = require('../src/main/aes_encrypt'); -const {lockFile, unlockFile} = require("../helpers/lock_mechanism"); - -async function fetchBackupSchemes(serverIp) { - // Fetching backup schemes from the server - const depResponse = await fetch(`http://${serverIp}/backup_schemes`, { - method: 'GET', - headers: {'x-api-key': 'uc_api'} - }); - const backupSchemesJson = await depResponse.json(); - const backupSchemes = backupSchemesJson['data']; - - // Specify the path where the backup schemes will be saved - const destPath = path.join(__dirname, '..', 'backupSchemes.json'); - - // Write the fetched backup schemes to the specified file - try { - await lockFile(destPath); - await fsPromises.writeFile(destPath, JSON.stringify(backupSchemes, null, 2)); // Use null, 2 for pretty formatting - await unlockFile(destPath); - console.log('Backup schemes saved successfully to', destPath); - } catch (error) { - console.error('Failed to save backup schemes:', error); - } -} - -async function fetchUsersAndDepartments(serverIp) { - try { - console.log('se asteapta depart'); - // Fetch departments - const depResponse = await fetch(`http://${serverIp}/users/departments`, { - method: 'GET', - headers: {'x-api-key': 'uc_api'} - }); - - const departmentsJson = await depResponse.json(); - const departments = departmentsJson['data']; - - console.log('se asteapta users') - // Fetch users - const userResponse = await fetch(`http://${serverIp}/users`, { - method: 'GET', - headers: {'x-api-key': 'uc_api'} - }); - const usersJson = await userResponse.json(); - const users = usersJson['data']; - - // Combine the JSON data - const combinedData = Object.values(departments).reduce((acc, {name, key}) => { - acc[name] = { - key, - users: users.filter(user => user.department === name).map(user => user.id) - }; - return acc; - }, {}); - - // Write combined data to a file - const filePath = path.join(__dirname, '..', 'usersInSystem.json'); - if(!fs.existsSync(filePath)){ - await fsPromises.writeFile(filePath, ''); - } - await lockFile(filePath); - await fsPromises.writeFile(filePath, JSON.stringify(combinedData, null, 2), 'utf8'); - - await encryptFileInPlace(filePath); - await unlockFile(filePath); - console.log('Data has been encrypted and saved.'); - } catch (error) { - console.error('Failed to fetch or process data:', error); - } -} - -async function decryptAndGetServerIP(filePath) { - try { - await lockFile(filePath); - await decryptFileInPlace(filePath); - - const fileContent = await fsPromises.readFile(filePath, 'utf8'); - const jsonData = JSON.parse(fileContent); - const serverIP = jsonData.ip; - - await encryptFileInPlace(filePath) - await unlockFile(filePath); - return serverIP; - } catch (error) { - console.error('An error occurred:', error); - return null; - } -} - -async function getDirectoryStructure(dirPath) { - const baseName = path.basename(dirPath); - const entries = await fsPromises.readdir(dirPath, {withFileTypes: true}); - const result = {}; - result[baseName] = {files: []}; - let totalSize = 0; - - for (let entry of entries) { - const entryPath = path.join(dirPath, entry.name); - if (entry.isDirectory()) { - // Recursively get structure for subdirectories - const {structure, size: subSize} = await getDirectoryStructure(entryPath); - result[baseName][entry.name] = structure[Object.keys(structure)[0]]; // Object.keys to fetch the first key name - totalSize += subSize; - } else { - // Add file name to the 'files' array and calculate total size - const stats = await fsPromises.stat(entryPath); - result[baseName].files.push(entry.name); - totalSize += stats.size; - } - } - return {structure: result, size: totalSize}; -} - -async function createBackupScheme(serverIp, structure, size) { - let filePath = path.join(__dirname, '..', 'loginData.json'); - await lockFile(filePath); - await decryptFileInPlace(filePath); - - const userContent = await fsPromises.readFile(filePath, 'utf8'); - const userJson = JSON.parse(userContent); - - await encryptFileInPlace(filePath) - await unlockFile(filePath); - - // Find the IP address of the machine - const machineIP = ip.address(); - - // Prepare the data to be sent - const dataToSend = { - id: userJson.id, - ip: machineIP, - directoryStructure: JSON.stringify(structure), - totalSize: size - }; - - const response = await fetch(`http://${serverIp}/backup_schemes`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': 'uc_api' - }, - body: JSON.stringify(dataToSend) - }); - - if (response.ok) { - console.log("Backup dir structure submitted.") - } else { - const data = await response.json() - console.log(data.message); - } -} - -async function watchDirectoryChanges(serverIp) { - async function readBackupConfig(filePath) { - await lockFile(filePath); - const fileContent = await fsPromises.readFile(filePath, 'utf8'); - await unlockFile(filePath); - return JSON.parse(fileContent); - } - - async function updateBackupConfig(filePath, structure, size) { - const data = { - path: filePath, - structure: structure, - size: size - }; - await lockFile(filePath); - await fsPromises.writeFile(path.join(__dirname, '..', 'dirBackup.json'), JSON.stringify(data, null, 2)); - await unlockFile(filePath); - } - - const configPath = path.join(__dirname, '..', 'dirBackup.json'); - if(!fs.existsSync(configPath)){ - console.log('backupDir file doesn\'t exists'); - return; - } - const backupConfig = await readBackupConfig(configPath); - const storedStructure = backupConfig.structure; - - const {structure: currentStructure, size: currentSize} = await getDirectoryStructure(backupConfig.path); - - if (JSON.stringify(storedStructure) !== JSON.stringify(currentStructure)) { - console.log("Directory structure has changed. Updating backup..."); - await createBackupScheme(serverIp, currentStructure, currentSize); - - await updateBackupConfig(backupConfig.path, currentStructure, currentSize); - console.log('FetcherProcess: start backup.'); - process.send({type: 'startBackup'}); - } else { - console.log("No changes detected in the directory structure."); - } -} - -let serverIp = null -decryptAndGetServerIP(path.join(__dirname, '..', 'ipConfig.json')).then(ip => { - serverIp = ip; -}); - -const timeoutInterval = 1 * 20 * 1000 //minutes * seconds * miliseconds - -setInterval(() => { - fetchUsersAndDepartments(serverIp); -}, timeoutInterval); - -setInterval(() => { - watchDirectoryChanges(serverIp); -}, timeoutInterval); - -setInterval(() => { - fetchBackupSchemes(serverIp); -}, timeoutInterval); - - -console.log("fetcher.js process started."); - -process.on('SIGINT', async () => { - console.log('Shutdown signal received in \'fetcher.js\'. Cleaning up...'); - - try { - console.log('Performing cleanup tasks...'); - console.log('Cleanup completed successfully.'); - } catch (error) { - console.error('An error occurred during cleanup:', error); - } finally { - console.log('Process terminated'); - process.exit(0); - } -}); diff --git a/User/jobs/send_file.js b/User/jobs/send_file.js deleted file mode 100644 index b37520f..0000000 --- a/User/jobs/send_file.js +++ /dev/null @@ -1,90 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const axios = require('axios'); -const FormData = require('form-data'); -const {lockFile, unlockFile} = require("../helpers/lock_mechanism"); -const {decryptFileInPlace, encryptFileInPlace} = require("../src/main/aes_encrypt"); - -async function decryptAndGetServerIP(filePath) { - try { - await lockFile(filePath); - await decryptFileInPlace(filePath); - - const fileContent = await fs.promises.readFile(filePath, 'utf8'); - const jsonData = JSON.parse(fileContent); - const serverIP = jsonData.ip; - - await encryptFileInPlace(filePath) - await unlockFile(filePath); - return serverIP; - } catch (error) { - console.error('An error occurred:', error); - return null; - } -} - -async function extractId() { - try { - // Path to the JSON file - const jsonFilePath = path.join(__dirname, '..', 'loginData.json'); - - // Read and parse the JSON file - const jsonData = await fs.promises.readFile(jsonFilePath, 'utf-8'); - const { id } = JSON.parse(jsonData); - - return id; - } catch (error) { - console.error('Failed to read file or parse data:', error); - throw error; - } -} - -async function uploadFile(filePath, serverIp) { - try { - const idUser = await extractId(); // Fetch ID from JSON - const nameOfFile = path.basename(filePath); // Extract the file name from the path - - // Ensure the file exists and get its size - if (!fs.existsSync(filePath)) { - console.error("File does not exist: " + filePath); - process.exit(1); - } - const fileSizeInBytes = fs.statSync(filePath).size; - - const form = new FormData(); - form.append('idUser', idUser); - form.append('nameOfFile', nameOfFile); - form.append('sizeOfFile', fileSizeInBytes); - form.append('file', fs.createReadStream(filePath)); - - const response = await axios.post(`http://${serverIp}/upload`, form, { - headers: { - ...form.getHeaders(), - }, - }); - - console.log('File uploaded successfully:', response.data); - process.exit(0); // Exit with status 0 (success) - } catch (error) { - console.error('Error uploading file:', error.message); - process.exit(1); // Exit with status 1 (error) - } -} - -// Command-line argument for file path -if (process.argv.length < 3) { - console.log('Usage: node + Welcome + + +
+
+

BACKUP FETCHER

+
+
+
+

Backup is not searched, please wait...

+
+
+ + diff --git a/User/render/html/login.html b/User/render/html/login.html new file mode 100644 index 0000000..9767cea --- /dev/null +++ b/User/render/html/login.html @@ -0,0 +1,38 @@ + + + + + + + + + + Login + + +
+
+

DO WE KNOW

+

EACH OTHER?

+
+ +
+ + + + + diff --git a/User/src/renderer/html/main_menu.html b/User/render/html/main_menu.html similarity index 58% rename from User/src/renderer/html/main_menu.html rename to User/render/html/main_menu.html index c5b63a1..dd8d0e7 100644 --- a/User/src/renderer/html/main_menu.html +++ b/User/render/html/main_menu.html @@ -9,45 +9,34 @@ - + Main Page -
-
-

CEO Authentication

- -
- - -
-
-

WELCOME BACK,

-

+

Hope you have a productive day!

- - + +
- - - + + +
- - +
- +
-
+

USERS


diff --git a/User/render/html/sign_up.html b/User/render/html/sign_up.html new file mode 100644 index 0000000..4a1bdcc --- /dev/null +++ b/User/render/html/sign_up.html @@ -0,0 +1,63 @@ + + + + + + + + + Signup Process + + + +
+ +
+
+

LET US MEET

+

EACH OTHER

+
+ + + + + +
+ + + +
+ + + + + diff --git a/User/src/renderer/html/sending_file_confirmation.html b/User/render/html/uc_not_found.html similarity index 52% rename from User/src/renderer/html/sending_file_confirmation.html rename to User/render/html/uc_not_found.html index ca50d3a..fb3ce49 100644 --- a/User/src/renderer/html/sending_file_confirmation.html +++ b/User/render/html/uc_not_found.html @@ -5,21 +5,22 @@ - + - - ; + - Sending file + UC Not Found
-

SENDING THE FILE!

-

PlEASE WAIT

- Description of GIF - +

SEARCHING FOR THE

+

SERVER...

+
+
+
+

Please wait while we try to connect to the server.

diff --git a/User/render/html/welcome.html b/User/render/html/welcome.html new file mode 100644 index 0000000..ffcc348 --- /dev/null +++ b/User/render/html/welcome.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + Welcome + + +
+
+

WELCOME TO THE APP

+
+
+
+

Initializing the application, please wait...

+
+
+ + diff --git a/User/render/js/helpers.js b/User/render/js/helpers.js new file mode 100644 index 0000000..13f13b9 --- /dev/null +++ b/User/render/js/helpers.js @@ -0,0 +1,36 @@ +function fadeIn() { + document.querySelector('.container').classList.remove('fade-out'); + document.querySelector('.container').classList.add('fade-in'); +} + +function fadeOut(destination) { + const container = document.querySelector('.container'); + container.classList.remove('fade-in'); + container.classList.add('fade-out'); + + console.log(destination); + setTimeout(() => {}, 5000); + + container.addEventListener('animationend', async () => { + try { + await window.electronAPI.changeContent(destination); + console.log('Navigated to', destination); + } catch (error) { + console.error('Error navigating:', error); + } + }); +} + +async function waitForResponse() { + return new Promise((resolve) => { + const idResponseCheck = setInterval(async () => { + const status = await window.electronAPI.hasResponseArrived(); + if (status) { + clearInterval(idResponseCheck); + const response = await window.electronAPI.getLastUcResult(); + resolve(response || null); // Resolve the response or null if not available + } + }, 100); // Check every 100 milliseconds if the response has arrived + }); +} + diff --git a/User/render/js/login.js b/User/render/js/login.js new file mode 100644 index 0000000..ecae6a5 --- /dev/null +++ b/User/render/js/login.js @@ -0,0 +1,150 @@ +document.addEventListener('DOMContentLoaded', async function () { + const submitButton = document.getElementById('submit'); + const signUpButton = document.getElementById('signup'); + const resetPasswordButton = document.getElementById('resetPassword'); + + // Retrieve the operation codes via IPC + const operationCodes = await window.electronAPI.getOperationsCodes(); + if (!operationCodes) { + await window.electronAPI.showAlert('Internal error of the application. Failed to retrieve operation codes.'); + return; + } + + const codeLogin = operationCodes.LOGIN; + const codeFindByEmail = operationCodes.FIND_BY_EMAIL; + const codeFindKeyByUser = operationCodes.FIND_KEY_BY_USER_ID; + const codeOk = operationCodes.OK; + + signUpButton.addEventListener('click', function (e) { + e.preventDefault(); + window.electronAPI.changeContent('sign_up'); + }); + + resetPasswordButton.addEventListener('click', function (e) { + e.preventDefault(); + window.electronAPI.changeContent('reset_password'); + }); + + // Submit button logic (handle login) + submitButton.addEventListener('click', async function (e) { + e.preventDefault(); + console.log('Submit button clicked'); + + const form = document.getElementById('loginForm'); + const formData = new FormData(form); + + const email = formData.get('email'); + const password = formData.get('password'); + + // Open a TCP socket to the stored IP + if (!await window.electronAPI.openUcSocket()) { + await window.electronAPI.showAlert('Internal error of the application. Unable to open socket.'); + return; + } + + // Attempt login + if (!await attemptLogin(email, password)) { + await window.electronAPI.closeUcSocket(); + return; + } + + // Fetch and store user info + if (!await fetchAndStoreUserInfo(email)) { + await window.electronAPI.closeUcSocket(); + return; + } + + // Fetch user info from local storage + const userInfo = await window.electronAPI.readUserConfig('user_info'); + if (!userInfo) { + await window.electronAPI.closeUcSocket(); + await window.electronAPI.showAlert('Failed to fetch user info.'); + return; + } + + // Fetch and store encryption key + if (!await fetchAndStoreEncryptionKey(userInfo.id)) { + await window.electronAPI.closeUcSocket(); + return; + } + + // Close the socket and navigate to main menu after success + await window.electronAPI.closeUcSocket(); + await window.electronAPI.changeContent('main_menu'); + }); + + async function attemptLogin(email, password) { + const app_type = await window.electronAPI.readUserConfig('app_type'); + const messageData = { email, password, app_type }; + + // Send the login message to the server + if (!await window.electronAPI.sendUcMessage(codeLogin, messageData)) { + await window.electronAPI.showAlert('Failed to send login request.'); + return false; + } + + // Wait for the response using waitForResponse + const response = await waitForResponse(); + if (response && response.operationCode === codeOk) { + await window.electronAPI.resetApplicationInfo(); + await window.electronAPI.resetUserConfig(); + await window.electronAPI.writeUserConfig('app_type', app_type); + await window.electronAPI.writeUserConfig('user_info', { email, password }); + return true; + }else if(response && respone.operationCode !== codeOk){ + await window.electronAPI.showAlert(response.metaInfo.message); + return false; + } + + await window.electronAPI.showAlert('Invalid login response received.'); + return false; + } + + async function fetchAndStoreUserInfo(userEmail) { + if (!await window.electronAPI.sendUcMessage(codeFindByEmail, { email: userEmail })) { + await window.electronAPI.showAlert('Failed to send request to fetch user info.'); + return false; + } + + // Wait for the response using waitForResponse + const response = await waitForResponse(); + if (response && response.operationCode === codeOk) { + let userInfo = await window.electronAPI.readUserConfig('user_info'); + if (!userInfo) { + userInfo = {}; + } + + userInfo.id = response.metaInfo.id; + userInfo.departmentId = response.metaInfo.departmentId; + userInfo.name = response.metaInfo.name || 'User'; + + await window.electronAPI.writeUserConfig('user_info', userInfo); + return true; + } + + await window.electronAPI.showAlert('Failed to fetch user info from server.'); + return false; + } + + async function fetchAndStoreEncryptionKey(userId) { + if (!await window.electronAPI.sendUcMessage(codeFindKeyByUser, { userId })) { + await window.electronAPI.showAlert('Failed to send request to fetch encryption key.'); + return false; + } + + // Wait for the response using waitForResponse + const response = await waitForResponse(); + if (response && response.operationCode === codeOk) { + const encryptionKey = { + key: response.metaInfo.key.key, + iv: response.metaInfo.key.iv, + }; + + await window.electronAPI.writeUserConfig('encryption_key', encryptionKey); + return true; + } + + await window.electronAPI.showAlert('Failed to fetch encryption key from server.'); + return false; + } +}); diff --git a/User/render/js/main_menu.js b/User/render/js/main_menu.js new file mode 100644 index 0000000..466564f --- /dev/null +++ b/User/render/js/main_menu.js @@ -0,0 +1,216 @@ +document.addEventListener('DOMContentLoaded', async function () { + setInterval(loadReceivedFiles, 3000); // Call loadReceivedFiles every 3000 ms (3 seconds) + + const backupButton = document.getElementById('backup-dir'); + const shareButton = document.getElementById('share-dir'); + const departmentButton = document.getElementById('department-dir'); + + const changeInfoButton = document.getElementById('change-info'); + const shareFileButton = document.getElementById('share-file'); + const logoutButton = document.getElementById('logout'); + const restoreBackupButton = document.getElementById('restore-backup'); + + await fetchUserInfo(); + await checkAndSetAllDirectories(); + + backupButton.addEventListener('click', async function () { + await setPath('backupDirectory'); + }); + + shareButton.addEventListener('click', async function () { + await setPath('shareDirectory'); + }); + + departmentButton.addEventListener('click', async function () { + await setPath('departmentDirectory'); + }); + + changeInfoButton.addEventListener('click', function () { + fadeOut('profile'); + }); + + shareFileButton.addEventListener('click', function () { + fadeOut('share_file'); + }); + + logoutButton.addEventListener('click', async function () { + fadeOut('login'); + }); + + // Add event listener for the restore backup button + restoreBackupButton.addEventListener('click', async function () { + await restoreBackup(); + }); +}); + +async function restoreBackup() { + const backupDirectory = await window.electronAPI.readApplicationInfo('backupDirectory'); + + if (backupDirectory && backupDirectory.path) { + // If backup directory is already set, display an alert + await window.electronAPI.showAlert('You have already set a backup directory. You cannot restore again.'); + return; + } + + // Prompt the user to choose the destination for the restored backup + const destinationPath = await window.electronAPI.selectDirectory(); + if (!destinationPath) { + return; // User canceled the directory selection + } + + // Call the IPC method to initiate the backup retrieval process + await window.electronAPI.startBackupRetrieval(destinationPath); + + // Switch the content to the 'backup_retrieve' page + fadeOut('backup_retrieve'); +} + +async function checkAndSetAllDirectories() { + await attachNotificationButton('backupDirectory', 'Set your backup directory!', 'backup_alert', 'alert'); + await attachNotificationButton('shareDirectory', 'Set your share directory!', 'share_alert', 'alert'); + await attachNotificationButton('departmentDirectory', 'Set your department directory!', 'department_alert', 'alert'); +} + +async function checkPathExistence(pathKey) { + return await window.electronAPI.readApplicationInfo(pathKey); +} + +async function setPath(pathKey) { + const path = await window.electronAPI.selectDirectory(); + if (path === undefined) return; + + const id = await window.electronAPI.createMemoryEntry() + console.log({id, path}) + await window.electronAPI.writeApplicationInfo(pathKey, {id, path}); +} + +async function attachNotificationButton(pathKey, buttonText, buttonId, buttonName) { + const path = await checkPathExistence(pathKey); + if (!path) { + const notificationsDiv = document.getElementById('notifications'); + const button = document.createElement('button'); + button.id = buttonId; + button.name = buttonName; + button.textContent = buttonText; + button.addEventListener('click', async function () { + await setPath(pathKey); + button.remove(); // Remove button after setting the path + }); + notificationsDiv.appendChild(button); + } +} + +async function fetchUserInfo() { + const usernameField = document.getElementById('username-field'); + + // Read the user credentials from the userConfig + let userInfo = await window.electronAPI.readUserConfig('user_info'); + if (userInfo && userInfo.name) { + usernameField.textContent = userInfo.name; + return; + } + + // Update the greeting with the fetched user's name + if (usernameField) { + usernameField.textContent = userInfo.name; // Update the h1 with the user's name + } else { + console.error("Username field is not available in the DOM."); + } +} + +async function loadReceivedFiles() { + // Read the shareDirectory from applicationInfo + const shareDirectoryData = await window.electronAPI.readApplicationInfo('shareDirectory'); + + if (!shareDirectoryData || !shareDirectoryData.id) { + console.log('No received files or directory ID found.'); + return; + } + + const directoryId = shareDirectoryData.id; + + // Fetch the directory structure (JSON objects with user names and file paths) + const directoryStructure = await window.electronAPI.readMemoryEntry(directoryId); + + if (!directoryStructure || !directoryStructure.structure) { + console.log('No received files found in the directory structure.'); + return; + } + + const notificationsDiv = document.getElementById('notifications'); + const existingButtons = Array.from(notificationsDiv.children).map(button => button.getAttribute('data-filepath')); + + // Iterate over each user and their files in the structure + Object.keys(directoryStructure.structure).forEach(userName => { + const userFiles = directoryStructure.structure[userName]; + + // Iterate over each file of the user + Object.keys(userFiles).forEach(fileName => { + const filePath = userFiles[fileName]; + + // Check if a button for this file path already exists + if (!existingButtons.includes(filePath)) { + const button = document.createElement('button'); + button.setAttribute('data-filepath', filePath); // Set a custom attribute to track the file path + button.name = 'notification'; + button.textContent = `You received a file "${fileName}" from ${userName}`; // Display the userName and file name + button.addEventListener('click', () => handleFileReceivedButtonPressed(filePath, button)); + notificationsDiv.appendChild(button); + } + }); + }); +} + +async function removeReceivedFile(filePath) { + // Read the shareDirectory from applicationInfo + const shareDirectoryData = await window.electronAPI.readApplicationInfo('shareDirectory'); + + if (!shareDirectoryData || !shareDirectoryData.id) { + console.log('No directory ID found to update.'); + return; + } + + const directoryId = shareDirectoryData.id; + + // Fetch the directory structure + let directoryStructure = await window.electronAPI.readMemoryEntry(directoryId); + + if (directoryStructure && directoryStructure.structure) { + // Iterate over each user and their files + for (let userName in directoryStructure.structure) { + let userFiles = directoryStructure.structure[userName]; + + // Check if the file exists and remove it + if (userFiles[filePath]) { + delete userFiles[filePath]; + + // Remove the user if no more files are left + if (Object.keys(userFiles).length === 0) { + delete directoryStructure.structure[userName]; + } + + // Update the directory structure in memory + await window.electronAPI.updateMemoryEntry(directoryId, directoryStructure); + console.log(`File ${filePath} removed from memory.`); + return; + } + } + } else { + console.log('No directory structure found in memory to update.'); + } +} + +async function handleFileReceivedButtonPressed(filePath, button) { + console.log('Notification button clicked!'); + + // Open the file in the file explorer + await window.electronAPI.showFileInExplorer(filePath) + .then(() => console.log('File explorer opened for: ' + filePath)) + .catch(error => console.error('Error opening file explorer:', error)); + + // Remove the button after opening the file + button.remove(); + + // Remove the file from memory + await removeReceivedFile(filePath); +} diff --git a/User/render/js/profile.js b/User/render/js/profile.js new file mode 100644 index 0000000..c942e50 --- /dev/null +++ b/User/render/js/profile.js @@ -0,0 +1,82 @@ +document.addEventListener('DOMContentLoaded', async function () { + const {email, password, name} = await window.electronAPI.readUserConfig('user_info'); + + const emailInput = document.querySelector('input[name="email"]'); + const usernameInput = document.querySelector('input[name="username"]'); + const passwordInput = document.querySelector('input[name="password"]'); + + // Pre-fill form fields with existing data + emailInput.value = email; + usernameInput.value = name; + passwordInput.value = password; + + const backButton = document.querySelector('button[name="login"]'); + const submitButton = document.querySelector('button[name="submit"]'); + + // Get operation codes from the backend + const operationCodes = await window.electronAPI.getOperationsCodes(); + if (!operationCodes) { + await window.electronAPI.showAlert('Internal error of the application.'); + return; + } + + const codeModifyUser = operationCodes.MODIFY_USER; + const codeOk = operationCodes.OK; + + // Handle the back button click + backButton.addEventListener('click', async function () { + console.log('Back button clicked!'); + fadeOut('main_menu'); + }); + + // Handle the submit button click + submitButton.addEventListener('click', async function (e) { + e.preventDefault(); + console.log('Submit button clicked!'); + + // Fetch form values + const email = emailInput.value; + const name = usernameInput.value; + const password = passwordInput.value; + + const { id, departmentId, app_type } = await window.electronAPI.readUserConfig('user_info'); // Fetch login data + + // Prepare the data to be sent via the UC socket + const messageData = { + id: id, + name: name, + email: email, + password: password, + departmentId: departmentId, + app_type: app_type // Include app_type in the payload + }; + + // Open UC socket + if (!await window.electronAPI.openUcSocket()) { + await window.electronAPI.showAlert('Failed to open socket. Internal error of the application.'); + return; + } + + // Send the message to the server using the UC socket + if (!await window.electronAPI.sendUcMessage(codeModifyUser, messageData)) { + await window.electronAPI.showAlert('Failed to send message.'); + return; + } + + // Wait for the response + const response = await waitForResponse(); + if (response && response.operationCode === codeOk) { + console.log('User update successful.'); + + // Save updated user info to the userConfig + await window.electronAPI.writeUserConfig('user_credentials', { email: email, password: password }); + await window.electronAPI.writeUserConfig('user_info', {id: id, departmentId: departmentId, name: name}); + + // Navigate back to the main menu + fadeOut('main_menu'); + } else { + console.log('Error updating user:', response?.metaInfo?.message || 'Unknown error'); + await window.electronAPI.showAlert(response?.metaInfo?.message || 'Unknown error occurred'); + } + }); +}); diff --git a/User/render/js/reset_password.js b/User/render/js/reset_password.js new file mode 100644 index 0000000..06a2f67 --- /dev/null +++ b/User/render/js/reset_password.js @@ -0,0 +1,63 @@ +document.addEventListener('DOMContentLoaded', async function () { + const backToLoginButton = document.getElementById('backToLogin'); + const resetPasswordButton = document.getElementById('resetPassword'); + + // Retrieve the operation codes via IPC + const operationCodes = await window.electronAPI.getOperationsCodes(); + if (!operationCodes) { + await window.electronAPI.showAlert('Internal error of the application.'); + return; + } + + const codeResetPassword = operationCodes.RESET_PASSWORD; + const codeOk = operationCodes.OK; + + // Back to login + backToLoginButton.addEventListener('click', function (e) { + e.preventDefault(); + window.electronAPI.changeContent('login'); + }); + + // Reset password logic + resetPasswordButton.addEventListener('click', async function (e) { + e.preventDefault(); + if (!await window.electronAPI.openUcSocket()) { + await window.electronAPI.showAlert('Internal error of the application.'); + return; + } + + const form = document.getElementById('resetPasswordForm'); + const formData = new FormData(form); + const email = formData.get('email'); + const newPassword = formData.get('newPassword'); + + // Ensure the email and new password are provided + if (!email || !newPassword) { + await window.electronAPI.showAlert('Please provide both email and new password.'); + return; + } + + if (!await attemptResetPassword(email, newPassword)) { + return; + } + + await window.electronAPI.closeUcSocket(); + await window.electronAPI.showAlert('Password reset successfully.'); + window.electronAPI.changeContent('login'); + }); + + async function attemptResetPassword(email, newPassword) { + const app_type = await window.electronAPI.readUserConfig('app_type'); + const messageData = { email, newPassword, app_type }; + + if (!await window.electronAPI.sendUcMessage(codeResetPassword, messageData)) return false; + + const response = await waitForResponse(); + if (response && response.operationCode === codeOk) { + return true; + } else { + await window.electronAPI.showAlert(response?.metaInfo?.message || 'Error resetting password.'); + return false; + } + } +}); diff --git a/User/render/js/share_file.js b/User/render/js/share_file.js new file mode 100644 index 0000000..2053302 --- /dev/null +++ b/User/render/js/share_file.js @@ -0,0 +1,118 @@ +document.addEventListener('DOMContentLoaded', async function () { + let user_info = {}; + let pathToFile = ''; + + user_info = await window.electronAPI.readUserConfig('user_info'); + if (!user_info) { + await window.electronAPI.showAlert('No user_info entry found.'); + return; + } + + // Function to update the file name display + function updateFileName() { + const fileNameElement = document.getElementById('fileName'); + if (!fileNameElement) { + return; + } + + if (pathToFile.trim() === '') { + fileNameElement.textContent = 'No file chosen'; + } else { + fileNameElement.textContent = pathToFile.split('\\').pop().split('/').pop(); + } + } + + updateFileName(); + + // Fetch user information from applicationInfo and create checkboxes for selecting users + async function fetchUsersAndCreateCheckboxes() { + const usersInfoId = await window.electronAPI.readApplicationInfo('active_users_info'); + if (!usersInfoId) { + await window.electronAPI.showAlert('Internal error.'); + return; + } + + const usersInfo = await window.electronAPI.readMemoryEntry(usersInfoId); + if (!usersInfo || usersInfo.length === 0) { + await window.electronAPI.showAlert('No active users found.'); + await window.electronAPI.changeContent('main_menu'); + return; + } + + const usersDiv = document.querySelector('.choose_user_form_content'); + usersDiv.innerHTML = ''; + usersInfo.forEach(user => { + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.name = 'users'; + checkbox.value = user.ip; // Store user's IP in the value + + const label = document.createElement('label'); + label.innerHTML = `${user.user_info.name}`; // Display user's name + label.insertBefore(checkbox, label.firstChild); + + usersDiv.appendChild(label); + }); + } + + // Event listener for selecting the file + document.getElementById('selectFile').addEventListener('click', async function () { + try { + pathToFile = await window.electronAPI.selectFile(); + updateFileName(); + } catch (error) { + console.error('Error opening file dialog:', error); + } + }); + + // Event listener for back button to navigate to main menu + document.getElementById('backButton').addEventListener('click', function () { + fadeOut('main_menu'); + }); + + // Submit button logic to queue file send tasks and then navigate to main_menu.html + document.getElementById('submitButton').addEventListener('click', async function (event) { + event.preventDefault(); + + // Check if a file was chosen + if (!pathToFile.trim()) { + await window.electronAPI.showAlert('File not chosen!'); + return; + } + + const form = document.getElementById('userDestForm'); + const checkboxes = form.querySelectorAll('input[name="users"]'); + const selectedUserIps = Array.from(checkboxes) + .filter(checkbox => checkbox.checked) + .map(checkbox => checkbox.value); // Get IP of the selected users + + if (!selectedUserIps.length) { + await window.electronAPI.showAlert('No user selected!'); + return; + } + + for (const selectedUserIp of selectedUserIps) { + try { + // Add task to send file to the queue + const task = { + ip: selectedUserIp, // Destination IP for the file + path: pathToFile, // File path + userName: user_info.name // Sender's username from userConfig + }; + await window.electronAPI.addTaskToSendFileQueue(task); + + console.log(`Task added to send file to IP ${selectedUserIp}`); + } catch (error) { + console.error(`Error processing IP ${selectedUserIp}:`, error); + } + } + + // Navigate back to the main menu after all tasks are queued + fadeOut('main_menu'); // Navigate to the main menu + }); + + // Fetch users and create checkboxes every 5 seconds (in case of updates) + setInterval(fetchUsersAndCreateCheckboxes, 5000); + + fetchUsersAndCreateCheckboxes().then(() => console.log('User checkboxes rendered')); +}); diff --git a/User/render/js/sign_up.js b/User/render/js/sign_up.js new file mode 100644 index 0000000..2ec0477 --- /dev/null +++ b/User/render/js/sign_up.js @@ -0,0 +1,134 @@ +document.addEventListener('DOMContentLoaded', async function () { + const nextToStep2Button = document.getElementById('nextToStep2'); + const backToStep1Button = document.getElementById('backToStep1'); + const submitSignupButton = document.getElementById('submitSignup'); + const backToLogin = document.getElementById('backToLogin'); + + const step1 = document.getElementById('step1'); + const step2 = document.getElementById('step2'); + + // This object will hold the user data + let userData = { + email: null, + name: null, + password: null, + app_type: null, + departmentId: null + }; + + if (!await window.electronAPI.openUcSocket()) { + alert('Internal error of the application.'); + return; + } + + const operationCodes = await window.electronAPI.getOperationsCodes(); + if (!operationCodes) { + await window.electronAPI.showAlert('Internal error of the application.'); + return; + } + + const codeGetDepartments = operationCodes.GET_DEPARTMENTS; + const codeSignUp = operationCodes.SIGN_UP; + const codeOk = operationCodes.OK; + + let departments = await getDepartments(); + if (departments === null) { + await window.electronAPI.showAlert('Failed to fetch departments.'); + return; + } + console.log(departments); + + // Handle form submission for Step 1 (Profile Information) + nextToStep2Button.addEventListener('click', async function (e) { + e.preventDefault(); // Prevent the default form submission + + // Collect data from Step 1 inputs + userData.email = document.querySelector('input[name="email"]').value; + userData.name = document.querySelector('input[name="name"]').value; + userData.password = document.querySelector('input[name="password"]').value; + userData.app_type = await window.electronAPI.readUserConfig('app_type'); + + // Move to Step 2 + const departmentList = document.getElementById('departmentList'); + departmentList.innerHTML = ''; // Clear existing departments + + // Populate department list + departments.forEach(department => { + if (department.name !== 'CEO' && department.name !== 'ADMIN') { + const label = document.createElement('label'); + label.innerHTML = ` + + ${department.name} + `; + departmentList.appendChild(label); + } + }); + + switchStep(step1, step2); + }); + + // Handle form submission for Step 2 (Department Selection) + submitSignupButton.addEventListener('click', async function (e) { + e.preventDefault(); // Prevent form from being submitted in the usual way + + // Get the selected department + userData.departmentId = document.querySelector('input[name="dept"]:checked')?.value; + if (!userData.departmentId) { + await window.electronAPI.showAlert('Please select a department.'); + return; + } + + if (!await attemptSignUp()) { + switchStep(step2, step1); + return; + } + + await window.electronAPI.showAlert('Signup successful!'); + await window.electronAPI.changeContent('login'); + }); + + // Back to Step 1 from Step 2 + backToStep1Button.addEventListener('click', function () { + switchStep(step2, step1); + }); + + // Back to login + backToLogin.addEventListener('click', async function (e) { + e.preventDefault(); + await window.electronAPI.closeUcSocket(); + await window.electronAPI.changeContent('login'); + }); + + async function getDepartments() { + if (!await window.electronAPI.sendUcMessage(codeGetDepartments)) return null; + + const response = await waitForResponse(); + if (response && response.operationCode === codeOk) { + return response.metaInfo.departments; + } + return null; + } + + async function attemptSignUp() { + if (!await window.electronAPI.sendUcMessage(codeSignUp, userData)) return false; + + const response = await waitForResponse(); + if (response && response.operationCode === codeOk) { + return true; + } + await window.electronAPI.showAlert(`Signup failed: ${response?.metaInfo?.message || 'Unknown error'}`); + return false; + } + + // Function to switch between steps + function switchStep(fromStep, toStep) { + fromStep.classList.remove('active'); // Fade out the current step + setTimeout(() => { + fromStep.style.display = 'none'; // Hide the current step after transition + toStep.style.display = 'flex'; // Ensure display is flex for the next step + setTimeout(() => { + toStep.classList.add('active'); // Fade in the next step + }, 20); // Small delay to allow display change before applying opacity + }, 500); // Transition duration (0.5s) + } +}); diff --git a/User/secret.key b/User/secret.key deleted file mode 100644 index 5931e61..0000000 --- a/User/secret.key +++ /dev/null @@ -1 +0,0 @@ -`]'hźV8nkAVf7K \ No newline at end of file diff --git a/User/src/helpers/backup_manager.ts b/User/src/helpers/backup_manager.ts new file mode 100644 index 0000000..38154dd --- /dev/null +++ b/User/src/helpers/backup_manager.ts @@ -0,0 +1,205 @@ +import fs from 'fs'; +import path from 'path'; +import { FileEncryptor } from './file_encryptor'; // Assume the class is in this file +import { MemoryManager } from './memory_manager'; // Assume this handles memory-based storage +import { JsonManager } from './json_manager'; // Manages JSON-based configurations +import { TcpClient } from '../network/tcp/tcp_client' // Import your TcpClient class +import { operationCodes } from '../network/operation_codes'; +import { parentPort } from 'worker_threads'; + +export class BackupManager { + private fileEncryptor: FileEncryptor | null = null; + private memoryManager: MemoryManager; + private applicationInfo: JsonManager; + private userConfig: JsonManager; + private readonly clientPort: number; + + constructor(userConfigPath: string, applicationInfoPath: string, memoryManagerPath: string, clientPort: number) { + this.applicationInfo = new JsonManager(applicationInfoPath); + this.userConfig = new JsonManager(userConfigPath); + this.memoryManager = new MemoryManager(memoryManagerPath); + this.clientPort = clientPort; + } + + async start(): Promise { + setInterval(async () => { + await this.initialize(); // Re-run every minute + }, 60000); // 1 minute interval + } + + // Initialize the backup process: fetch data from the app info and memory + private async initialize(): Promise { + // Initialize the encryption settings from UserConfig + const encryptionKeyData = await this.userConfig.readValue('encryption_key'); + if (!encryptionKeyData || !encryptionKeyData.key || !encryptionKeyData.iv) { + console.error('Encryption key data is missing in user configuration.'); + return; + } + + // Create a new FileEncryptor with the retrieved key and IV + this.fileEncryptor = new FileEncryptor(encryptionKeyData.key, encryptionKeyData.iv); + + // Get the name of the user + const userInfo = await this.userConfig.readValue('user_info'); + if (!userInfo || !userInfo.name) { + console.error('User information is missing in user configuration.'); + return; + } + + const userName = userInfo.name; + + // Get the backup directory information from ApplicationInfo + const backupDirectoryData = await this.applicationInfo.readValue('backupDirectory'); + if (!backupDirectoryData || !backupDirectoryData.id || !backupDirectoryData.path) { + console.error('Backup directory information is missing in application info.'); + return; + } + + const activeUsersIp = await this.applicationInfo.readValue('users_ip'); + if (!activeUsersIp || !activeUsersIp.length) { + console.error('No active users found.'); + return; + } + + const backupDirectoryId = backupDirectoryData.id; + const backupDirectoryPath = backupDirectoryData.path; + + // Get the file structure from MemoryManager + const directoryData = await this.memoryManager.retrieveMetaInformation(backupDirectoryId); + if (!directoryData || !directoryData.structure) { + console.error('Backup directory structure is missing in memory.'); + return; + } + + // Send files to the list of IPs + await this.sendFilesToUsers(directoryData.structure, userName, activeUsersIp, backupDirectoryPath); + } + + // Method to encrypt a file and return the base64 string + private encryptFile(filePath: string): string { + if (!this.fileEncryptor) { + return filePath; + } + + if (!fs.existsSync(filePath)) { + console.error(`File not found: ${filePath}`); + return ''; + } + + // Use FileEncryptor to encrypt the file and return the base64 string + return this.fileEncryptor.encryptFileToBase64(filePath); + } + + // Send files to the list of users and remove successfully sent files from the list + private async sendFilesToUsers(fileStructure: { [key: string]: string }, userName: string, usersIp: string[], backupDirectoryPath: string): Promise { + let unsentFiles = Object.keys(fileStructure); // Keep track of unsent files + + for (const fileName of unsentFiles) { + const filePath = fileStructure[fileName]; + const encryptedFileContent = this.encryptFile(filePath); + + if (!encryptedFileContent) { + console.error(`Failed to encrypt file: ${fileName}`); + continue; // Skip to the next file + } + + // Calculate relative file path + const relativeFilePath = path.relative(backupDirectoryPath, filePath); // Get the relative file path + + // Meta information to send + const metaInfo = { + userName, // Name of the user + relativeFilePath // Relative path to preserve the directory structure + }; + + for (const ip of usersIp) { + const tcpClient = new TcpClient(this.clientPort); + tcpClient.openSocket(ip); + + // Wait for AES key and send the file + try { + const sendSuccess = await this.waitForAesKeyAndSendFile(tcpClient, operationCodes.BACKUP_FILE, metaInfo, Buffer.from(encryptedFileContent, 'base64')); + if (sendSuccess) { + console.log(`Successfully sent file: ${fileName} to ${ip}`); + unsentFiles = unsentFiles.filter(f => f !== fileName); // Remove the file from the unsent list + tcpClient.closeSocket(); + break; // Move to the next file after successful send + } + } catch (error) { + console.error(`Failed to send file: ${fileName} to ${ip}. Error: ${error}`); + tcpClient.closeSocket(); + } + } + } + + // If there are any unsent files, notify the parent process + if (unsentFiles.length > 0) { + parentPort?.postMessage({ success: false, message: 'Backup could not be completed for all files', unsentFiles }); + } else { + parentPort?.postMessage({ success: true, message: 'Backup completed successfully' }); + } + } + + // Wait for AES key to be set, send the file, and wait for the response + private async waitForAesKeyAndSendFile(tcpClient: TcpClient, operationCode: string, metaInfo: { [key: string]: any }, fileContent: Buffer, checkInterval: number = 500, timeout: number = 10000): Promise { + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + const intervalId = setInterval(async () => { + const elapsedTime = Date.now() - startTime; + + // Check if AES key is set, if timeout occurs, reject + if (!tcpClient.isAesKeySet()) { + if (elapsedTime > timeout) { + clearInterval(intervalId); + console.error('Timeout waiting for AES key.'); + reject(new Error('Timeout waiting for AES key.')); + } + return; // Continue waiting for AES key + } + + // AES key is set, send the message + clearInterval(intervalId); + + try { + const success = await tcpClient.sendMessage(operationCode, metaInfo, fileContent); + if (!success) { + reject(new Error('Failed to send the file content.')); + return; + } + + // Wait for a response after sending the message + const responseReceived = await this.waitForMessageResponse(tcpClient, timeout); + if (!responseReceived) { + reject(new Error('Timeout waiting for the message response.')); + return; + } + + // Everything went fine + resolve(true); + } catch (error) { + reject(error); + } + }, checkInterval); // Check for AES key every `checkInterval` + }); + } + + // Method to wait for the response from the TCP client + private waitForMessageResponse(tcpClient: TcpClient, timeout: number = 10000): Promise { + return new Promise((resolve) => { + const startTime = Date.now(); + const intervalId = setInterval(() => { + const response = tcpClient.getLastResult(); + const elapsedTime = Date.now() - startTime; + + if (response) { + clearInterval(intervalId); + resolve(true); + } else if (elapsedTime > timeout) { + clearInterval(intervalId); + resolve(false); // No response after timeout + } + }, 100); + }); + } +} diff --git a/User/src/helpers/backup_retrieval.ts b/User/src/helpers/backup_retrieval.ts new file mode 100644 index 0000000..a21a122 --- /dev/null +++ b/User/src/helpers/backup_retrieval.ts @@ -0,0 +1,224 @@ +import { JsonManager } from './json_manager'; // Assuming this manages JSON configurations +import { TcpClient } from '../network/tcp/tcp_client'; // Assuming this handles TCP client connections +import { operationCodes } from '../network/operation_codes'; // Assuming this holds operation codes +import { parentPort } from 'worker_threads'; +import path from 'path'; +import fs from 'fs'; +import crypto from 'crypto'; // Import the crypto module for encryption and decryption + +export class BackupRetrievalWorker { + private userConfig: JsonManager; + private applicationInfo: JsonManager; + private readonly clientPort: number; + private readonly destinationPath: string; + private encryptionKey: Buffer | null = null; + private iv: Buffer | null = null; + + constructor(userConfigPath: string, applicationInfoPath: string, clientPort: number, destinationPath: string) { + this.userConfig = new JsonManager(userConfigPath); + this.applicationInfo = new JsonManager(applicationInfoPath); + this.clientPort = clientPort; + this.destinationPath = destinationPath; + } + + async start(): Promise { + // Retrieve the necessary data from userConfig and applicationInfo + const userInfo = await this.userConfig.readValue('user_info'); + if (!userInfo || !userInfo.name) { + console.error('User information or name is missing.'); + return; + } + const userName = userInfo.name; + + // Load encryption key and IV from userConfig + const encryptionData = await this.userConfig.readValue('encryption_key'); + if (!encryptionData || !encryptionData.key || !encryptionData.iv) { + console.error('Encryption key or IV is missing.'); + return; + } + + // Convert encryption key and IV from base64 to buffer + this.encryptionKey = Buffer.from(encryptionData.key, 'base64'); + this.iv = Buffer.from(encryptionData.iv, 'base64'); + + const activeUsersIp = await this.applicationInfo.readValue('users_ip'); + if (!activeUsersIp || !activeUsersIp.length) { + console.error('No active users found.'); + return; + } + + // Process each IP and request backup data + for (const ip of activeUsersIp) { + try { + const success = await this.processBackupForIp(ip, userName); + if (success) { + console.log(`Backup retrieved successfully from ${ip}`); + } else { + console.error(`Failed to retrieve backup from ${ip}`); + } + } catch (error) { + console.error(`Error processing backup from ${ip}: ${error}`); + } + } + + // Notify the parent that the worker is done + parentPort?.postMessage({ success: true, message: 'Backup retrieval completed.' }); + } + + private async processBackupForIp(ip: string, userName: string): Promise { + const tcpClient = new TcpClient(this.clientPort); + tcpClient.openSocket(ip); + + try { + // Wait for the AES key to be set before continuing + const aesSet = await this.waitForAesKey(tcpClient); + if (!aesSet) { + console.error(`Timeout waiting for AES key on IP ${ip}`); + return false; + } + + // Step 1: Check if a backup exists for the user on this IP + const backupExists = await this.checkIfBackupExists(tcpClient, userName); + if (!backupExists) { + console.log(`No backup found for user ${userName} on IP ${ip}`); + return false; + } + + // Step 2: Request the structure of the backup directory + const backupStructure = await this.requestBackupStructure(tcpClient, userName); + if (!backupStructure || Object.keys(backupStructure).length === 0) { + console.log(`No files found in backup structure for user ${userName} on IP ${ip}`); + return false; + } + + // Step 3: Request and retrieve each file from the backup + for (const relativeFilePath of Object.keys(backupStructure)) { + const fileRequestSuccess = await this.requestBackupFile(tcpClient, userName, relativeFilePath); + if (!fileRequestSuccess) { + console.error(`Failed to retrieve file ${relativeFilePath} from backup for user ${userName} on IP ${ip}`); + return false; // Stop if any file fails to be retrieved + } + } + + return true; // All files retrieved successfully + } catch (error) { + console.error(`Error during backup processing for IP ${ip}: ${error}`); + return false; + } finally { + tcpClient.closeSocket(); // Ensure socket is closed + } + } + + // Wait for AES key to be set + private async waitForAesKey(tcpClient: TcpClient, timeout: number = 10000): Promise { + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + const intervalId = setInterval(() => { + const elapsedTime = Date.now() - startTime; + + if (tcpClient.isAesKeySet()) { + clearInterval(intervalId); + resolve(true); // AES key is set, we can proceed + } else if (elapsedTime > timeout) { + clearInterval(intervalId); + resolve(false); // Timeout reached, AES key not set + } + }, 500); // Check every 500ms + }); + } + + // Wait for a message response with timeout + private async waitForMessageResponse(tcpClient: TcpClient, timeout: number = 10000): Promise { + const startTime = Date.now(); + + return new Promise((resolve) => { + const intervalId = setInterval(() => { + const elapsedTime = Date.now() - startTime; + const response = tcpClient.getLastResult(); + + if (response) { + clearInterval(intervalId); + resolve(response); // Return the response + } else if (elapsedTime > timeout) { + clearInterval(intervalId); + resolve(null); // Timeout, no response + } + }, 500); // Check every 500ms + }); + } + + // Check if the backup exists for the user on the remote IP + private async checkIfBackupExists(tcpClient: TcpClient, userName: string): Promise { + const metaInfo = { name: userName }; + await tcpClient.sendMessage(operationCodes.IS_BACKUP_CREATED, metaInfo); + + const response = await this.waitForMessageResponse(tcpClient); + return response?.metaInfo?.backupExists === true; + } + + // Request the backup structure from the remote IP + private async requestBackupStructure(tcpClient: TcpClient, userName: string): Promise { + const metaInfo = { name: userName }; + await tcpClient.sendMessage(operationCodes.GET_BACKUP_STRUCTURE, metaInfo); + + const response = await this.waitForMessageResponse(tcpClient); + return response?.metaInfo?.structure ? response.metaInfo.structure : null; + } + + // Request a file from the backup and wait for it to be decrypted and stored + private async requestBackupFile(tcpClient: TcpClient, userName: string, relativeFilePath: string): Promise { + const metaInfo = { name: userName, relativeFilePath }; + await tcpClient.sendMessage(operationCodes.REQ_FILE_FROM_BACKUP, metaInfo); + + const response = await this.waitForMessageResponse(tcpClient); + if (response?.operationCode === operationCodes.OK && response.metaInfo && response.fileContent) { + return this.saveFile(response.metaInfo.relativeFilePath, response.fileContent); + } + return false; + } + + // Decrypt the file content using AES-256-CBC and save the decrypted file + private saveFile(relativeFilePath: string, fileContent: string): boolean { + if (!this.encryptionKey || !this.iv) { + console.error('Encryption key or IV is not set.'); + return false; + } + + // Decode the base64-encoded file content into a buffer + let encryptedBuffer: Buffer; + try { + encryptedBuffer = Buffer.from(fileContent, 'base64'); + } catch (error) { + console.error('Error decoding base64 file content:', error); + return false; + } + + // Decrypt the file content + let decryptedContent: Buffer; + try { + const decipher = crypto.createDecipheriv('aes-256-cbc', this.encryptionKey, this.iv); + decryptedContent = Buffer.concat([decipher.update(encryptedBuffer), decipher.final()]); + } catch (error) { + console.error('Error decrypting file:', error); + return false; + } + + // Save the decrypted file content + const fullFilePath = path.join(this.destinationPath, relativeFilePath); + + try { + const dirPath = path.dirname(fullFilePath); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); // Ensure directory exists + } + + fs.writeFileSync(fullFilePath, decryptedContent); + console.log(`File saved successfully: ${fullFilePath}`); + return true; + } catch (error: any) { + console.error(`Error saving file ${relativeFilePath}: ${error.message}`); + return false; + } + } +} diff --git a/User/src/helpers/department_sharer.ts b/User/src/helpers/department_sharer.ts new file mode 100644 index 0000000..766095f --- /dev/null +++ b/User/src/helpers/department_sharer.ts @@ -0,0 +1,278 @@ +import fs from 'fs'; +import path from 'path'; +import { TcpClient } from '../network/tcp/tcp_client'; // Assuming this class exists +import { operationCodes } from '../network/operation_codes'; +import { JsonManager } from './json_manager'; // Manages JSON configurations +import { MemoryManager } from './memory_manager'; // Manages in-memory data structures + +export class DepartmentSharer { + private userConfig: JsonManager; + private applicationInfo: JsonManager; + private memoryManager: MemoryManager; // To read the department files + private departmentDirectory: string | null; + private readonly clientPort: number; + + constructor( + userConfigPath: string, + applicationInfoPath: string, + memoryManagerPath: string, + clientPort: number + ) { + this.userConfig = new JsonManager(userConfigPath); + this.applicationInfo = new JsonManager(applicationInfoPath); + this.memoryManager = new MemoryManager(memoryManagerPath); // To retrieve files + this.clientPort = clientPort; + this.departmentDirectory = null; + } + + // Start sharing files with the department every minute + async start(): Promise { + setInterval(async () => { + await this.shareFilesWithDepartment(); // Retry every minute + }, 10000); // 10-second interval for testing + } + + // Share files with users in the same department + private async shareFilesWithDepartment(): Promise { + console.log('\n\nStarting Department Share Process\n\n'); + + // Get the current user's department information + const userInfo = await this.userConfig.readValue('user_info'); + if (!userInfo || !userInfo.departmentId || !userInfo.name) { + console.error('User information or department ID is missing in the configuration.'); + return; + } + + const departmentId = userInfo.departmentId; + const userName = userInfo.name; + + // Get the list of active users from applicationInfo + const activeUsersId = await this.applicationInfo.readValue('active_users_info'); + if (!activeUsersId) { + console.error('No active users found.'); + return; + } + + const activeUsers = await this.memoryManager.retrieveMetaInformation(activeUsersId); + if (!activeUsers || activeUsers.length === 0) { + console.error('No active users found.'); + return; + } + + // Filter users who belong to the same department + const departmentUsers = activeUsers.filter((user: any) => user.user_info.departmentId === departmentId); + + if (departmentUsers.length === 0) { + console.log('No users found in the same department.'); + return; + } + + // Get department directory info + const departmentData = await this.applicationInfo.readValue('departmentDirectory'); + if (!departmentData || !departmentData.path || !departmentData.id) { + console.error('No department directory found.'); + return; + } + + this.departmentDirectory = departmentData.path; + + // Read files from the MemoryManager related to this department + const departmentFiles = await this.memoryManager.retrieveMetaInformation(departmentData.id); + if (!departmentFiles || !departmentFiles.structure) { + console.error('No files found for this department in the memory manager.'); + return; + } + + // Send each file to every department user + await this.shareFilesWithUsers(departmentUsers, departmentFiles.structure, userName); + + // After sending all files, clear the department directory + await this.clearDepartmentDirectory(departmentUsers); + } + + // Send the files to the users in the department + private async shareFilesWithUsers(users: any[], files: { [key: string]: string }, userName: string): Promise { + const unsentFiles = Object.keys(files); // Keep track of unsent files + + for (const fileName of unsentFiles) { + const filePath = files[fileName]; + + // Ensure the file exists before attempting to send + if (!fs.existsSync(filePath)) { + console.error(`File not found: ${filePath}`); + continue; + } + + // Read the file content + const fileContent = fs.readFileSync(filePath); + + // Get the relative path of the file (used in the meta info) + if (!this.departmentDirectory) return; + const relativeFilePath = path.relative(this.departmentDirectory, filePath); + + // Prepare the metaInfo (same structure as FileSharer) + const metaInfo = { + userName, // Sender's username + relativeFilePath // Use the relative path to preserve directory structure + }; + + // Send the file to each user in the same department + for (const user of users) { + const userIp = user.ip; + const tcpClient = new TcpClient(this.clientPort); + + // Open a connection to the user's IP + tcpClient.openSocket(userIp); + + try { + // Wait for the AES key and send the file + const success = await this.waitForAesKeyAndSendFile(tcpClient, operationCodes.DEPARTMENT_FILE, metaInfo, fileContent); + if (success) { + console.log(`File successfully sent: ${fileName} to user ${userIp}`); + unsentFiles.splice(unsentFiles.indexOf(fileName), 1); // Remove successfully sent file + tcpClient.closeSocket(); // Close the connection after sending + break; // Move to the next file after a successful send + } + } catch (error) { + console.error(`Failed to send file: ${fileName} to IP: ${userIp}. Error: ${error}`); + tcpClient.closeSocket(); // Ensure socket is closed on error + } + } + } + + if (unsentFiles.length > 0) { + console.log('Some files could not be sent, retrying later.'); + } else { + console.log('All files shared successfully.'); + } + } + + // Clear the department directory for all users after sharing + private async clearDepartmentDirectory(users: any[]): Promise { + for (const user of users) { + const userIp = user.ip; + const tcpClient = new TcpClient(this.clientPort); + + // Open a connection to the user's IP + tcpClient.openSocket(userIp); + + try { + const success = await this.waitForAesKeyAndSendClearDepartment(tcpClient, operationCodes.CLEAR_DEPARTMENT); + if (success) { + console.log(`Department directory cleared successfully for user ${userIp}`); + } else { + console.error(`Failed to clear department directory for user ${userIp}`); + } + } catch (error) { + console.error(`Error clearing department directory for user ${userIp}:`, error); + } finally { + tcpClient.closeSocket(); // Ensure the socket is closed + } + } + } + + // Wait for AES key to be set, send the file, and wait for the response + private async waitForAesKeyAndSendFile(tcpClient: TcpClient, operationCode: string, metaInfo: { [key: string]: any }, fileContent: Buffer, checkInterval: number = 500, timeout: number = 10000): Promise { + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + const intervalId = setInterval(async () => { + const elapsedTime = Date.now() - startTime; + + // Check if AES key is set, if timeout occurs, reject + if (!tcpClient.isAesKeySet()) { + if (elapsedTime > timeout) { + clearInterval(intervalId); + console.error('Timeout waiting for AES key.'); + reject(new Error('Timeout waiting for AES key.')); + } + return; // Continue waiting for AES key + } + + // AES key is set, send the file + clearInterval(intervalId); + + try { + const success = await tcpClient.sendMessage(operationCode, metaInfo, fileContent); + if (!success) { + reject(new Error('Failed to send the file content.')); + return; + } + + // Wait for a response after sending the message + const responseReceived = await this.waitForMessageResponse(tcpClient, timeout); + if (!responseReceived) { + reject(new Error('Timeout waiting for the message response.')); + return; + } + + resolve(true); // Everything went fine + } catch (error) { + reject(error); + } + }, checkInterval); // Check for AES key every `checkInterval` + }); + } + + // Wait for AES key to be set, send the clear department message, and wait for the response + private async waitForAesKeyAndSendClearDepartment(tcpClient: TcpClient, operationCode: string, checkInterval: number = 500, timeout: number = 10000): Promise { + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + const intervalId = setInterval(async () => { + const elapsedTime = Date.now() - startTime; + + // Check if AES key is set, if timeout occurs, reject + if (!tcpClient.isAesKeySet()) { + if (elapsedTime > timeout) { + clearInterval(intervalId); + console.error('Timeout waiting for AES key.'); + reject(new Error('Timeout waiting for AES key.')); + } + return; // Continue waiting for AES key + } + + // AES key is set, send the clear department message + clearInterval(intervalId); + + try { + const success = await tcpClient.sendMessage(operationCode, {}); + if (!success) { + reject(new Error('Failed to send the clear department operation.')); + return; + } + + // Wait for a response after sending the message + const responseReceived = await this.waitForMessageResponse(tcpClient, timeout); + if (!responseReceived) { + reject(new Error('Timeout waiting for the message response.')); + return; + } + + resolve(true); // Everything went fine + } catch (error) { + reject(error); + } + }, checkInterval); // Check for AES key every `checkInterval` + }); + } + + // Wait for the response from the TCP client + private waitForMessageResponse(tcpClient: TcpClient, timeout: number = 10000): Promise { + return new Promise((resolve) => { + const startTime = Date.now(); + const intervalId = setInterval(() => { + const response = tcpClient.getLastResult(); + const elapsedTime = Date.now() - startTime; + + if (response?.operationCode === operationCodes.OK) { + clearInterval(intervalId); + resolve(true); // Message successfully received + } else if (elapsedTime > timeout) { + clearInterval(intervalId); + resolve(false); // No response after timeout + } + }, 100); // Check every 100ms + }); + } +} diff --git a/User/src/helpers/directory_watcher.ts b/User/src/helpers/directory_watcher.ts new file mode 100644 index 0000000..b4404b9 --- /dev/null +++ b/User/src/helpers/directory_watcher.ts @@ -0,0 +1,129 @@ +import { promises as fs, watch, FSWatcher } from 'fs'; +import path from 'path'; +import { MemoryManager } from './memory_manager'; +import { JsonManager } from './json_manager'; + +export class DirectoryWatcher { + private directoryPath: string; + private directoryMemoryId: string; + private directoryScheme: any; + private applicationInfo: JsonManager; + private memoryManager: MemoryManager; + private readonly sourceKey: string; + private directoryWatcher: FSWatcher | null; // To store the watcher reference + private totalSize: number; // To store total directory size + + constructor(memoryManagerPath: string, applicationInfoPath: string, sourceKey: string) { + this.sourceKey = sourceKey; + this.applicationInfo = new JsonManager(applicationInfoPath); + this.memoryManager = new MemoryManager(memoryManagerPath); + this.directoryMemoryId = ''; + this.directoryPath = ''; + this.directoryWatcher = null; // Initialize with no watcher + this.totalSize = 0; // Initialize size with zero + } + + // Method to initialize and validate the backup directory + async initialize(): Promise { + const directoryData = await this.applicationInfo.readValue(this.sourceKey); + if (!directoryData) { + return false; + } + + this.directoryPath = directoryData.path; + this.directoryMemoryId = directoryData.id; + + if (!this.directoryMemoryId || !this.directoryPath) { + console.error('Components of entry in \'DirectoryWatcher\' not found.'); + await this.applicationInfo.removeValue(this.sourceKey); + return false; + } + + if (!this.directoryScheme || Object.keys(this.directoryScheme).length === 0) { + // No structure in memory, scan and save it + const result = await this.buildDirectoryScheme(this.directoryPath); + this.directoryScheme = result.structure; + this.totalSize = result.size; + + await this.memoryManager.updateMetaInformation(this.directoryMemoryId, { + structure: this.directoryScheme, + totalSize: this.totalSize, + }); + } + + // Start watching the directory (after stopping any existing watcher) + this.restartWatcher(); + return true; + } + + // Recursively build the directory structure and calculate the total size + private async buildDirectoryScheme(dirPath: string): Promise<{ structure: any, size: number }> { + const directoryScheme: any = {}; + let totalSize = 0; + + const items = await fs.readdir(dirPath, { withFileTypes: true }); + + for (const item of items) { + const fullPath = path.join(dirPath, item.name); + const stats = await fs.stat(fullPath); // Get stats for each item + + if (item.isDirectory()) { + // If it's a directory, recursively build its structure and accumulate size + const { structure, size } = await this.buildDirectoryScheme(fullPath); + directoryScheme[item.name] = structure; + totalSize += size; + } else if (item.isFile()) { + // If it's a file, store its full path and accumulate size + directoryScheme[item.name] = fullPath; + totalSize += stats.size; // Add file size + } + } + + return { structure: directoryScheme, size: totalSize }; + } + + // Restart the directory watcher, ensuring any previous watcher is closed + private restartWatcher(): void { + if (this.directoryWatcher) { + console.log('Stopping existing watcher...'); + this.directoryWatcher.close(); // Stop the existing watcher + } + + this.startDirectoryWatcher(); + } + + // Start watching the backup directory for changes + private startDirectoryWatcher(): void { + if (!this.directoryPath) { + throw new Error('Backup directory not set. Cannot start watcher.'); + } + + this.directoryWatcher = watch(this.directoryPath, { recursive: true }, async (eventType, filename) => { + if (filename) { + console.log(`File change detected: ${eventType} - ${filename}`); + // Rebuild the directory scheme and update memory + const result = await this.buildDirectoryScheme(this.directoryPath); + this.directoryScheme = result.structure; + this.totalSize = result.size; + + await this.memoryManager.updateMetaInformation(this.directoryMemoryId, { + structure: this.directoryScheme, + totalSize: this.totalSize, + }); + + console.log('Directory structure and size updated in memory.'); + } + }); + + console.log(`Watching for changes in: ${this.directoryPath}`); + } + + // Close the directory watcher + public closeWatcher(): void { + if (this.directoryWatcher) { + console.log(`Stopping watcher for ${this.directoryPath}`); + this.directoryWatcher.close(); + this.directoryWatcher = null; // Clear the reference after closing + } + } +} diff --git a/User/src/helpers/file_encryptor.ts b/User/src/helpers/file_encryptor.ts new file mode 100644 index 0000000..20bc391 --- /dev/null +++ b/User/src/helpers/file_encryptor.ts @@ -0,0 +1,55 @@ +import fs from 'fs'; +import crypto from 'crypto'; + +export class FileEncryptor { + private readonly encryptionKey: Buffer; + private readonly iv: Buffer; + + constructor(base64Key: string, base64Iv: string) { + // Decode the base64-encoded key and IV + this.encryptionKey = Buffer.from(base64Key, 'base64'); + this.iv = Buffer.from(base64Iv, 'base64'); + } + + // Method to read a file, encrypt it, and return the encrypted content as a base64 string + public encryptFileToBase64(filePath: string): string { + try { + // Read the file contents + const fileBuffer = fs.readFileSync(filePath); + + // Create the cipher using AES-256-CBC (or another algorithm you prefer) + const cipher = crypto.createCipheriv('aes-256-cbc', this.encryptionKey, this.iv); + + // Encrypt the file data + let encryptedData = cipher.update(fileBuffer); + encryptedData = Buffer.concat([encryptedData, cipher.final()]); + + // Return the encrypted data as a base64 string + return encryptedData.toString('base64'); + } catch (err) { + console.error(`Error encrypting file at path ${filePath}:`, err); + throw err; + } + } + + // Method to decrypt base64-encoded encrypted content and return the decrypted buffer + public decryptBase64(encryptedBase64: string): Buffer { + try { + // Decode the base64-encoded encrypted data + const encryptedData = Buffer.from(encryptedBase64, 'base64'); + + // Create the decipher using AES-256-CBC + const decipher = crypto.createDecipheriv('aes-256-cbc', this.encryptionKey, this.iv); + + // Decrypt the data + let decryptedData = decipher.update(encryptedData); + decryptedData = Buffer.concat([decryptedData, decipher.final()]); + + // Return the decrypted buffer + return decryptedData; + } catch (err) { + console.error('Error decrypting data:', err); + throw err; + } + } +} diff --git a/User/src/helpers/file_sharer.ts b/User/src/helpers/file_sharer.ts new file mode 100644 index 0000000..b6a6d7e --- /dev/null +++ b/User/src/helpers/file_sharer.ts @@ -0,0 +1,152 @@ +import { QueueManager } from './queue_manager'; // Assume the QueueManager is in this path +import { TcpClient } from '../network/tcp/tcp_client'; // Import your TcpClient class +import { operationCodes } from '../network/operation_codes'; +import fs from "fs"; +import path from "path"; +import {compareFnFileItemTask, FileItemTask} from "../interfaces/file_item_task"; + +interface FileSendTask { + ip: string; + path: string; + userName: string; +} + +export class FileSharer { + private queueManager: QueueManager; + private readonly clientPort: number; + + constructor(queueFilePath: string, clientPort: number) { + + + this.queueManager = new QueueManager(queueFilePath, compareFnFileItemTask); + this.clientPort = clientPort; + } + + // Start processing the file queue + async start(): Promise { + setInterval(async () => { + await this.processQueue(); // Process the queue at regular intervals + }, 10000); // 10 seconds interval + } + + // Method to process the queue + private async processQueue(): Promise { + while (!this.queueManager.isEmpty()) { + const task = this.queueManager.peek(); + + if (task) { + const success = await this.sendFile(task); + + if (!success) { + console.error(`Failed to send file: ${task.path} to IP: ${task.ip}. Re-adding to queue.`); + this.queueManager.dequeue(); + this.queueManager.enqueue(task); // Re-add to queue if failed + } + else{ + console.log(`File sent successfully: ${task.path} to IP: ${task.ip}.`); + this.queueManager.dequeue(); + } + } + } + } + + // Method to send the file to a specific IP using the TcpClient + private async sendFile(task: FileSendTask): Promise { + const { ip, path: filePath, userName } = task; + + // Ensure the file exists before attempting to send + if (!fs.existsSync(filePath)) { + console.error(`File not found: ${filePath}`); + return false; + } + + // Read the file contents + const fileContent = fs.readFileSync(filePath); + + // Extract the file name from the file path using path.basename (handles both Windows and Unix) + const fileName = path.basename(filePath); + + const metaInfo = { + userName, // Sender's username + relativeFilePath: fileName, // Use the file name instead of the full path + }; + + const tcpClient = new TcpClient(this.clientPort); + tcpClient.openSocket(ip); + + // Wait for the AES key and then send the file + try { + const sendSuccess = await this.waitForAesKeyAndSendFile(tcpClient, operationCodes.SHARE_FILE, metaInfo, fileContent, task); + tcpClient.closeSocket(); + + return sendSuccess; + } catch (error) { + console.error(`Error sending file: ${filePath} to IP: ${ip}. Error: ${error}`); + tcpClient.closeSocket(); + return false; + } + } + + // Wait for AES key to be set and send the file, handling the response + private async waitForAesKeyAndSendFile(tcpClient: TcpClient, operationCode: string, metaInfo: { [key: string]: any }, fileContent: Buffer, task: FileSendTask, checkInterval: number = 500, timeout: number = 10000): Promise { + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + const intervalId = setInterval(async () => { + const elapsedTime = Date.now() - startTime; + + // Check if AES key is set, if timeout occurs, reject + if (!tcpClient.isAesKeySet()) { + if (elapsedTime > timeout) { + clearInterval(intervalId); + console.error('Timeout waiting for AES key.'); + + reject(new Error('Timeout waiting for AES key.')); + } + return; // Continue waiting for AES key + } + + // AES key is set, send the message + clearInterval(intervalId); + + try { + const success = await tcpClient.sendMessage(operationCode, metaInfo, fileContent); + if (!success) { + reject(new Error('Failed to send the file content.')); + return; + } + + // Wait for a response after sending the message + const responseReceived = await this.waitForMessageResponse(tcpClient, timeout); + if (!responseReceived) { + reject(new Error('Timeout waiting for the message response.')); + return; + } + + resolve(true); // Everything went fine + } catch (error) { + reject(error); + } + }, checkInterval); // Check for AES key every `checkInterval` + }); + } + + // Method to wait for the response from the TCP client + private waitForMessageResponse(tcpClient: TcpClient, timeout: number = 10000): Promise { + return new Promise((resolve) => { + const startTime = Date.now(); + const intervalId = setInterval(() => { + const response = tcpClient.getLastResult(); + const elapsedTime = Date.now() - startTime; + + if (response?.operationCode === operationCodes.OK) { + clearInterval(intervalId); + resolve(true); // Message successfully received + } else if (elapsedTime > timeout) { + clearInterval(intervalId); + resolve(false); // No response after timeout + } + }, 100); // Check every 100ms + }); + } +} diff --git a/User/src/helpers/json_manager.ts b/User/src/helpers/json_manager.ts new file mode 100644 index 0000000..627acac --- /dev/null +++ b/User/src/helpers/json_manager.ts @@ -0,0 +1,119 @@ +import fs from 'fs'; +import path from 'path'; + +export class JsonManager { + private readonly filePath: string; + private readonly lockFilePath: string; + + constructor(filePath: string) { + const dir = path.dirname(filePath); + + // Check if the directory exists, throw error if it doesn't + if (!fs.existsSync(dir)) { + throw new Error(`The directory does not exist: ${dir}`); + } + + this.filePath = filePath; + this.lockFilePath = `${filePath}.lock`; // Define the lock file path + + // If the file doesn't exist, create it + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, JSON.stringify({}, null, 2), 'utf8'); + } + } + + // Method to acquire a lock (create .lock file) + private async acquireLock(): Promise { + while (fs.existsSync(this.lockFilePath)) { + // Wait until the lock file is released + await new Promise(resolve => setTimeout(resolve, 100)); // 100ms delay before retrying + } + // Create the lock file + fs.writeFileSync(this.lockFilePath, ''); + } + + // Method to release the lock (delete .lock file) + private releaseLock(): void { + if (fs.existsSync(this.lockFilePath)) { + fs.unlinkSync(this.lockFilePath); + } + } + + // Read a value by key from the JSON file with a lock + public async readValue(key: string): Promise { + await this.acquireLock(); // Acquire the lock + + try { + if (!fs.existsSync(this.filePath)) return null; + + const data = JSON.parse(fs.readFileSync(this.filePath, 'utf8')); + return data[key] !== undefined ? data[key] : null; + } catch (err: any) { + console.error(`Error reading from JSON file: ${err.message}`); + return null; + } finally { + this.releaseLock(); // Always release the lock after the operation + } + } + + // Write a key-value pair to the JSON file with a lock + public async writeValue(key: string, value: any): Promise { + await this.acquireLock(); // Acquire the lock + + try { + let data: { [key: string]: any } = {}; + + if (fs.existsSync(this.filePath)) { + data = JSON.parse(fs.readFileSync(this.filePath, 'utf8')); + } + + // Update the key with the new value + data[key] = value; + + fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2), 'utf8'); + return true; + } catch (err: any) { + console.error(`Error writing to JSON file: ${err.message}`); + return false; + } finally { + this.releaseLock(); // Always release the lock after the operation + } + } + + // Remove a key-value pair from the JSON file with a lock + public async removeValue(key: string): Promise { + await this.acquireLock(); // Acquire the lock + + try { + if (!fs.existsSync(this.filePath)) return false; + + const data = JSON.parse(fs.readFileSync(this.filePath, 'utf8')); + if (data[key] !== undefined) { + delete data[key]; + fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2), 'utf8'); + return true; + } + return false; + } catch (err: any) { + console.error(`Error removing key from JSON file: ${err.message}`); + return false; + } finally { + this.releaseLock(); // Always release the lock after the operation + } + } + + // Reset the JSON file by clearing all data with a lock + public async resetFile(): Promise { + await this.acquireLock(); // Acquire the lock + + try { + fs.writeFileSync(this.filePath, JSON.stringify({}, null, 2), 'utf8'); + return true; + } catch (err: any) { + console.error(`Error resetting JSON file: ${err.message}`); + return false; + } finally { + this.releaseLock(); // Always release the lock after the operation + } + } +} diff --git a/User/src/helpers/memory_manager.ts b/User/src/helpers/memory_manager.ts new file mode 100644 index 0000000..93e93b2 --- /dev/null +++ b/User/src/helpers/memory_manager.ts @@ -0,0 +1,47 @@ +import { v4 as uuidv4 } from 'uuid'; +import { JsonManager } from './json_manager'; + +export class MemoryManager extends JsonManager { + constructor(filePath: string) { + super(filePath); // Call the parent constructor to ensure file initialization + } + + // Generate a new unique GUID and ensure it doesn't already exist in the file + private generateUniqueGuid(): Promise { + const generate = async (): Promise => { + const guid = uuidv4(); + const value = await this.readValue(guid); + if (value === null) { + return guid; + } + return generate(); + }; + return generate(); + } + + // Store meta information with a unique GUID as the key + public async storeMetaInformation(metaInfo: any): Promise { + const guid = await this.generateUniqueGuid(); + const success = await this.writeValue(guid, metaInfo); + if (success) { + return guid; // Return the unique GUID for future reference + } else { + throw new Error('Failed to store meta information.'); + } + } + + // Retrieve meta information using the GUID + public retrieveMetaInformation(guid: string): Promise { + return this.readValue(guid); + } + + // Update meta information by merging new data into existing data + public async updateMetaInformation(guid: string, newMetaInfo: any): Promise { + return await this.writeValue(guid, newMetaInfo); + } + + // Remove meta information using the GUID + public removeMetaInformation(guid: string): Promise { + return this.removeValue(guid); + } +} diff --git a/User/src/helpers/queue_manager.ts b/User/src/helpers/queue_manager.ts new file mode 100644 index 0000000..cc3aeb0 --- /dev/null +++ b/User/src/helpers/queue_manager.ts @@ -0,0 +1,137 @@ +import fs from 'fs'; +import path from 'path'; + +export class QueueManager { + private readonly filePath: string; + private readonly lockFilePath: string; + private queue: T[]; + private readonly compareFn: (a: T, b: T) => boolean; // Comparison function + + constructor(filePath: string, compareFn: (a: T, b: T) => boolean) { + this.filePath = filePath; + this.lockFilePath = `${filePath}.lock`; // Define the lock file path + this.queue = []; + this.compareFn = compareFn; + + const dir = path.dirname(filePath); + + // Check if the directory exists, throw error if it doesn't + if (!fs.existsSync(dir)) { + throw new Error(`The directory does not exist: ${dir}`); + } + + // If the file doesn't exist, create it + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, JSON.stringify([], null, 2), 'utf8'); + } + } + + // Method to acquire a lock (create .lock file) + private acquireLock(): void { + while (fs.existsSync(this.lockFilePath)) { + // Wait until the lock file is released + this.sleepSync(100); // 100ms delay before retrying + } + // Create the lock file + fs.writeFileSync(this.lockFilePath, ''); + } + + // Sleep function to simulate delay for locking mechanism + private sleepSync(ms: number): void { + const start = Date.now(); + while (Date.now() - start < ms) { + // busy wait + } + } + + // Method to release the lock (delete .lock file) + private releaseLock(): void { + if (fs.existsSync(this.lockFilePath)) { + fs.unlinkSync(this.lockFilePath); + } + } + + // Load the queue from the JSON file + loadQueue(): void { + this.acquireLock(); // Acquire the lock + + try { + const fileData = fs.readFileSync(this.filePath, 'utf8'); + this.queue = JSON.parse(fileData) || []; + } catch (err) { + // If the file doesn't exist or is invalid, start with an empty queue + this.queue = []; + } finally { + this.releaseLock(); // Release the lock + } + } + + // Save the queue back to the JSON file + saveQueue(): void { + this.acquireLock(); // Acquire the lock + + try { + fs.writeFileSync(this.filePath, JSON.stringify(this.queue, null, 2), 'utf8'); + } finally { + this.releaseLock(); // Release the lock + } + } + + // Enqueue: Add an item to the end of the queue if it doesn't already exist + enqueue(item: T): void { + this.loadQueue(); // Ensure we load the latest queue + + // Check if the item already exists in the queue + const exists = this.queue.some(existingItem => this.compareFn(existingItem, item)); + + console.log(this.queue); + + if (!exists) { + this.queue.push(item); + this.saveQueue(); // Save the updated queue + } else { + console.log('Item already exists in the queue. Skipping enqueue.'); + } + } + + // Dequeue: Remove an item from the front of the queue + dequeue(): T | null { + this.loadQueue(); // Ensure we load the latest queue + if (this.queue.length === 0) { + return null; // Queue is empty + } + const item = this.queue.shift() as T; // Remove the first item + this.saveQueue(); // Save the updated queue + return item; + } + + // Peek: Get the item at the front of the queue without removing it + peek(): T | null { + this.loadQueue(); // Ensure we load the latest queue + return this.queue.length > 0 ? this.queue[0] : null; + } + + // Check if the queue is empty + isEmpty(): boolean { + this.loadQueue(); // Ensure we load the latest queue + return this.queue.length === 0; + } + + // Get the length of the queue + length(): number { + this.loadQueue(); // Ensure we load the latest queue + return this.queue.length; + } + + // Clear the entire queue + clearQueue(): void { + this.acquireLock(); // Acquire the lock + + try { + this.queue = []; // Clear the queue + fs.writeFileSync(this.filePath, JSON.stringify(this.queue, null, 2), 'utf8'); + } finally { + this.releaseLock(); // Release the lock + } + } +} diff --git a/User/src/helpers/task_scheduler.ts b/User/src/helpers/task_scheduler.ts new file mode 100644 index 0000000..e250c1d --- /dev/null +++ b/User/src/helpers/task_scheduler.ts @@ -0,0 +1,76 @@ +import { JsonManager } from './json_manager'; // Assuming you have this class for managing user/memory +import { WindowManager } from './window_manager'; // For managing app navigation +import { UdpClient } from '../network/udp/udp_client'; + +export class TaskScheduler { + private applicationInfo: JsonManager; + private windowManager: WindowManager; + private appStarted: boolean = false; + private intervalIds: NodeJS.Timeout[] = []; // Array to store interval IDs + + constructor(applicationInfo: JsonManager, windowManager: WindowManager) { + this.applicationInfo = applicationInfo; + this.windowManager = windowManager; + } + + // Function to schedule the UC check task with dynamic UDP client creation + public startUCCheck(udpPort: number, okPage: string, errorPage: string, interval: number = 30000) { + const intervalId = setInterval(async () => { + try { + const udpClient = new UdpClient(udpPort); // Create UdpClient with the provided port + const aliveClients = await udpClient.getAliveClients(); + const storedIp = await this.applicationInfo.readValue('serverIp'); + const foundClient = aliveClients.length > 0; + + if (foundClient) { + const ipAddress = aliveClients[0]; // Just using the first alive client + + if (!storedIp || storedIp !== ipAddress) { + await this.applicationInfo.writeValue('serverIp', ipAddress); + if (!this.appStarted) { + await this.windowManager.changeContent(okPage); + } + this.appStarted = true; + } else if (!this.appStarted) { + await this.windowManager.changeContent(okPage); + this.appStarted = true; + } + } else { + await this.windowManager.changeContent(errorPage); + } + } catch (err) { + console.error('Error checking UC:', err); + await this.windowManager.changeContent(errorPage); + } + }, interval); + + this.intervalIds.push(intervalId); + } + + // Function to schedule the IP lookup task, storing the active addresses in memory + public async startUserIPLookup(udpPort: number, interval: number = 30000) { + const serverIp = await this.applicationInfo.readValue('serverIp'); // Ensure it's awaited if it's an async function + const intervalId = setInterval(async () => { + try { + const udpClient = new UdpClient(udpPort); // Create a UDP client with the provided port + const activeIPs = await udpClient.getAliveClients(); // Get the list of active IPs + + // Filter out the serverIp from the list of active clients + const filteredIPs = activeIPs.filter(ip => ip !== serverIp); + + // Save the filtered IPs to 'users_ip' + await this.applicationInfo.writeValue('users_ip', filteredIPs); + } catch (err) { + console.error('Error during user IP lookup:', err); + } + }, interval); + + this.intervalIds.push(intervalId); // Store the interval ID + } + + // Function to stop all tasks (UC check, user worker, etc.) + public stopAllTasks() { + this.intervalIds.forEach(clearInterval); // Clear all intervals + this.intervalIds = []; // Reset the array + } +} diff --git a/User/src/helpers/tcp_communicator.ts b/User/src/helpers/tcp_communicator.ts new file mode 100644 index 0000000..ac85298 --- /dev/null +++ b/User/src/helpers/tcp_communicator.ts @@ -0,0 +1,81 @@ +import { TcpClient } from "../network/tcp/tcp_client"; +import { ParsedMessage } from "../network/message_handler"; + +export class TcpCommunicator { + private readonly ip: string; + private readonly port: number; + private tcpClient: TcpClient | null = null; + private lastResult: ParsedMessage | null = null; + + constructor(ip: string, port: number) { + this.ip = ip; + this.port = port; + } + + async connect(): Promise { + this.tcpClient = new TcpClient(this.port); + this.tcpClient.openSocket(this.ip); + return this.tcpClient.isSocketConnected(); + } + + async sendMessage(operationCode: string, metaInfo?: { [key: string]: any }, fileContent?: Buffer): Promise { + if (!this.tcpClient || !this.tcpClient.isSocketConnected()) return false; + + // Wait until the AES key is set before sending the message + return new Promise((resolve) => { + const idWaitForAes = setInterval(async () => { + if (this.tcpClient?.isAesKeySet()) { + clearInterval(idWaitForAes); + + // Send the message once AES key is set + const status = await this.tcpClient!.sendMessage(operationCode, metaInfo, fileContent); + + if (status) { + await this.waitForResponse(); + } + + resolve(status); + } + }, 100); + }); + } + + getLastResult(): ParsedMessage | null { + const message = this.lastResult; + this.lastResult = null; + return message; + } + + hasResponseArrived(): boolean { + if(!this.tcpClient) return false; + return this.lastResult !== null; + } + + private waitForResponse(): Promise { + return new Promise((resolve, reject) => { + if (!this.tcpClient) { + reject(); + } + + // Start interval for waiting for the response + const responseInterval = setInterval(() => { + if (!this.tcpClient?.isSocketConnected()) { + clearInterval(responseInterval); + resolve(); + } + + if (this.tcpClient?.isMessageReceived()) { + this.lastResult = this.tcpClient.getLastResult(); + clearInterval(responseInterval); // Stop checking once we have a response + resolve(); + } + }, 100); // Check every 100 milliseconds + }); + } + + async disconnect(): Promise { + if (!this.tcpClient || !this.tcpClient.isSocketConnected()) return true; + this.tcpClient.closeSocket(); + return true; + } +} diff --git a/User/src/helpers/users_info_fetcher.ts b/User/src/helpers/users_info_fetcher.ts new file mode 100644 index 0000000..a4827f3 --- /dev/null +++ b/User/src/helpers/users_info_fetcher.ts @@ -0,0 +1,111 @@ +import { JsonManager } from "./json_manager"; +import { MemoryManager } from "./memory_manager"; +import { operationCodes } from "../network/operation_codes"; +import { TcpCommunicator } from "./tcp_communicator"; + +export class UsersInfoFetcher { + private applicationInfo: JsonManager; + private memoryManager: MemoryManager; + private tcpCommunicator: TcpCommunicator | null = null; + private readonly clientPort: number; + private memoryId: string; + private readonly activeUsersKey: string; + + constructor(applicationInfoPath: string, memoryManagerPath: string, clientPort: number) { + this.applicationInfo = new JsonManager(applicationInfoPath); + this.memoryManager = new MemoryManager(memoryManagerPath); + this.memoryId = ''; + this.clientPort = clientPort; + this.tcpCommunicator = null; + this.activeUsersKey = 'active_users_info'; + } + + // Method to start checking user info periodically (every minute) + async start(): Promise { + setInterval(async () => { + await this.initialize(); // Re-run every minute + }, 60000); // 1 minute interval + } + + // Initialize and fetch user IPs and process users info + private async initialize() { + const usersIps = await this.applicationInfo.readValue('users_ip'); + if (!usersIps) { + console.error('No IP addresses found in users_ip'); + return; + } + + // Ensure active_users_info exists in the memory + this.memoryId = await this.applicationInfo.readValue(this.activeUsersKey); + if (!this.memoryId) { + this.memoryId = await this.memoryManager.storeMetaInformation([]); + await this.applicationInfo.writeValue(this.activeUsersKey, this.memoryId); + } + + // Check user information + await this.checkUsersInfo(usersIps); + } + + // Check user info from the list of IPs + private async checkUsersInfo(usersIps: string[]) { + let usersInfo: Array<{ ip: string, user_info: any }> = []; // Array to store IP and user_info objects + + for (const ip of usersIps) { + this.tcpCommunicator = new TcpCommunicator(ip, this.clientPort); + if (!await this.tcpCommunicator.connect()) { + console.error(`Failed to open connection for IP: ${ip}`); + continue; + } + + // Wait for the response for 10 seconds + const response = await this.waitForResponse(); + console.log(response); + + // If a response is received and is successful, append it to usersInfo + if (response && response.metaInfo) { + console.log(`Response received from ${ip}:`, response); + usersInfo.push({ + ip: ip, + user_info: response.metaInfo // Push the IP and user_info object into the array + }); + } + + await this.tcpCommunicator.disconnect(); + } + + await this.updateActiveUsers(usersInfo); // Update active users information in the memory + } + + // Send the message once and wait for the response for a specific timeout + private async waitForResponse(timeout: number = 10000): Promise { + // Send the message once + const status = await this.tcpCommunicator?.sendMessage(operationCodes.GET_USER_INFORMATION); + if (!status) { + return null; + } + + // Wait for the response until the timeout + return new Promise((resolve) => { + const startTime = Date.now(); + + const checkResponseInterval = setInterval(async () => { + // Check if the message has been received + if (this.tcpCommunicator?.hasResponseArrived()) { + clearInterval(checkResponseInterval); + resolve(this.tcpCommunicator?.getLastResult()); // Return the response once it arrives + } + + // If the timeout is reached, stop checking and resolve with null + if (Date.now() - startTime > timeout) { + clearInterval(checkResponseInterval); + resolve(null); + } + }, 100); // Check every 100ms if the response has arrived + }); + } + + // Update active users information in the memory + private async updateActiveUsers(userInfo: any[]) { + await this.memoryManager.updateMetaInformation(this.memoryId, userInfo); // Update active users in memory + } +} diff --git a/User/src/helpers/window_manager.ts b/User/src/helpers/window_manager.ts new file mode 100644 index 0000000..c950b4e --- /dev/null +++ b/User/src/helpers/window_manager.ts @@ -0,0 +1,92 @@ +import { BrowserWindow, dialog, shell } from 'electron'; +import fs from 'fs'; +import path from 'path'; + +export class WindowManager { + private readonly mainWindow: BrowserWindow; + private readonly pathToPagesDir: string; + + constructor(mainWindow: BrowserWindow, pathToPagesDir: string) { + this.pathToPagesDir = pathToPagesDir; + this.mainWindow = mainWindow; + } + + // Show an alert dialog + async showAlert(message: string): Promise { + if (this.mainWindow) { + await dialog.showMessageBox(this.mainWindow, { + type: 'info', + title: 'Alert', + message: message, + buttons: ['OK'], + }); + } else { + console.error('Main window is not available.'); + } + } + + // Change the content of the current window to load a new HTML file + async changeContent(destination: string): Promise { + if (this.mainWindow) { + try { + const destinationPath = path.join(this.pathToPagesDir, `${destination}.html`); + console.log(`Navigating to: ${destinationPath}`); + + // Load the destination HTML file into the main window + await this.mainWindow.loadFile(destinationPath); + console.log(`Navigated to ${destination}`); + } catch (error) { + console.error('Error changing content:', error); + throw error; // Pass the error back to the render process + } + } else { + console.error('Main window is not available.'); + } + } + + // New method to select a directory + async selectDirectory(): Promise { + const result = await dialog.showOpenDialog(this.mainWindow, { + properties: ['openDirectory'], // Only allow selecting directories + }); + + // If the user cancels, result.filePaths will be an empty array + if (result.filePaths && result.filePaths.length > 0) { + return result.filePaths[0]; // Return the selected directory path + } else { + console.log('No directory selected.'); + return undefined; // Return undefined if no directory was selected + } + } + + async showFileInExplorer(filePath: string): Promise { + if (filePath && fs.existsSync(filePath)) { + try { + // Use Electron's shell module to show the file in the explorer + shell.showItemInFolder(filePath); + console.log(`Opened file explorer for: ${filePath}`); + } catch (error: any) { + console.error(`Error showing file in explorer: ${error.message}`); + } + } else { + console.error('File path is undefined or does not exist.'); + } + } + + // New method to open the file explorer and choose a file + async selectFile(): Promise { + const result = await dialog.showOpenDialog(this.mainWindow, { + properties: ['openFile'], // Allow selecting a file + filters: [ + { name: 'All Files', extensions: ['*'] } // Optionally filter for specific file types + ] + }); + + if (result.filePaths && result.filePaths.length > 0) { + return result.filePaths[0]; // Return the selected file path + } else { + console.log('No file selected.'); + return undefined; // Return undefined if no file was selected + } + } +} diff --git a/User/src/helpers/worker_manager.ts b/User/src/helpers/worker_manager.ts new file mode 100644 index 0000000..40950b2 --- /dev/null +++ b/User/src/helpers/worker_manager.ts @@ -0,0 +1,155 @@ +import { Worker } from 'worker_threads'; +import path from 'path'; +import {WindowManager} from "./window_manager"; + +export class WorkerManager { + private readonly pathToWorkerDir: string; + private windowManager: WindowManager + private workers: Worker[]; // Array to store running workers + + constructor(pathToWorkerDir: string, windowManager: WindowManager) { + this.pathToWorkerDir = pathToWorkerDir; + this.windowManager = windowManager; + this.workers = []; // Initialize the array to store workers + } + + // Start the Connection Pool Worker + async startWatchersWorker(memoryManagerPath: string, applicationInfoPath: string): Promise { + return new Promise((resolve, reject) => { + const worker = new Worker(path.join(this.pathToWorkerDir, 'watcher_worker.js'), { + workerData: { memoryManagerPath, applicationInfoPath }, // Pass the port to the worker + }); + + this.workers.push(worker); // Store the worker reference + + worker.on('message', (data) => { + console.log('Connection Pool Worker message:', data); + }); + + worker.on('error', (err) => { + console.error('Connection Pool Worker error:', err); + worker.terminate(); + this.removeWorker(worker); + reject(err); // Reject the promise if there's an error + }); + + worker.on('exit', (code) => { + console.log(`Connection Pool Worker exited with code ${code}`); + this.removeWorker(worker); // Remove worker reference when it exits + resolve(); // Resolve when the worker exits cleanly + }); + }); + } + + // Start the Servers Worker (UDP and TCP servers) + async startServersWorker(host: string, udpPort: number, tcpPort: number): Promise { + return new Promise((resolve, reject) => { + const worker = new Worker(path.join(this.pathToWorkerDir, 'servers_worker.js'), { + workerData: { HOST: host, USER_UDP_PORT: udpPort, USER_TCP_PORT: tcpPort }, // Pass host and ports to the worker + }); + + this.workers.push(worker); // Store the worker reference + + worker.on('message', (data) => { + console.log('Servers Worker message:', data); + }); + + worker.on('error', (err) => { + console.error('Servers Worker error:', err); + worker.terminate(); + this.removeWorker(worker); + reject(err); // Reject the promise if there's an error + }); + + worker.on('exit', (code) => { + console.log(`Servers Worker exited with code ${code}`); + this.removeWorker(worker); // Remove worker reference when it exits + resolve(); // Resolve when the worker exits cleanly + }); + }); + } + + // Start the Users Info Worker + async startResourceCoordinatorWorker(usersConfigPath: string, applicationInfoPath: string, memoryManagerPath: string, queueManagerPath: string, tcpPort: number): Promise { + return new Promise((resolve, reject) => { + const worker = new Worker(path.join(this.pathToWorkerDir, 'resource_coordinator_worker.js'), { + workerData: { + usersConfigPath, + applicationInfoPath, + memoryManagerPath, + queueManagerPath, + tcpPort + }, // Pass necessary parameters to the worker + }); + + this.workers.push(worker); // Store the worker reference + + worker.on('message', (data) => { + console.log('Users Info Worker message:', data); + }); + + worker.on('error', (err) => { + console.error('Users Info Worker error:', err); + worker.terminate(); + this.removeWorker(worker); + reject(err); // Reject the promise if there's an error + }); + + worker.on('exit', (code) => { + console.log(`Users Info Worker exited with code ${code}`); + this.removeWorker(worker); // Remove worker reference when it exits + resolve(); // Resolve when the worker exits cleanly + }); + }); + } + + // Start the Backup Retrieval Worker + async startBackupRetrievalWorker( + userConfigPath: string, + applicationInfoPath: string, + clientPort: number, + destinationPath: string + ): Promise { + return new Promise((resolve, reject) => { + const worker = new Worker(path.join(this.pathToWorkerDir, 'backup_retrieval_worker.js'), { + workerData: { userConfigPath, applicationInfoPath, clientPort, destinationPath }, // Pass parameters to the worker + }); + + this.workers.push(worker); // Store the worker reference + + worker.on('message', (data) => { + console.log('Backup Retrieval Worker message:', data); + this.windowManager.changeContent('main_menu'); + this.windowManager.showAlert(data.message); + }); + + worker.on('error', (err) => { + console.error('Backup Retrieval Worker error:', err); + worker.terminate(); + this.removeWorker(worker); + reject(err); // Reject the promise if there's an error + }); + + worker.on('exit', (code) => { + console.log(`Backup Retrieval Worker exited with code ${code}`); + this.removeWorker(worker); // Remove worker reference when it exits + resolve(); // Resolve when the worker exits cleanly + }); + }); + } + + // Close all running workers + closeAllWorkers(): void { + console.log('Terminating all running workers...'); + this.workers.forEach(worker => worker.terminate()); // Terminate each worker + this.workers = []; // Clear the array after terminating all workers + } + + // Helper method to remove a worker from the workers array when it exits + private removeWorker(worker: Worker): void { + const index = this.workers.indexOf(worker); + if (index > -1) { + this.workers.splice(index, 1); // Remove the worker from the array + } + } +} diff --git a/User/src/interfaces/file_item_task.ts b/User/src/interfaces/file_item_task.ts new file mode 100644 index 0000000..6934379 --- /dev/null +++ b/User/src/interfaces/file_item_task.ts @@ -0,0 +1,8 @@ +export interface FileItemTask { + ip: string; + path: string; + userName: string; +} + +export const compareFnFileItemTask = (task1: FileItemTask, task2: FileItemTask) => + task1.ip === task2.ip && task1.path === task2.path; diff --git a/User/src/interfaces/pool_request.ts b/User/src/interfaces/pool_request.ts new file mode 100644 index 0000000..73e9302 --- /dev/null +++ b/User/src/interfaces/pool_request.ts @@ -0,0 +1,15 @@ +interface PoolRequest { + type: PoolOperation; // Renamed to PoolOperation + clientId: string; // Add clientId to the request + data: PoolDataBundle; +} + +interface PoolDataBundle { + port?: number; + ip?: string; + operationCode?: string; // Keep operationCode here + metaInfo?: { [key: string]: any }; + fileContent?: Buffer; +} + +type PoolOperation = 'open' | 'send' | 'close'; // Define the allowed PoolOperations diff --git a/User/src/interfaces/registered_client.ts b/User/src/interfaces/registered_client.ts new file mode 100644 index 0000000..71ac44b --- /dev/null +++ b/User/src/interfaces/registered_client.ts @@ -0,0 +1,5 @@ +interface RegisteredClient { + id: string; + ip: string; + port: number; +} \ No newline at end of file diff --git a/User/src/main/aes_encrypt.js b/User/src/main/aes_encrypt.js deleted file mode 100644 index aaa5abe..0000000 --- a/User/src/main/aes_encrypt.js +++ /dev/null @@ -1,115 +0,0 @@ -const crypto = require('crypto'); -const fs = require('fs'); -const path = require('path'); - -async function readKeyFromFile(filePath) { - try { - await fs.promises.access(filePath, fs.constants.F_OK); - return fs.promises.readFile(filePath); // Use asynchronous readFile - } catch (error) { - console.error('Error reading key file:', error); - return null; - } -} - -// Paths to the key files remain the same -const IV_FILE_PATH = path.join(__dirname, '..', '..', 'iv.key'); -const SECRET_KEY_FILE_PATH = path.join(__dirname, '..', '..', 'secret.key'); - -async function encryptFileInPlace(filePath) { - // Await the resolution of these promises - const IV = await readKeyFromFile(IV_FILE_PATH); - const SECRET_KEY = await readKeyFromFile(SECRET_KEY_FILE_PATH); - - if (!IV || !SECRET_KEY) { - throw new Error('Failed to load IV or Secret Key'); - } - - const tempEncryptedFilePath = filePath + '.enc'; // Temporary encrypted file - return new Promise((resolve, reject) => { - const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(SECRET_KEY), IV); - const input = fs.createReadStream(filePath); - const output = fs.createWriteStream(tempEncryptedFilePath); - - input.pipe(cipher).pipe(output); - - output.on('finish', () => { - fs.rename(tempEncryptedFilePath, filePath, (err) => { - if (err) reject(err); - else resolve('File encrypted successfully and replaced original.'); - }); - }); - output.on('error', reject); - }); -} - -async function decryptFileInPlace(filePath) { - // Await the resolution of these promises - const IV = await readKeyFromFile(IV_FILE_PATH); - const SECRET_KEY = await readKeyFromFile(SECRET_KEY_FILE_PATH); - - if (!IV || !SECRET_KEY) { - throw new Error('Failed to load IV or Secret Key'); - } - - const tempDecryptedFilePath = filePath + '.dec'; // Temporary decrypted file - return new Promise((resolve, reject) => { - const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(SECRET_KEY), IV); - const input = fs.createReadStream(filePath); - const output = fs.createWriteStream(tempDecryptedFilePath); - - input.pipe(decipher).pipe(output); - - output.on('finish', () => { - fs.rename(tempDecryptedFilePath, filePath, (err) => { - if (err) reject(err); - else resolve('File decrypted successfully and replaced original.'); - }); - }); - output.on('error', reject); - }); -} - -function cryptForKey(content, key, decrypt = false) { - const algorithm = 'aes-256-ctr'; - const secretKey = crypto.createHash('sha256').update(String(key)).digest('base64').substr(0, 32); - let cipher; - - if (decrypt) { - cipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.alloc(16, 0)); // Using a zeroed IV for CTR - } else { - cipher = crypto.createCipheriv(algorithm, secretKey, Buffer.alloc(16, 0)); - } - - return Buffer.concat([cipher.update(content), cipher.final()]); -} - -// Encrypts a file in place with a given key -async function encryptFileWithKey(filePath, key) { - try { - const fileContent = await fs.promises.readFile(filePath); - const encryptedContent = cryptForKey(fileContent, key, false); - await fs.promises.writeFile(filePath, encryptedContent); - console.log(`File encrypted successfully: ${filePath}`); - } catch (error) { - console.error(`Error encrypting file: ${error.message}`); - } -} - -async function decryptFileWithKey(filePath, key) { - try { - const fileContent = await fs.promises.readFile(filePath); - const decryptedContent = cryptForKey(fileContent, key, true); - await fs.promises.writeFile(filePath, decryptedContent); - console.log(`File decrypted successfully: ${filePath}`); - } catch (error) { - console.error(`Error decrypting file: ${error.message}`); - } -} - -module.exports = { - encryptFileInPlace, - decryptFileInPlace, - encryptFileWithKey, - decryptFileWithKey, -} diff --git a/User/src/main/main.js b/User/src/main/main.js deleted file mode 100644 index 3e38464..0000000 --- a/User/src/main/main.js +++ /dev/null @@ -1,422 +0,0 @@ -const {app, BrowserWindow, screen, ipcMain, dialog, shell} = require('electron'); -const fs = require('fs'); -const path = require('path'); -const crypto = require('crypto'); -const {fork} = require('child_process'); - -const {encryptFileInPlace, decryptFileInPlace} = require('./aes_encrypt'); -const {lockFile, unlockFile} = require("../../helpers/lock_mechanism"); -const exec = require("nodemon/lib/config/exec"); - -const isMac = process.platform === 'darwin'; -let html_page = undefined; -let mainWindow = undefined; -let alertWindow = undefined; - -let fetcherProcess = null; -let backupProcess = null; -let externalEndpointsProcess = null; -let sendFileProcess = null; - -const createInitialKeys = () => { - const SECRET_KEY = crypto.randomBytes(32); - const IV = crypto.randomBytes(16); - - const secretKeyPath = path.join(__dirname, '..', '..', 'secret.key'); - const ivPath = path.join(__dirname, '..', '..', 'iv.key'); - - fs.writeFileSync(secretKeyPath, SECRET_KEY); - console.log(`Secret Key saved to ${secretKeyPath}`); - - fs.writeFileSync(ivPath, IV); - console.log(`IV saved to ${ivPath}`); -} - -const checkForServerConnection = async () => { - const pathToIpConfig = path.join(__dirname, '..', '..', 'ipConfig.json'); - - try { - console.log('Checking for server connection'); - await fs.promises.access(pathToIpConfig); - - await lockFile(pathToIpConfig); - await decryptFileInPlace(pathToIpConfig); - const ipConfig = await fs.promises.readFile(pathToIpConfig, 'utf-8'); - const { ip } = JSON.parse(ipConfig); - - const response = await fetch(`http://${ip}/heartbeat`); - - await encryptFileInPlace(pathToIpConfig) - await unlockFile(pathToIpConfig); - return response.ok; - } catch (error) { - console.error("Error:", error); - try{ - fs.unlinkSync(pathToIpConfig); - } - catch{ - - } - return false; - } -}; - -async function runStartupChecks() { - try { - await fs.promises.access(path.join(__dirname, '..', '..', 'secret.key'), fs.constants.F_OK); - await fs.promises.access(path.join(__dirname, '..', '..', 'iv.key'), fs.constants.F_OK); - } catch (err) { - console.error("Startup error detected:", err); - - // Run the npm clean script - await exec('npm run clean', { cwd: path.join(__dirname, '..', '..') }, (error, stdout, stderr) => { - if (error) { - console.error('Error occurred while running npm run clean:', stderr); - return; - } - console.log('npm run clean output:', stdout); - createInitialKeys(); - }); - } -} - -const createMainWindow = (async (title, width, height) => { - mainWindow = new BrowserWindow({ - title: title, - width: width, - height: height, - resizable: false, - icon: path.join(__dirname, '..', 'renderer', 'assets', 'hard-disk.png'), - webPreferences: { - preload: path.join(__dirname, 'preload.js') - } - }); - - await runStartupChecks(); - html_page = await checkForServerConnection() ? 'login.html' : 'ip_submit.html'; - - //mainWindow.setMenu(null); - mainWindow.loadFile(path.join(__dirname, '..', 'renderer', 'html', html_page)) - .then(() => { - console.log('Main window loaded!') - }) - .catch(err => console.error('Failed to load main window:', err)); -}); - -const createAlertWindow = (title, width, height) => { - alertWindow = new BrowserWindow({ - width: width, - height: height, - title: title, - icon: path.join(__dirname, '..', 'renderer', 'assets', 'hard-disk.png'), - resizable: false, - webPreferences: { - nodeIntegration: false, - preload: path.join(__dirname, 'preload.js') - } - }); - - //alertWindow.setMenu(null); - - alertWindow.loadFile(path.join(__dirname, '..', 'renderer', 'html', 'alert_modal.html')).then(() => { - console.log('Alert window loaded!') - }) - .catch(err => console.error('Failed to load alert window:', err)); - - alertWindow.on('closed', () => { - alertWindow = undefined; - }); -} - -function showAlert(message) { - if (alertWindow === undefined) { - const title = 'Alert'; - const mainScreen = screen.getPrimaryDisplay(); - const {width, height} = mainScreen.size; - createAlertWindow(title, width / 4, height / 4); - } - - alertWindow.webContents.once('dom-ready', () => { - alertWindow.webContents.executeJavaScript(`showAlert("${message}")`); - }); -} - -app.whenReady().then(() => { - const title = "Application"; - const mainScreen = screen.getPrimaryDisplay(); - const {width, height} = mainScreen.size; - createMainWindow(title, width / 1.5, height / 1.5); - - app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0) { - createMainWindow(title, width, height); - } - }); -}); - -app.on('before-quit', () => { - if (backupProcess !== null) { - backupProcess.kill(); - } - if (externalEndpointsProcess !== null) { - externalEndpointsProcess.kill(); - } - if (fetcherProcess !== null) { - fetcherProcess.kill(); - } - - if (sendFileProcess !== null) { - sendFileProcess.kill(); - } -}); - -app.on('window-all-closed', () => { - if (!isMac) { - app.quit(); - } -}); - -ipcMain.handle('write-file', async (event, fileName, content) => { - try { - let filePath = path.join(__dirname, '..', '..', fileName); - await lockFile(filePath); - await fs.promises.writeFile(filePath, content); - await encryptFileInPlace(filePath); - await unlockFile(filePath) - console.log(`File successfully written to ${filePath}`); - return {success: true}; - } catch (error) { - console.error('Failed to write file:', error); - return {success: false, error: error.message}; - } -}); - -ipcMain.handle('delete-file', async (event, fileName) => { - try { - let filePath = path.join(__dirname, '..', '..', fileName); - - await fs.promises.unlink(filePath); - - console.log(`File ${filePath} successfully deleted`); - return {success: true}; - } catch (error) { - console.error('Failed to delete file:', error); - return {success: false, error: error.message}; - } -}); - -ipcMain.handle('read-file', async (event, fileName) => { - try { - let filePath = path.join(__dirname, '..', '..', fileName); - await lockFile(filePath); - await decryptFileInPlace(filePath); - const content = await fs.promises.readFile(filePath, 'utf-8'); - - await encryptFileInPlace(filePath); - await unlockFile(filePath); - return {success: true, content}; - } catch (error) { - console.error('Error reading file:', error); - return {success: false, error: error.message}; - } -}); - -ipcMain.handle('change-content', async (event, nextPage) => { - try { - html_page = nextPage; - await mainWindow.loadFile(path.join(__dirname, '..', 'renderer', 'html', nextPage)); - return true; - } catch (error) { - console.error('Error changing content:', error); - return false; - } -}); - -ipcMain.handle('open-dir-dialog', async (event) => { - try { - const result = await dialog.showOpenDialog({ - properties: ['openDirectory'], - }); - - if (result.canceled || result.filePaths.length === 0) { - return {canceled: true} - } - - return result.filePaths[0]; - } catch (error) { - console.error('Error opening file dialog:', error); - return null; - } -}) - -ipcMain.handle('open-json-dir-config', async (event, fileName) => { - try { - const result = await dialog.showOpenDialog({ - properties: ['openDirectory'], - }); - - if (result.canceled || result.filePaths.length === 0) { - return {canceled: true} - } - - const dirPath = result.filePaths[0]; - await fs.promises.writeFile( - path.join(__dirname, '..', '..', fileName), - JSON.stringify({ - path: dirPath - }, null, 2)); - - return true; - } catch (error) { - console.error('Error opening file dialog:', error); - return {error: error.message}; - } -}); - -ipcMain.handle('open-file-dialog', async (event) => { - const result = await dialog.showOpenDialog({ - properties: ['openFile'] - }); - - return result.filePaths[0] || ''; -}); - -ipcMain.handle('check-file-exists', async (event, fileName) => { - try { - const filePath = path.join(__dirname, '..', '..', fileName); - return await fs.promises.access(filePath) - .then(() => true) - .catch(() => false); - } catch (error) { - console.error('Error checking file existence:', error); - throw error; // Propagate the error to the renderer process - } -}); - -ipcMain.handle('show-alert', async (event, message) => { - showAlert(message); -}); - -ipcMain.on('close-alert-window', () => { - if (alertWindow) { - alertWindow.close(); - alertWindow = undefined; - } -}); - -//External processes -ipcMain.handle('start-main-processes', async (event, args) => { - if (!backupProcess) { - backupProcess = fork(path.join(__dirname, '..', '..', 'jobs', 'backup.js'), args, { silent: false }); - backupProcess.on('exit', () => { - backupProcess = null; - }); - backupProcess.on('error', (err) => { - console.log('Backup process error:', err); - }); - } - - if (!fetcherProcess) { - fetcherProcess = fork(path.join(__dirname, '..', '..', 'jobs', 'fetcher.js'), args, { silent: false }); - fetcherProcess.on('exit', () => { - fetcherProcess = null; - }); - fetcherProcess.on('error', (err) => { - console.log('Fetcher process error:', err); - }); - - fetcherProcess.on('message', (message) => { - if (message.type === 'startBackup') { - backupProcess.send({ - type: 'startBackup' - }); - } - }); - } - - if (!externalEndpointsProcess) { - externalEndpointsProcess = fork(path.join(__dirname, '..', '..', 'jobs', 'external_endpoints.js'), args, { silent: false }); - externalEndpointsProcess.on('exit', () => { - externalEndpointsProcess = null; - }); - externalEndpointsProcess.on('error', (err) => { - console.log('External endpoints process error:', err); - }); - } - - return true; // Indicate that the operation has started -}); - -ipcMain.handle('start-send-file-process', async (event, args) => { - if (sendFileProcess === null) { - sendFileProcess = fork(path.join(__dirname, '..', '..', 'jobs', 'send_file.js'), args, {silent: false}); - sendFileProcess.on('exit', () => { - sendFileProcess = null; - }); - } - return true; // Indicate that the operation has started -}); - -ipcMain.handle('kill-before-logout', async(event) =>{ - if (backupProcess !== null) { - backupProcess.kill('SIGINT'); - } - if (externalEndpointsProcess !== null) { - externalEndpointsProcess.kill('SIGINT'); - } - if (fetcherProcess !== null) { - fetcherProcess.kill('SIGINT'); - } - - if (sendFileProcess !== null) { - sendFileProcess.kill('SIGINT'); - } -}) - -ipcMain.handle('decrypt-backup-files', async(event, destPath) => { - console.log('ai intrat in handle') - if(backupProcess != null){ - console.log('esti in process'); - backupProcess.send({ - type: 'decryptBackup', - decryptDestPath: destPath - }); - } -}) - -// Handle showing a file in the system file explorer -ipcMain.handle('show-file-in-explorer', async (event, filePath) => { - try { - // Ensure the file exists before attempting to show it - await fs.promises.access(filePath, fs.constants.F_OK); - shell.showItemInFolder(filePath); // Opens the file explorer and highlights the file - return true; - } catch (error) { - console.error('File does not exist:', error); - return false - } -}); - -// Handle removing a path from 'filesReceived.json' -ipcMain.handle('remove-path-from-received-files', async (event, filePath) => { - try { - const jsonFilePath = path.join(__dirname, '..', '..', 'filesReceived.json'); - await lockFile(jsonFilePath); - await decryptFileInPlace(jsonFilePath); - const data = await fs.promises.readFile(jsonFilePath, 'utf8'); - - const jsonData = JSON.parse(data); - - // Filter out the specified file path - jsonData.receivedFiles = jsonData.receivedFiles.filter(file => file !== filePath); - - await fs.promises.writeFile(jsonFilePath, JSON.stringify(jsonData, null, 2), 'utf8'); - await encryptFileInPlace(jsonFilePath); - await unlockFile(jsonFilePath); - - return true; - } catch (error) { - console.error('Error updating filesReceived.json:', error); - throw new Error('Failed to update received files list.'); - } -}); diff --git a/User/src/main/main.ts b/User/src/main/main.ts new file mode 100644 index 0000000..ba2d507 --- /dev/null +++ b/User/src/main/main.ts @@ -0,0 +1,299 @@ +import {app, BrowserWindow, ipcMain, IpcMainInvokeEvent} from 'electron'; +import path from 'path'; +import { promises as fs } from 'fs'; +import dotenv from 'dotenv'; + +import {WorkerManager} from "../helpers/worker_manager"; +import {DirectoryWatcher} from "../helpers/directory_watcher"; +import {QueueManager} from "../helpers/queue_manager"; +import {compareFnFileItemTask, FileItemTask} from "../interfaces/file_item_task"; +import {TaskScheduler} from "../helpers/task_scheduler"; +import {WindowManager} from "../helpers/window_manager"; +import {JsonManager} from "../helpers/json_manager"; +import {MemoryManager} from "../helpers/memory_manager"; +import {TcpCommunicator} from "../helpers/tcp_communicator"; + +import {operationCodes} from "../network/operation_codes"; + +// Load environment variables +dotenv.config({ path: path.join(__dirname, '..', '..', '.env') }); + +const UDP_PORT = process.env.UDP_PORT ? parseInt(process.env.UDP_PORT) : 41233; +const TCP_PORT = process.env.TCP_PORT ? parseInt(process.env.TCP_PORT) : 41234; +const HOST = process.env.HOST || '0.0.0.0'; + +let mainWindow: BrowserWindow | null = null; +let windowManager: WindowManager | null = null; +let tcpCommunicator: TcpCommunicator | null = null; +let userConfig: JsonManager | null = null; +let applicationInfo: JsonManager | null = null; +let memoryManager: MemoryManager | null = null; +let workerManager: WorkerManager | null = null; +let taskScheduler: TaskScheduler | null = null; +let backupDirectoryManager: DirectoryWatcher | null = null; +let departmentShareManager: DirectoryWatcher | null = null; +let sendFileQueue: QueueManager | null = null; + +const pathToPagesDir = path.join(__dirname, '..', '..', 'render', 'html'); +const pathToWorkerDir = path.join(__dirname, '..', 'workers'); +const pathToJsons = path.join(__dirname, '..', 'json_files'); +const pathToClientsBackups = path.join(__dirname, '..', 'backups'); + +async function cleanupAndExit() { + // Stop all workers + if (workerManager) { + console.log('Terminating all workers...'); + workerManager.closeAllWorkers(); + } + + // Reset memory + if (memoryManager) { + await memoryManager.resetFile(); + } + + // Close watchers + if (backupDirectoryManager) { + console.log('Stopping backup directory watcher...'); + backupDirectoryManager.closeWatcher(); // Add this method to DirectoryWatcher to close the watcher + } + + if (departmentShareManager) { + console.log('Stopping department directory watcher...'); + departmentShareManager.closeWatcher(); // Add this method to DirectoryWatcher to close the watcher + } + + if(taskScheduler){ + console.log('Stopping all tasks...'); + taskScheduler.stopAllTasks() + } + + if(workerManager){ + console.log('Terminating all workers...'); + workerManager.closeAllWorkers() + } + + console.log('Cleanup complete, exiting application.'); + app.quit(); // This will properly close the application +} + +async function ensureDirectoryExists(dirPath: string): Promise { + try { + await fs.access(dirPath); + } catch (err) { + // If the directory doesn't exist, create it + await fs.mkdir(dirPath, { recursive: true }); + console.log(`Directory created: ${dirPath}`); + } +} + +app.whenReady().then(async () => { + const title = 'Application'; + const mainScreen = require('electron').screen.getPrimaryDisplay(); + const { width, height } = mainScreen.size; + + mainWindow = new BrowserWindow({ + title, + width: width / 1.5, + height: height / 1.5, + resizable: false, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + nodeIntegration: false, + contextIsolation: true, + }, + }); + + await ensureDirectoryExists(pathToJsons); + await ensureDirectoryExists(pathToClientsBackups); + + windowManager = new WindowManager(mainWindow, pathToPagesDir); + userConfig = new JsonManager(path.join(pathToJsons, 'userConfig.json')); + applicationInfo = new JsonManager(path.join(pathToJsons, 'application.json')); + memoryManager = new MemoryManager(path.join(pathToJsons, 'memory.json')); + sendFileQueue = new QueueManager(path.join(pathToJsons, 'sendFileTasks.json'), compareFnFileItemTask); + + taskScheduler = new TaskScheduler(applicationInfo, windowManager); + workerManager = new WorkerManager(pathToWorkerDir, windowManager); + + await userConfig.writeValue('app_type', 'client'); + await applicationInfo.writeValue('users_ip', []); + await memoryManager.resetFile(); + + workerManager.startWatchersWorker(path.join(pathToJsons, 'memory.json'), path.join(pathToJsons, 'application.json')); + workerManager.startServersWorker(HOST, UDP_PORT, TCP_PORT); + workerManager.startResourceCoordinatorWorker( + path.join(pathToJsons, 'userConfig.json'), + path.join(pathToJsons, 'application.json'), + path.join(pathToJsons, 'memory.json'), + path.join(pathToJsons, 'sendFileTasks.json'), + TCP_PORT + ); + + + taskScheduler.startUCCheck(UDP_PORT, 'login', 'uc_not_found'); + taskScheduler.startUserIPLookup(UDP_PORT); + + registerIPCHandlers(); + + await windowManager.changeContent('welcome'); +}); + +app.on('window-all-closed', async () => { + console.log('All windows closed, starting cleanup...'); + await cleanupAndExit(); // Call cleanup when all windows are closed +}); + +// Catch CTRL+C (SIGINT) and clean up resources +process.on('SIGINT', async () => { + console.log('CTRL+C pressed, starting cleanup...'); + await cleanupAndExit(); // Call cleanup on SIGINT +}); + +app.on('before-quit', async () => { + console.log('Application is quitting, starting cleanup...'); + await cleanupAndExit(); // Call cleanup before app quit +}); + +// Register IPC handlers +function registerIPCHandlers() { + // Window Manager IPC Handlers + ipcMain.handle('show-alert', async (event: IpcMainInvokeEvent, message: string) => { + if (!windowManager) throw new Error('WindowManager is not initialized.'); + await windowManager.showAlert(message); + }); + + ipcMain.handle('change-content', async (_event: IpcMainInvokeEvent, destination: string) => { + if (!windowManager) throw new Error('WindowManager is not initialized.'); + await windowManager.changeContent(destination); + }); + + ipcMain.handle('select-directory', async (_event: IpcMainInvokeEvent) => { + if (!windowManager) throw new Error('WindowManager is not initialized.'); + return await windowManager.selectDirectory(); + }); + + ipcMain.handle('select-file', async (_event: IpcMainInvokeEvent) => { + if (!windowManager) throw new Error('WindowManager is not initialized.'); + return await windowManager.selectFile(); + }); + + ipcMain.handle('show-file-in-explorer', async (_event: IpcMainInvokeEvent, path: string) => { + if (!windowManager) throw new Error('WindowManager is not initialized.'); + return await windowManager.showFileInExplorer(path); + }); + + // TcpMethods IPC Handlers + ipcMain.handle('open-socket', async (_event: IpcMainInvokeEvent) => { + if (!applicationInfo) throw new Error('TcpMethods is not initialized.'); + + const serverIp = await applicationInfo.readValue('serverIp'); + if (!serverIp) return; + + tcpCommunicator = new TcpCommunicator(serverIp, TCP_PORT); + return await tcpCommunicator.connect() + }); + + ipcMain.handle('send-message', async (_event: IpcMainInvokeEvent, operationCode: string, metaInfo?: { [key: string]: any }, fileContent?: Buffer) => { + if (!tcpCommunicator) throw new Error('TcpMethods is not initialized.'); + return await tcpCommunicator.sendMessage(operationCode, metaInfo, fileContent); + }); + + ipcMain.handle('has-response-arrived', async (_event: IpcMainInvokeEvent) => { + if (!tcpCommunicator) throw new Error('TcpMethods is not initialized.'); + return tcpCommunicator.hasResponseArrived(); + }); + + ipcMain.handle('close-socket', async (_event: IpcMainInvokeEvent) => { + if (!tcpCommunicator) throw new Error('TcpMethods is not initialized.'); + return await tcpCommunicator.disconnect(); + }); + + ipcMain.handle('get-last-result', async (_event: IpcMainInvokeEvent) => { + if (!tcpCommunicator) throw new Error('TcpMethods is not initialized.'); + return tcpCommunicator.getLastResult(); + }); + + ipcMain.handle('get-operation-codes', () => { + return operationCodes; + }); + + // UserConfig IPC Handlers + ipcMain.handle('read-user-json-files', async (_event: IpcMainInvokeEvent, key: string) => { + if (!userConfig) throw new Error('UserConfig is not initialized.'); + return await userConfig.readValue(key); + }); + + ipcMain.handle('write-user-json-files', async (_event: IpcMainInvokeEvent, key: string, value: any) => { + if (!userConfig) throw new Error('UserConfig is not initialized.'); + return userConfig.writeValue(key, value); + }); + + ipcMain.handle('reset-user-json-files', async () => { + if (!userConfig) throw new Error('UserConfig is not initialized.'); + return userConfig.resetFile(); + }); + + ipcMain.handle('remove-user-json-files-key', async (_event: IpcMainInvokeEvent, key: string) => { + if (!userConfig) throw new Error('UserConfig is not initialized.'); + return userConfig.removeValue(key); + }); + + // ApplicationPreferences IPC Handlers + ipcMain.handle('read-application-json-files', async (_event: IpcMainInvokeEvent, key: string) => { + if (!applicationInfo) throw new Error('ApplicationInfo is not initialized.'); + return await applicationInfo.readValue(key); + }); + + ipcMain.handle('write-application-json-files', async (_event: IpcMainInvokeEvent, key: string, value: any) => { + if (!applicationInfo) throw new Error('ApplicationInfo is not initialized.'); + return applicationInfo.writeValue(key, value); + }); + + ipcMain.handle('reset-application-json-files', async () => { + if (!applicationInfo) throw new Error('ApplicationInfo is not initialized.'); + return applicationInfo.resetFile(); + }); + + ipcMain.handle('remove-application-json-files-key', async (_event: IpcMainInvokeEvent, key: string) => { + if (!applicationInfo) throw new Error('ApplicationInfo is not initialized.'); + return applicationInfo.removeValue(key); + }); + + // Memory IPC Handlers + ipcMain.handle('memory-create-entry', async () => { + if (!memoryManager) throw new Error('MemoryManager is not initialized.'); + return memoryManager.storeMetaInformation({}); + }); + + ipcMain.handle('memory-read-entry', async (_event: IpcMainInvokeEvent, id: string) => { + if (!memoryManager) throw new Error('MemoryManager is not initialized.'); + return memoryManager.retrieveMetaInformation(id); + }); + + ipcMain.handle('memory-update-entry', async (_event: IpcMainInvokeEvent, id: string, data: any) => { + if (!memoryManager) throw new Error('MemoryManager is not initialized.'); + return memoryManager.updateMetaInformation(id, data); + }); + + ipcMain.handle('memory-remove-entry', async (_event: IpcMainInvokeEvent, id: string) => { + if (!memoryManager) throw new Error('MemoryManager is not initialized.'); + return memoryManager.removeMetaInformation(id); + }); + + // Queue IPC Handlers + ipcMain.handle('add-task-to-send-file-queue', async (event: IpcMainInvokeEvent, task: FileItemTask) => { + if (!sendFileQueue) throw new Error('SendFileQueue is not initialized.'); + sendFileQueue.enqueue(task); + }); + + // BackupRetrievalWorker IPC Handler + ipcMain.handle('start-backup-retrieval', async (_event: IpcMainInvokeEvent, destinationPath: string) => { + if (!workerManager) throw new Error('WorkerManager is not initialized.'); + return workerManager.startBackupRetrievalWorker( + path.join(pathToJsons, 'userConfig.json'), + path.join(pathToJsons, 'application.json'), + TCP_PORT, + destinationPath + ); + }); +} \ No newline at end of file diff --git a/User/src/main/preload.js b/User/src/main/preload.js deleted file mode 100644 index a1ac5ca..0000000 --- a/User/src/main/preload.js +++ /dev/null @@ -1,21 +0,0 @@ -const {contextBridge, ipcRenderer} = require('electron'); - -contextBridge.exposeInMainWorld('electronAPI', { - writeFile: (fileName, content) => ipcRenderer.invoke('write-file', fileName, content), - readFile: (fileName) => ipcRenderer.invoke('read-file', fileName), - deleteFile: (fileName) => ipcRenderer.invoke('delete-file', fileName), - changeContent: (nextPage) => ipcRenderer.invoke('change-content', nextPage), - showAlert: (message) => ipcRenderer.invoke('show-alert', message), - closeAlertWindow: () => ipcRenderer.send('close-alert-window'), - openJsonDirConfigDialog: (fileName) => ipcRenderer.invoke('open-json-dir-config', fileName), - openDirDialog: () => ipcRenderer.invoke('open-dir-dialog'), - openFileDialog: () => ipcRenderer.invoke('open-file-dialog'), - checkFileExists: (fileName) => ipcRenderer.invoke('check-file-exists', fileName), - - startMainProcesses: async (args) => ipcRenderer.invoke('start-main-processes', args), - killBeforeLogout: async() => ipcRenderer.invoke('kill-before-logout'), - decryptFiles: async(destPath) => ipcRenderer.invoke('decrypt-backup-files', destPath), - - showFileInExplorer: (filePath) => ipcRenderer.invoke('show-file-in-explorer', filePath), - removePathFromReceivedFiles: (filePath) => ipcRenderer.invoke('remove-path-from-received-files', filePath) -}); \ No newline at end of file diff --git a/User/src/main/preload.ts b/User/src/main/preload.ts new file mode 100644 index 0000000..0aa21cc --- /dev/null +++ b/User/src/main/preload.ts @@ -0,0 +1,43 @@ +import { contextBridge, ipcRenderer } from 'electron'; +import {FileItemTask} from "../interfaces/file_item_task"; + +contextBridge.exposeInMainWorld('electronAPI', { + // UserConfig methods + readUserConfig: (key: string): Promise => ipcRenderer.invoke('read-user-json-files', key), + writeUserConfig: (key: string, value: any): Promise => ipcRenderer.invoke('write-user-json-files', key, value), + removeUserConfig: (key: string) : Promise => ipcRenderer.invoke('remove-user-json-files', key), + resetUserConfig: (): Promise => ipcRenderer.invoke('reset-user-json-files'), + + // ApplicationPreferences methods + readApplicationInfo: (key: string): Promise => ipcRenderer.invoke('read-application-json-files', key), + writeApplicationInfo: (key: string, value: any): Promise => ipcRenderer.invoke('write-application-json-files', key, value), + removeApplicationInfo: (key: string) : Promise => ipcRenderer.invoke('remove-application-preferences', key), + resetApplicationInfo: (): Promise => ipcRenderer.invoke('reset-application-json-files'), + + // UcCommunication methods + openUcSocket: (): Promise => ipcRenderer.invoke('open-socket'), + sendUcMessage: (operationCode: string, metaInfo: any, fileContent: any): Promise => ipcRenderer.invoke('send-message', operationCode, metaInfo, fileContent), + closeUcSocket: (): Promise => ipcRenderer.invoke('close-socket'), + hasResponseArrived: (): Promise => ipcRenderer.invoke('has-response-arrived'), + getLastUcResult: (): Promise => ipcRenderer.invoke('get-last-result'), + getOperationsCodes: (): Promise<{ data: { [key: string]: string } }> => ipcRenderer.invoke('get-operation-codes'), + + // MemoryManager methods + createMemoryEntry: (): Promise => ipcRenderer.invoke('memory-create-entry'), + readMemoryEntry: (id: string): Promise => ipcRenderer.invoke('memory-read-entry', id), + updateMemoryEntry: (id: string, data: any): Promise => ipcRenderer.invoke('memory-update-entry', id, data), + removeMemoryEntry: (id: string): Promise => ipcRenderer.invoke('memory-remove-entry', id), + + // UI methods + showAlert: (message: string): Promise => ipcRenderer.invoke('show-alert', message), + changeContent: (destination: string): Promise => ipcRenderer.invoke('change-content', destination), + selectDirectory: (): Promise => ipcRenderer.invoke('select-directory'), + selectFile: (): Promise => ipcRenderer.invoke('select-file'), + showFileInExplorer: (path: string): Promise => ipcRenderer.invoke('show-file-in-explorer', path), + + // Queue methods + addTaskToSendFileQueue: (task: FileItemTask): Promise => ipcRenderer.invoke('add-task-to-send-file-queue', task), + + // BackupRetrievalWorker + startBackupRetrieval: (destinationPath: string): Promise => ipcRenderer.invoke('start-backup-retrieval', destinationPath) +}); diff --git a/User/src/network/connection_manager.ts b/User/src/network/connection_manager.ts new file mode 100644 index 0000000..f7f29f8 --- /dev/null +++ b/User/src/network/connection_manager.ts @@ -0,0 +1,46 @@ +import { SocketCommunicatorBase } from "./socket_communicator/socket_communicator_base"; + +interface Connection { + communicator: SocketCommunicatorBase; +} + +export class ConnectionManager { + private readonly connections: { [key: string]: Connection }; + + constructor() { + this.connections = {}; + } + + // Adds a new communicator, keyed by both IP and port + addConnection(ip: string, port: number, communicator: SocketCommunicatorBase): void { + const key = `${ip}:${port}`; + + // Store the communicator along with the client's public and private keys + this.connections[key] = { + communicator + }; + + console.log(`Added communicator for ${ip}:${port}, generated public and private keys.`); + } + + // Removes a communicator based on IP and port + removeCommunicator(ip: string, port: number): void { + const key = `${ip}:${port}`; + if (this.connections[key]) { + delete this.connections[key]; + console.log(`Removed communicator for ${ip}:${port}`); + } + } + + // Retrieves a communicator based on IP and port + getCommunicator(ip: string, port: number): SocketCommunicatorBase | null { + const key = `${ip}:${port}`; + return this.connections[key] ? this.connections[key].communicator : null; + } + + // Checks if a communicator exists for a given IP and port + communicatorExists(ip: string, port: number): boolean { + const key = `${ip}:${port}`; + return this.connections[key] !== undefined; + } +} diff --git a/User/src/network/message_handler.ts b/User/src/network/message_handler.ts new file mode 100644 index 0000000..6583a1d --- /dev/null +++ b/User/src/network/message_handler.ts @@ -0,0 +1,65 @@ +export interface ParsedMessage { + operationCode: string; + metaInfo?: { [key: string]: any }; + fileContent?: Buffer; +} + +export class MessageHandler { + // Format the message with operationCode, guid, metaInfo, and fileContent (Base64 for fileContent) + static formatMessage( + operationCode: string, + metaInfo?: { [key: string]: any }, + fileContent?: Buffer + ): string { + let message = `${operationCode}\n`; // First part: operationCode and guid + + if (metaInfo && Object.keys(metaInfo).length > 0) { + message += `${JSON.stringify(metaInfo)}\n`; // Add metaInfo + } + + if (fileContent && fileContent.length > 0) { + message += fileContent.toString('base64'); // Convert buffer to Base64 for fileContent + } + + return message; + } + + // Parse the incoming message (convert Base64 back to Buffer if fileContent is present) + static parseMessage(msg: string): ParsedMessage { + const parts = msg.split('\n'); // Split by \n (operationCode, metaInfo, and fileContent are on separate lines) + + // First part should always be the operation code + const operationCode = parts[0]?.trim(); + if (!operationCode) { + throw new Error('Missing operation code in the message'); + } + + let metaInfo: { [key: string]: any } | undefined = undefined; + let fileContent: Buffer | undefined = undefined; + + // Parse the metaInfo (JSON object) if present + if (parts[1]) { + try { + metaInfo = JSON.parse(parts[1].trim()); + } catch (err) { + console.error('Invalid metaInfo JSON format:', err); + } + } + + // Convert Base64 string back to Buffer for fileContent if present + if (parts[2]) { + fileContent = Buffer.from(parts[2].trim(), 'base64'); + } + + return { + operationCode, + metaInfo, + fileContent, + }; + } + + // Validate if the parsed message contains an operation code + static validateMessage(parsedMessage: ParsedMessage | null): boolean { + return !!parsedMessage?.operationCode; + } +} \ No newline at end of file diff --git a/User/src/network/network.ts b/User/src/network/network.ts new file mode 100644 index 0000000..8f65956 --- /dev/null +++ b/User/src/network/network.ts @@ -0,0 +1,2 @@ +export { UdpClient} from './udp/udp_client'; +export { TcpClient } from './tcp/tcp_client' \ No newline at end of file diff --git a/User/src/network/operation_codes.ts b/User/src/network/operation_codes.ts new file mode 100644 index 0000000..8c1c5a4 --- /dev/null +++ b/User/src/network/operation_codes.ts @@ -0,0 +1,39 @@ +export let operationCodes = { + // General Operations + HEARTBEAT: 'HEARTBEAT', + ALIVE: 'ALIVE', + SET_PUBLIC_KEY: 'SET_PUBLIC_KEY', + SET_AES_KEY: 'SET_AES_KEY', + RESET_DATABASE: 'RESET_DATABASE', + OK: 'OK', + ERR: 'ERR', + END: 'END', + UNKNOWN_COMMAND: 'UNKNOWN_COMMAND', + + // Auth Operations + LOGIN: 'LOGIN', + SIGN_UP: 'SIGN_UP', + RESET_PASSWORD: 'RESET_PASSWORD', + + FIND_BY_EMAIL: 'FIND_BY_EMAIL', + MODIFY_USER: 'MODIFY_USER', + + GET_DEPARTMENTS: 'GET_DEPARTMENTS', + CREATE_DEPARTMENT: 'CREATE_DEPARTMENT', + MODIFY_DEPARTMENT: 'MODIFY_DEPARTMENT', + DELETE_DEPARTMENT: 'DELETE_DEPARTMENT', + + GET_USERS: 'GET_USERS', + FIND_KEY_BY_USER_ID: 'FIND_KEY_BY_USER_ID', + + GET_USER_INFORMATION: 'GET_USER_INFORMATION', + CLEAR_BACKUP: 'CLEAR_BACKUP', + BACKUP_FILE: 'BACKUP_FILE', + SHARE_FILE: 'SHARE_FILE', + CLEAR_DEPARTMENT: 'CLEAR_DEPARTMENT', + DEPARTMENT_FILE: 'DEPARTMENT_FILE', + IS_BACKUP_CREATED: 'IS_BACKUP_CREATED', + GET_BACKUP_STRUCTURE: 'GET_BACKUP_STRUCTURE', + REQ_FILE_FROM_BACKUP: 'REQ_FILE_FROM_BACKUP', + DECRYPT_AND_SAVE_OLD_BACKUP_FILE: 'DECRYPT_AND_SAVE_OLD_BACKUP_FILE', +}; \ No newline at end of file diff --git a/User/src/network/operations_base/operation_base.ts b/User/src/network/operations_base/operation_base.ts new file mode 100644 index 0000000..d0d62da --- /dev/null +++ b/User/src/network/operations_base/operation_base.ts @@ -0,0 +1,43 @@ +import { ParsedMessage } from '../message_handler'; +import { OperationHandler } from './operation_handler'; +import { OperationPlugin } from './operation_plugin'; + +export abstract class OperationBase implements OperationPlugin { + // Shared operation codes + public static readonly operationCodes = { + OK: 'OK', + ERR: 'ERR', + END: 'END', + }; + + // Default handler for OK operation + public static handleOk(parsedMessage: ParsedMessage): ParsedMessage { + console.log('OK operation received'); + return parsedMessage; // Typically, you would just acknowledge with OK, returning as-is + } + + // Default handler for ERR operation + public static handleErr(parsedMessage: ParsedMessage): ParsedMessage { + console.log('ERR operation received: ', parsedMessage.metaInfo?.message || 'No error details provided'); + return parsedMessage; // Typically, you would log the error and return + } + + // Default handler for END operation + public static handleEnd(parsedMessage: ParsedMessage): ParsedMessage { + console.log('END operation received'); + return { + operationCode: OperationBase.operationCodes.END, + metaInfo: { message: 'Connection ended.' }, + }; + } + + // Register the common OK, ERR, and END handlers + public static registerCommonOperations(operationHandler: OperationHandler): void { + operationHandler.registerHandler(OperationBase.operationCodes.OK, OperationBase.handleOk); + operationHandler.registerHandler(OperationBase.operationCodes.ERR, OperationBase.handleErr); + operationHandler.registerHandler(OperationBase.operationCodes.END, OperationBase.handleEnd); + } + + // Abstract register method that will be implemented by subclasses + public abstract register(operationHandler: OperationHandler): void; +} diff --git a/User/src/network/operations_base/operation_handler.ts b/User/src/network/operations_base/operation_handler.ts new file mode 100644 index 0000000..76c00ce --- /dev/null +++ b/User/src/network/operations_base/operation_handler.ts @@ -0,0 +1,58 @@ +// operation_handler.ts +import { ParsedMessage, MessageHandler } from '../message_handler'; +import { OperationPlugin } from './operation_plugin'; + +type OperationHandlerFunction = (parsedMessage: ParsedMessage) => ParsedMessage; + +export class OperationHandler { + private static instance: OperationHandler; + private handlers: { [operationCode: string]: OperationHandlerFunction } = {}; + + private constructor() { + // Register only the unknown command handler on initialization + this.registerHandler('UNKNOWN_COMMAND', this.handleUnknownCommand); + } + + // Singleton instance + public static getInstance(): OperationHandler { + if (!OperationHandler.instance) { + OperationHandler.instance = new OperationHandler(); + } + return OperationHandler.instance; + } + + // Register a handler for a specific operation code + public registerHandler(operationCode: string, handler: OperationHandlerFunction): void { + this.handlers[operationCode] = handler; + } + + // Handle operation request + public handleOperation(rawMessage: string): ParsedMessage { + // Parse and validate the message + const parsedMessage = MessageHandler.parseMessage(rawMessage); + if (!parsedMessage || !MessageHandler.validateMessage(parsedMessage)) { + return this.handleUnknownCommand(parsedMessage); + } + + // Dispatch the handler for the given operation code + const handler = this.handlers[parsedMessage.operationCode]; + if (handler) { + return handler(parsedMessage); + } else { + return this.handleUnknownCommand(parsedMessage); + } + } + + // Default handler for unknown commands + private handleUnknownCommand(parsedMessage: ParsedMessage): ParsedMessage { + return { + operationCode: 'UNKNOWN_COMMAND', + metaInfo: { message: 'Unknown command received.' }, + }; + } + + // Plugin system: Load plugins to register handlers + public loadPlugin(plugin: OperationPlugin): void { + plugin.register(this); + } +} diff --git a/User/src/network/operations_base/operation_plugin.ts b/User/src/network/operations_base/operation_plugin.ts new file mode 100644 index 0000000..6b11a29 --- /dev/null +++ b/User/src/network/operations_base/operation_plugin.ts @@ -0,0 +1,6 @@ +// operation_plugin.ts +import { OperationHandler } from './operation_handler'; + +export interface OperationPlugin { + register(operationHandler: OperationHandler): void; +} diff --git a/User/src/network/operations_custom/general_operations.ts b/User/src/network/operations_custom/general_operations.ts new file mode 100644 index 0000000..d9d83a2 --- /dev/null +++ b/User/src/network/operations_custom/general_operations.ts @@ -0,0 +1,80 @@ +import { ParsedMessage } from '../message_handler'; +import { OperationBase } from '../operations_base/operation_base'; +import { OperationHandler } from '../operations_base/operation_handler'; +import os from 'node:os'; + +export class GeneralOperations extends OperationBase { + public static readonly operationCodes = { + ...OperationBase.operationCodes, // Inherit common operation codes (OK, ERR, END) + HEARTBEAT: 'HEARTBEAT', + ALIVE: 'ALIVE', + SET_PUBLIC_KEY: 'SET_PUBLIC_KEY', + SET_AES_KEY: 'SET_AES_KEY', + }; + + // Handle heartbeat operation + public static handleHeartbeat(): ParsedMessage { + const networkInterfaces = os.networkInterfaces(); + let ipAddress = 'Unknown'; + + for (const iface of Object.values(networkInterfaces)) { + // @ts-ignore + for (const address of iface) { + if (address.family === 'IPv4' && !address.internal) { + ipAddress = address.address; + break; + } + } + if (ipAddress !== 'Unknown') break; + } + + return { + operationCode: GeneralOperations.operationCodes.ALIVE, + metaInfo: { ipAddress }, + }; + } + + // Handle public key exchange + public static handlePublicKey(parsedMessage: ParsedMessage): ParsedMessage { + const clientPublicKey = parsedMessage.metaInfo?.publicKey; + if (clientPublicKey) { + return { + operationCode: GeneralOperations.operationCodes.SET_PUBLIC_KEY, + metaInfo: { publicKey: clientPublicKey }, + }; + } else { + return { + operationCode: GeneralOperations.operationCodes.ERR, + metaInfo: { message: 'No public key provided.' }, + }; + } + } + + // Handle AES key exchange + public static handleAESKey(parsedMessage: ParsedMessage): ParsedMessage { + const aesKey = parsedMessage.metaInfo?.aesKey; + const aesIv = parsedMessage.metaInfo?.aesIv; + if (aesKey && aesIv) { + return { + operationCode: GeneralOperations.operationCodes.SET_AES_KEY, + metaInfo: { aesKey: aesKey, aesIv: aesIv }, + }; + } else { + return { + operationCode: GeneralOperations.operationCodes.ERR, + metaInfo: { message: 'No AES key provided.' }, + }; + } + } + + // Register general operations with the OperationHandler + public register(operationHandler: OperationHandler): void { + // Register specific handlers for the general operations + operationHandler.registerHandler(GeneralOperations.operationCodes.HEARTBEAT, GeneralOperations.handleHeartbeat); + operationHandler.registerHandler(GeneralOperations.operationCodes.SET_PUBLIC_KEY, GeneralOperations.handlePublicKey); + operationHandler.registerHandler(GeneralOperations.operationCodes.SET_AES_KEY, GeneralOperations.handleAESKey); // Register AES key handler + + // Register common operations inherited from the base class + OperationBase.registerCommonOperations(operationHandler); + } +} diff --git a/User/src/network/operations_custom/user_to_user_operations.ts b/User/src/network/operations_custom/user_to_user_operations.ts new file mode 100644 index 0000000..9bf7b88 --- /dev/null +++ b/User/src/network/operations_custom/user_to_user_operations.ts @@ -0,0 +1,500 @@ +import { ParsedMessage } from '../message_handler'; +import { OperationBase } from '../operations_base/operation_base'; +import { OperationHandler } from '../operations_base/operation_handler'; +import path from 'path'; +import fs from 'fs'; +import { FileEncryptor } from '../../helpers/file_encryptor'; + +const LOCK_FILE_EXTENSION = '.lock'; + +export class UserToUserOperations extends OperationBase { + public static readonly operationCodes = { + ...OperationBase.operationCodes, // Inherit common operation codes (OK, ERR, END) + GET_USER_INFORMATION: 'GET_USER_INFORMATION', + BACKUP_FILE: 'BACKUP_FILE', + CLEAR_BACKUP: 'CLEAR_BACKUP', + SHARE_FILE: 'SHARE_FILE', + CLEAR_DEPARTMENT: 'CLEAR_DEPARTMENT', + DEPARTMENT_FILE: 'DEPARTMENT_FILE', + IS_BACKUP_CREATED: 'IS_BACKUP_CREATED', + GET_BACKUP_STRUCTURE: 'GET_BACKUP_STRUCTURE', + REQ_FILE_FROM_BACKUP: 'REQ_FILE_FROM_BACKUP', + DECRYPT_AND_SAVE_OLD_BACKUP_FILE: 'DECRYPT_AND_SAVE_OLD_BACKUP_FILE', + }; + + // Utility function to pause execution (sleep) + static sleep(ms: number): void { + const start = Date.now(); + while (Date.now() - start < ms) { + // Busy wait loop (not optimal but fine for this short duration) + } + } + + // Read JSON file with a lock mechanism + static readJsonSync(filePath: string): any { + const lockFilePath = `${filePath}${LOCK_FILE_EXTENSION}`; + const absolutePath = path.resolve(filePath); + + try { + // Loop until the lock file is removed by another process + while (fs.existsSync(lockFilePath)) { + console.log(`Waiting for lock file to be released: ${lockFilePath}`); + UserToUserOperations.sleep(100); + } + + // Create lock file to signal this process is working on the file + fs.writeFileSync(lockFilePath, ''); // Create the lock file + + // Read and parse the JSON file + const fileContents = fs.readFileSync(absolutePath, 'utf-8'); + const parsedJson = JSON.parse(fileContents); + + // Once processing is done, delete the lock file + fs.unlinkSync(lockFilePath); // Remove the lock file + + return parsedJson; // Return the parsed JSON data + } catch (error) { + console.error(`Error reading or parsing JSON from ${filePath}:`, error); + + // Ensure the lock file is removed even in case of an error + if (fs.existsSync(lockFilePath)) { + fs.unlinkSync(lockFilePath); + } + + return null; // Return null or throw error based on preference + } + } + + // Handle user information retrieval + public static handleGetUserInformation(parsedMessage: ParsedMessage): ParsedMessage { + const pathToUserJson = path.join(__dirname, '..', '..', 'json_files', 'userConfig.json'); + const userInfo = UserToUserOperations.readJsonSync(pathToUserJson); + + if (userInfo) { + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: userInfo.user_info, + }; + } else { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Error fetching user information.' }, + }; + } + } + + // Handle file reception and saving + public static handleBackupFile(parsedMessage: ParsedMessage): ParsedMessage { + // Ensure metaInfo and fileContent are available + if (!parsedMessage.metaInfo || !parsedMessage.fileContent) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing file content or meta information.' }, + }; + } + + const { userName, relativeFilePath } = parsedMessage.metaInfo; + + if (!userName || !relativeFilePath) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing user name or file path information.' }, + }; + } + + // Base directory where backups will be stored + const baseBackupDir = path.join(__dirname, '..', '..', 'backups'); + + // Full path where the file will be stored (under the user's directory) + const fullFilePath = path.join(baseBackupDir, userName, relativeFilePath); + + try { + // Ensure the directory structure exists (create directories if they don't exist) + const dirPath = path.dirname(fullFilePath); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + + // Write the file content to the correct path + fs.writeFileSync(fullFilePath, Buffer.from(parsedMessage.fileContent as Buffer), 'base64'); + + console.log(`File saved successfully: ${fullFilePath}`); + + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: { message: `File saved successfully: ${relativeFilePath}` }, + }; + } catch (error: any) { + console.error(`Error saving file: ${error.message}`); + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: `Error saving file: ${error.message}` }, + }; + } + } + + // Handle clearing all backups for a user + public static handleClearBackup(parsedMessage: ParsedMessage): ParsedMessage { + // Ensure the userName is available in metaInfo + if (!parsedMessage.metaInfo || !parsedMessage.metaInfo.userName) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing user name in meta information.' }, + }; + } + + const { userName } = parsedMessage.metaInfo; + + // Base directory where backups are stored + const baseBackupDir = path.join(__dirname, '..', '..', 'backups'); + const userBackupDir = path.join(baseBackupDir, userName); + + try { + // Check if the user's backup directory exists + if (fs.existsSync(userBackupDir)) { + // Recursively delete the user's backup directory + fs.rmSync(userBackupDir, { recursive: true, force: true }); + console.log(`Backup cleared successfully for user: ${userName}`); + + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: { message: `Backup cleared successfully for user: ${userName}` }, + }; + } else { + console.warn(`Backup directory not found for user: ${userName}`); + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: `Backup directory not found for user: ${userName}` }, + }; + } + } catch (error: any) { + console.error(`Error clearing backup for user: ${userName} - ${error.message}`); + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: `Error clearing backup for user: ${userName}` }, + }; + } + } + + // Handle sharing file operation + public static handleShareFile(parsedMessage: ParsedMessage): ParsedMessage { + // Ensure metaInfo and fileContent are available + if (!parsedMessage.metaInfo || !parsedMessage.fileContent) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing file content or meta information.' }, + }; + } + + const { userName, relativeFilePath } = parsedMessage.metaInfo; + + if (!userName || !relativeFilePath) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing user name or file path information.' }, + }; + } + + // Path to the application.json to read the shareDirectory field + const pathToApplicationJson = path.join(__dirname, '..', '..', 'json_files', 'application.json'); + const appInfo = UserToUserOperations.readJsonSync(pathToApplicationJson); + + // Corrected the condition + if (!appInfo || !appInfo.shareDirectory) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Error retrieving share directory from application.json.' }, + }; + } + + // Get the share directory path + const shareDirectory = appInfo.shareDirectory.path; + + // Full path where the file will be stored (under the user's directory in the shared folder) + const fullFilePath = path.join(shareDirectory, userName, relativeFilePath); + + try { + // Ensure the directory structure exists (create directories if they don't exist) + const dirPath = path.dirname(fullFilePath); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + + // Write the file content to the correct path + fs.writeFileSync(fullFilePath, Buffer.from(parsedMessage.fileContent as Buffer), 'base64'); + + console.log(`File shared successfully: ${fullFilePath}`); + + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: { message: `File shared successfully: ${relativeFilePath}` }, + }; + } catch (error: any) { + console.error(`Error sharing file: ${error.message}`); + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: `Error sharing file: ${error.message}` }, + }; + } + } + + public static handleClearDepartment(parsedMessage: ParsedMessage): ParsedMessage { + // Ensure the userName is available in metaInfo + if (!parsedMessage.metaInfo || !parsedMessage.metaInfo.userName) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing user name in meta information.' }, + }; + } + + const { userName } = parsedMessage.metaInfo; + + const pathToApplicationJson = path.join(__dirname, '..', '..', 'json_files', 'application.json'); + const appInfo = UserToUserOperations.readJsonSync(pathToApplicationJson); + + // Corrected the condition + if (!appInfo || !appInfo.departmentDirectory) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Error retrieving share directory from application.json.' }, + }; + } + + // Get the share directory path + const baseDepartmentDir = appInfo.departmentDirectory.path; + const userDepartmentDir = path.join(baseDepartmentDir, userName); + + try { + // Check if the user's backup directory exists + if (fs.existsSync(userDepartmentDir)) { + // Recursively delete the user's backup directory + fs.rmSync(userDepartmentDir, { recursive: true, force: true }); + console.log(`Backup cleared successfully for user: ${userName}`); + + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: { message: `Backup cleared successfully for user: ${userName}`}, + }; + } else { + console.warn(`Backup directory not found for user: ${userName}`); + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: `Backup directory not found for user: ${userName}`}, + }; + } + } catch (error: any) { + console.error(`Error clearing backup for user: ${userName} - ${error.message}`); + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: `Error clearing backup for user: ${userName}`}, + }; + } + } + + public static handleDepartmentFile(parsedMessage: ParsedMessage): ParsedMessage { + // Ensure metaInfo and fileContent are available + if (!parsedMessage.metaInfo || !parsedMessage.fileContent) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing file content or meta information.' }, + }; + } + + const { userName, relativeFilePath } = parsedMessage.metaInfo; + + if (!userName || !relativeFilePath) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing user name or file path information.' }, + }; + } + + // Path to the application.json to read the shareDirectory field + const pathToApplicationJson = path.join(__dirname, '..', '..', 'json_files', 'application.json'); + const appInfo = UserToUserOperations.readJsonSync(pathToApplicationJson); + + // Corrected the condition + if (!appInfo || !appInfo.departmentDirectory) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Error retrieving share directory from application.json.' }, + }; + } + + // Get the share directory path + const departmentDirectory = appInfo.departmentDirectory.path; + + // Full path where the file will be stored (under the user's directory in the shared folder) + const fullFilePath = path.join(departmentDirectory, userName, relativeFilePath); + + try { + // Ensure the directory structure exists (create directories if they don't exist) + const dirPath = path.dirname(fullFilePath); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + + // Write the file content to the correct path + fs.writeFileSync(fullFilePath, Buffer.from(parsedMessage.fileContent as Buffer), 'base64'); + + console.log(`File shared successfully: ${fullFilePath}`); + + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: { message: `File shared successfully: ${relativeFilePath}`}, + }; + } catch (error: any) { + console.error(`Error sharing file: ${error.message}`); + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: {message: `Error sharing file: ${error.message}`}, + }; + } + } + + // Check if a backup has been created for a user + public static handleIsBackupCreated(parsedMessage: ParsedMessage): ParsedMessage { + if(!parsedMessage.metaInfo) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing metaInfo.' }, + }; + } + + const {name} = parsedMessage.metaInfo; + if (!name) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing name in meta information.' }, + }; + } + + const baseBackupDir = path.join(__dirname, '..', '..', 'backups'); + const userBackupDir = path.join(baseBackupDir, name); + + console.log(userBackupDir); + + const exists = fs.existsSync(userBackupDir); + + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: { backupExists: exists }, + }; + } + + // Get the structure of the backup directory for a user + public static handleGetBackupStructure(parsedMessage: ParsedMessage): ParsedMessage { + if(!parsedMessage.metaInfo) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing metaInfo.' }, + }; + } + + const {name} = parsedMessage.metaInfo; + if (!name) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing name in meta information.' }, + }; + } + + const baseBackupDir = path.join(__dirname, '..', '..', 'backups'); + const userBackupDir = path.join(baseBackupDir, name); + + if (!fs.existsSync(userBackupDir)) { + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: { structure: {} }, // Return empty structure if directory doesn't exist + }; + } + + const fileStructure = UserToUserOperations.buildDirectoryStructure(userBackupDir); + + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: { structure: fileStructure }, + }; + } + + // Build the directory structure recursively + private static buildDirectoryStructure(directoryPath: string): any { + const structure: any = {}; + const files = fs.readdirSync(directoryPath); + + for (const file of files) { + const filePath = path.join(directoryPath, file); + const stats = fs.statSync(filePath); + + if (stats.isDirectory()) { + structure[file] = UserToUserOperations.buildDirectoryStructure(filePath); // Recursive for subdirectories + } else { + structure[file] = path.relative(directoryPath, filePath); + } + } + + return structure; + } + + // Handle file request from backup directory + public static handleReqFileFromBackup(parsedMessage: ParsedMessage): ParsedMessage { + if(!parsedMessage.metaInfo) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing metaInfo.' }, + }; + } + + const { name, relativeFilePath } = parsedMessage.metaInfo; + + if (!name || !relativeFilePath) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'Missing name or file path in meta information.' }, + }; + } + + const baseBackupDir = path.join(__dirname, '..', '..', 'backups'); + const userBackupDir = path.join(baseBackupDir, name); + const fullFilePath = path.join(userBackupDir, relativeFilePath); + + if (!fs.existsSync(fullFilePath)) { + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: 'File not found in backup.' }, + }; + } + + try { + const fileContent = fs.readFileSync(fullFilePath, 'base64'); + return { + operationCode: UserToUserOperations.operationCodes.OK, + metaInfo: { relativeFilePath }, + fileContent: Buffer.from(fileContent, 'base64') + }; + } catch (error: any) { + console.error(`Error reading file: ${error.message}`); + return { + operationCode: UserToUserOperations.operationCodes.ERR, + metaInfo: { message: `Error reading file: ${error.message}`}, + }; + } + } + + // Register user-to-user operations with the OperationHandler + public register(operationHandler: OperationHandler): void { + // Register specific handlers for user-to-user operations + operationHandler.registerHandler(UserToUserOperations.operationCodes.GET_USER_INFORMATION, UserToUserOperations.handleGetUserInformation); + operationHandler.registerHandler(UserToUserOperations.operationCodes.BACKUP_FILE, UserToUserOperations.handleBackupFile); + operationHandler.registerHandler(UserToUserOperations.operationCodes.CLEAR_BACKUP, UserToUserOperations.handleClearBackup); + operationHandler.registerHandler(UserToUserOperations.operationCodes.SHARE_FILE, UserToUserOperations.handleShareFile); + operationHandler.registerHandler(UserToUserOperations.operationCodes.DEPARTMENT_FILE, UserToUserOperations.handleDepartmentFile); + operationHandler.registerHandler(UserToUserOperations.operationCodes.CLEAR_DEPARTMENT, UserToUserOperations.handleClearDepartment); + operationHandler.registerHandler(UserToUserOperations.operationCodes.IS_BACKUP_CREATED, UserToUserOperations.handleIsBackupCreated); + operationHandler.registerHandler(UserToUserOperations.operationCodes.GET_BACKUP_STRUCTURE, UserToUserOperations.handleGetBackupStructure); + operationHandler.registerHandler(UserToUserOperations.operationCodes.REQ_FILE_FROM_BACKUP, UserToUserOperations.handleReqFileFromBackup); + + // Register common operations inherited from the base class + OperationBase.registerCommonOperations(operationHandler); + } +} diff --git a/User/src/network/socket_communicator/socket_communicator_base.ts b/User/src/network/socket_communicator/socket_communicator_base.ts new file mode 100644 index 0000000..cbe8e7d --- /dev/null +++ b/User/src/network/socket_communicator/socket_communicator_base.ts @@ -0,0 +1,21 @@ +import { ParsedMessage } from '../message_handler'; +import { OperationHandler } from '../operations_base/operation_handler'; + +export abstract class SocketCommunicatorBase { + protected readonly ip: string; + protected readonly port: number; + protected readonly operationHandler: OperationHandler; + protected handlerResult: ParsedMessage | null; + + protected constructor(ip: string, port: number, operationHandler: OperationHandler) { + this.ip = ip; + this.port = port; + this.operationHandler = operationHandler + this.handlerResult = null; + } + + // Getter for the handler result + getHandlerResult(): ParsedMessage | null { + return this.handlerResult; + } +} diff --git a/User/src/network/socket_communicator/tcp_client_communicator.ts b/User/src/network/socket_communicator/tcp_client_communicator.ts new file mode 100644 index 0000000..7f262e6 --- /dev/null +++ b/User/src/network/socket_communicator/tcp_client_communicator.ts @@ -0,0 +1,167 @@ +import { Socket } from 'net'; +import { createCipheriv, createDecipheriv } from 'crypto'; +import { MessageHandler, ParsedMessage } from '../message_handler'; +import { SocketCommunicatorBase } from './socket_communicator_base'; +import { OperationHandler } from '../operations_base/operation_handler'; +import {constants, publicDecrypt} from "node:crypto"; +import {operationCodes} from "../operation_codes"; + +const END_OF_MESSAGE = ''; // Define a unique marker for end of message + +export class TcpClientCommunicator extends SocketCommunicatorBase { + private readonly socket: Socket; + private aesKey: Buffer | null; + private aesIv: Buffer | null; + private messageBuffer: string; + private serverPublicKey: string | null; + private isAesKeySetFlag: boolean; + + constructor(socket: Socket, ip: string, port: number, operationHandler: OperationHandler) { + super(ip, port, operationHandler); + this.socket = socket; + this.aesKey = null; + this.aesIv = null; + this.serverPublicKey = null; + this.messageBuffer = ''; // Buffer for message reassembly + this.isAesKeySetFlag = false; + } + + setServerPublicKey(publicKey: string): void { + this.serverPublicKey = publicKey; + console.log('Server public key set.'); + } + + // Set the AES key when received + setAesKey(aesKey: string, aesIv: string): void { + this.aesKey = Buffer.from(aesKey, 'base64'); + this.aesIv = Buffer.from(aesIv, 'base64'); + console.log('AES key set.'); + } + + // Encrypt a message with AES + private encryptWithAes(message: string): string { + if (!this.aesKey || !this.aesIv) { + throw new Error('AES key or IV is not set.'); + } + const cipher = createCipheriv('aes-256-cbc', this.aesKey, this.aesIv); + let encrypted = cipher.update(message, 'utf-8'); + encrypted = Buffer.concat([encrypted, cipher.final()]); + return encrypted.toString('base64'); + } + + private decryptWithAes(encryptedMessage: string): string { + if (!this.aesKey || !this.aesIv) { + throw new Error('AES key or IV is not set.'); + } + const decipher = createDecipheriv('aes-256-cbc', this.aesKey, this.aesIv); + let decrypted = decipher.update(Buffer.from(encryptedMessage, 'base64')); + decrypted = Buffer.concat([decrypted, decipher.final()]); + return decrypted.toString('utf-8'); + } + + private decryptWithRsa(message: string): string { + if (!this.serverPublicKey) { + throw new Error('Server public key not set.'); + } + try { + const encryptedMessage = Buffer.from(message.toString(), 'base64'); + // Decrypt the message using the server's public key + const decrypted = publicDecrypt( + { + key: this.serverPublicKey, + padding: constants.RSA_PKCS1_PADDING, // Matching padding for decryption + }, + encryptedMessage + ); + return decrypted.toString('utf-8'); + } catch (error) { + console.error('RSA decryption failed:', error); + throw new Error('Failed to decrypt RSA message.'); + } + } + + // Handle incoming chunks of data + async handleIncomingChunk(data: Buffer): Promise { + const incomingMessage = data.toString(); + this.messageBuffer += incomingMessage; + + // Check if the message ends with END_OF_MESSAGE + if (this.messageBuffer.endsWith(END_OF_MESSAGE)) { + // Remove the END_OF_MESSAGE marker and process the message + const completeMessage = this.messageBuffer.slice(0, -END_OF_MESSAGE.length); + + this.handleIncomingMessage(completeMessage); + + // Clear the message buffer after processing + this.messageBuffer = ''; + } + } + + // Send a chunked message over the socket + async sendChunkedMessage(operationCode: string, metaInfo?: { [key: string]: any }, fileContent?: Buffer): Promise { + const message = MessageHandler.formatMessage(operationCode, metaInfo, fileContent); + let outgoingMessage: string; + + // Encrypt the message with AES if available + if (this.aesKey && this.aesIv) { + outgoingMessage = this.encryptWithAes(message); + } else { + outgoingMessage = message // Send plain text if AES is not set + } + + // Append the end marker to the message + outgoingMessage += END_OF_MESSAGE; + + await this.writeToSocket(outgoingMessage); + } + + // Write message to socket + private writeToSocket(message: string): Promise { + return new Promise((resolve, reject) => { + this.socket.write(message, (err: any) => { + if (err) { + console.error('Error sending message over TCP:', err); + return reject(err); + } + resolve(); + }); + }); + } + + // Handle incoming message (decrypted if AES is set) + handleIncomingMessage(incomingMessage: string): void { + let messageToProcess = incomingMessage; + if (this.aesKey && this.aesIv) { + messageToProcess = this.decryptWithAes(incomingMessage); + }else if(this.serverPublicKey){ + messageToProcess = this.decryptWithRsa(incomingMessage); + } + + console.log(`\n\nComplete Message:\n${messageToProcess}\n\n`); + + const result = this.operationHandler.handleOperation(messageToProcess); + + if(result.operationCode === operationCodes.SET_AES_KEY){ + this.isAesKeySetFlag = true; + this.setAesKey(result.metaInfo?.aesKey, result.metaInfo?.aesIv); + return; + } + + if(result.operationCode === operationCodes.SET_PUBLIC_KEY) { + this.setServerPublicKey(result.metaInfo?.publicKey); + return; + } + + this.handlerResult = result + } + + // Check if AES key is set + isAesKeySet(): boolean { + return this.isAesKeySetFlag; + } + + // Get handler result for operation handling + getHandlerResult(): ParsedMessage | null { + return this.handlerResult; + } +} diff --git a/User/src/network/socket_communicator/tcp_server_communicator.ts b/User/src/network/socket_communicator/tcp_server_communicator.ts new file mode 100644 index 0000000..8f57dba --- /dev/null +++ b/User/src/network/socket_communicator/tcp_server_communicator.ts @@ -0,0 +1,161 @@ +import { Socket } from 'net'; +import { privateEncrypt, privateDecrypt, generateKeyPairSync, createCipheriv, createDecipheriv, randomBytes } from 'crypto'; +import { MessageHandler, ParsedMessage } from '../message_handler'; +import { SocketCommunicatorBase } from './socket_communicator_base'; +import { OperationHandler } from '../operations_base/operation_handler'; +import {constants} from "node:crypto"; + +const END_OF_MESSAGE = ''; // Define a unique marker for end of message + +export class TcpServerCommunicator extends SocketCommunicatorBase { + private readonly socket: Socket; + private privateKey: string | null; + private publicKey: string | null; + private aesKey: Buffer | null; + private aesIv: Buffer | null; + private messageBuffer: string; + + constructor(socket: Socket, ip: string, port: number, operationHandler: OperationHandler) { + super(ip, port, operationHandler); + this.socket = socket; + this.privateKey = null; + this.publicKey = null; // Client public key will be set later + this.aesKey = null; + this.aesIv = null; + this.messageBuffer = ''; // Initialize the message buffer + this.generateKeyPair(); // Generate RSA key pair for encryption + } + + // Generate RSA key pair (public and private keys) + generateKeyPair(): void { + const { privateKey, publicKey } = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + this.privateKey = privateKey; + this.publicKey = publicKey; + console.log('RSA key pair generated.'); + } + + // Send the server's public key to the client + async sendPublicKey(): Promise { + if (!this.publicKey) { + throw new Error('Public key is not available. Please generate RSA key pair.'); + } + + const message = MessageHandler.formatMessage('SET_PUBLIC_KEY', { publicKey: this.publicKey }); + await this.writeToSocket(message + END_OF_MESSAGE); + console.log('Public key sent to client.'); + } + + // Generate AES key and IV, then send them to the client + async sendAesKey(): Promise { + this.aesKey = randomBytes(32); // 256-bit AES key + this.aesIv = randomBytes(16); // AES IV + + const aesKeyBase64 = this.aesKey.toString('base64'); + const aesIvBase64 = this.aesIv.toString('base64'); + + const formattedMessage = MessageHandler.formatMessage('SET_AES_KEY', { aesKey: aesKeyBase64, aesIv: aesIvBase64 }); + const encryptedMessage = this.encryptWithRsa(Buffer.from(formattedMessage)).toString('base64'); + + await this.writeToSocket(encryptedMessage + END_OF_MESSAGE); + console.log('AES key and IV sent to client.'); + } + + // Encrypt a message with the server's private key (RSA encryption) + private encryptWithRsa(message: Buffer): Buffer { + const bufferMessage = Buffer.isBuffer(message) ? message : Buffer.from(message); + if (!this.privateKey) throw new Error('Server private key not set.'); + + return privateEncrypt( + { + key: this.privateKey, + padding: constants.RSA_PKCS1_PADDING, // PKCS1 padding + }, + bufferMessage + ); + } + + // Decrypt AES-encrypted messages + private decryptWithAes(encryptedMessage: string): string { + if (!this.aesKey || !this.aesIv) { + throw new Error('AES key not set.'); + } + + const decipher = createDecipheriv('aes-256-cbc', this.aesKey, this.aesIv); + let decrypted = decipher.update(Buffer.from(encryptedMessage, 'base64')); + decrypted = Buffer.concat([decrypted, decipher.final()]); + return decrypted.toString('utf-8'); + } + + // Encrypt a message with AES + private encryptWithAes(message: string): string { + if (!this.aesKey || !this.aesIv) { + throw new Error('AES key or IV is not set.'); + } + const cipher = createCipheriv('aes-256-cbc', this.aesKey, this.aesIv); + let encrypted = cipher.update(message, 'utf-8'); + encrypted = Buffer.concat([encrypted, cipher.final()]); + return encrypted.toString('base64'); + } + + // Handle incoming chunks of data + async handleIncomingChunk(data: Buffer): Promise { + const incomingMessage = data.toString(); + this.messageBuffer += incomingMessage; + + // Check if the message ends with END_OF_MESSAGE + if (this.messageBuffer.endsWith(END_OF_MESSAGE)) { + // Remove the END_OF_MESSAGE marker and process the message + const completeMessage = this.messageBuffer.slice(0, -END_OF_MESSAGE.length); + + console.log(`\n\nComplete Message:\n${completeMessage}\n\n`); + + this.handleIncomingMessage(completeMessage); + + // Clear the message buffer after processing + this.messageBuffer = ''; + } + } + + // Send chunked message + async sendChunkedMessage(operationCode: string, metaInfo?: { [key: string]: any }, fileContent?: Buffer): Promise { + const message = MessageHandler.formatMessage(operationCode, metaInfo, fileContent); + let outgoingMessage: string; + + if (this.aesKey && this.aesIv) { + outgoingMessage = this.encryptWithAes(message); + } else { + outgoingMessage = message; + } + + outgoingMessage += END_OF_MESSAGE; // Append end-of-message marker + await this.writeToSocket(outgoingMessage); + } + + // Handle incoming message (decrypt with AES if available) + handleIncomingMessage(incomingMessage: string): void { + let messageToProcess = incomingMessage; + + if (this.aesKey && this.aesIv) { + messageToProcess = this.decryptWithAes(incomingMessage); + } + + this.handlerResult = this.operationHandler.handleOperation(messageToProcess); + } + + // Write message to socket + private writeToSocket(message: string): Promise { + return new Promise((resolve, reject) => { + this.socket.write(message, (err: any) => { + if (err) { + console.error('Error sending message over TCP:', err); + return reject(err); + } + resolve(); + }); + }); + } +} diff --git a/User/src/network/socket_communicator/udp_socket_communicator.ts b/User/src/network/socket_communicator/udp_socket_communicator.ts new file mode 100644 index 0000000..0385573 --- /dev/null +++ b/User/src/network/socket_communicator/udp_socket_communicator.ts @@ -0,0 +1,39 @@ +import { Socket as UdpSocket } from 'dgram'; +import { MessageHandler } from '../message_handler'; +import { SocketCommunicatorBase } from './socket_communicator_base'; +import {OperationHandler} from "../operations_base/operation_handler"; + +export class UdpSocketCommunicator extends SocketCommunicatorBase { + private readonly socket: UdpSocket; + + constructor(socket: UdpSocket, ip: string, port: number, operationHandler: OperationHandler) { + super(ip, port, operationHandler); + this.socket = socket; + } + + // Handle incoming message (no decryption needed for UDP) + handleIncomingMessage(incomingMessage: string): void { + this.handlerResult = this.operationHandler.handleOperation(incomingMessage); + } + + // Send plain message over UDP (metaInfo as JSON and fileContent as Buffer) + async sendMessage(operationCode: string, metaInfo?: { [key: string]: any }, fileContent?: Buffer): Promise { + const message = MessageHandler.formatMessage(operationCode, metaInfo, fileContent); + + await this.sendUdpMessage(message); + } + + // Helper method to wrap socket.send in a Promise for async/await support + private sendUdpMessage(message: string): Promise { + return new Promise((resolve, reject) => { + this.socket.send(message, this.port, this.ip, (err: any) => { + if (err) { + console.error('Error sending UDP message:', err); + return reject(err); + } + console.log(`Plain message sent to ${this.ip}:${this.port} (UDP)`); + resolve(); + }); + }); + } +} diff --git a/User/src/network/tcp/tcp_client.ts b/User/src/network/tcp/tcp_client.ts new file mode 100644 index 0000000..557939a --- /dev/null +++ b/User/src/network/tcp/tcp_client.ts @@ -0,0 +1,100 @@ +import net, { Socket } from 'net'; +import { TcpClientCommunicator } from '../socket_communicator/tcp_client_communicator'; +import { OperationHandler } from '../operations_base/operation_handler'; +import { operationCodes } from "../operation_codes"; +import { GeneralOperations } from "../operations_custom/general_operations"; +import { UserToUserOperations } from "../operations_custom/user_to_user_operations"; +import { ParsedMessage } from "../message_handler"; + +export class TcpClient { + private readonly tcp_port: number; + private socket: Socket | null; + private communicator: TcpClientCommunicator | null; + private readonly operationHandler: OperationHandler; + private lastResult: ParsedMessage | null; + + constructor(tcp_port: number) { + this.tcp_port = tcp_port; + this.socket = null; + this.communicator = null; + this.operationHandler = OperationHandler.getInstance(); + this.lastResult = null; + + this.operationHandler.loadPlugin(new GeneralOperations()); + this.operationHandler.loadPlugin(new UserToUserOperations()); + } + + // Open a TCP socket connection + openSocket(ip: string): void { + this.socket = new net.Socket(); + + this.socket.connect(this.tcp_port, ip, () => { + console.log(`Client connected to server at ${ip}:${this.tcp_port}`); + this.communicator = new TcpClientCommunicator(this.socket as Socket, ip, this.tcp_port, this.operationHandler); + }); + + this.socket.on('error', (err) => { + console.error(`Connection error to server at ${ip}:${this.tcp_port}: ${err.message}`); + }); + + this.socket.on('data', async (data: Buffer) => { + if (this.communicator) { + await this.communicator.handleIncomingChunk(data); // Let the communicator handle the chunks + this.lastResult = this.communicator.getHandlerResult(); + } + }); + + this.socket.on('close', () => { + console.log(`Connection closed: ${ip}:${this.tcp_port}`); + this.lastResult = null; // Clear the last result on socket close + }); + } + + // Close the socket connection + closeSocket(): void { + if (this.socket) { + this.socket.end(); + this.socket = null; + this.communicator = null; + this.lastResult = null; // Clear the last result on close + console.log('Client socket connection closed.'); + } + } + + // Send a message with operationCode, metaInfo, and fileContent in chunks + async sendMessage(operationCode: string, metaInfo?: { [key: string]: any }, fileContent?: Buffer): Promise { + if (!this.communicator || !this.isAesKeySet()) { + console.error('Communicator not initialized or AES key not set.'); + return false; + } + + await this.communicator.sendChunkedMessage(operationCode, metaInfo, fileContent); // Send message via communicator + return true; + } + + // Check if AES key is set + isAesKeySet(): boolean { + if(!this.communicator) return false; + return this.communicator?.isAesKeySet() + } + + // Check if the message is received (based on if lastResult is available) + isMessageReceived(): boolean { + console.log('Is message received?'); + console.log(this.lastResult); + console.log(this.lastResult !== null); + return this.lastResult !== null; + } + + // Get the last result (and clear it after returning) + getLastResult(): ParsedMessage | null { + const result = this.lastResult; + this.lastResult = null; + return result; + } + + // Check if the socket is still connected + isSocketConnected(): boolean { + return this.socket !== null && !this.socket.destroyed; + } +} diff --git a/User/src/network/tcp/tcp_server.ts b/User/src/network/tcp/tcp_server.ts new file mode 100644 index 0000000..98d1504 --- /dev/null +++ b/User/src/network/tcp/tcp_server.ts @@ -0,0 +1,111 @@ +import net, { Socket } from 'net'; +import path from 'path'; +import dotenv from 'dotenv'; +import { ConnectionManager } from "../connection_manager"; +import { TcpServerCommunicator } from "../socket_communicator/tcp_server_communicator"; +import { GeneralOperations } from "../operations_custom/general_operations"; +import { OperationHandler } from "../operations_base/operation_handler"; +import { UserToUserOperations } from "../operations_custom/user_to_user_operations"; + +dotenv.config({ path: path.resolve(__dirname, './config/.env') }); + +export class TcpServer { + private readonly connectionManager: ConnectionManager; + private readonly operationHandler: OperationHandler; + private readonly port: number; + private readonly host: string; + + constructor(host: string, port: number) { + this.connectionManager = new ConnectionManager(); + this.operationHandler = OperationHandler.getInstance(); + this.host = host; + this.port = port; + + this.operationHandler.loadPlugin(new GeneralOperations()); + this.operationHandler.loadPlugin(new UserToUserOperations()); + } + + // Start the TCP server + public start(): void { + const tcpServer = net.createServer(); + + // Handle incoming connections + tcpServer.on('connection', (socket: Socket) => { + const ip = socket.remoteAddress || 'unknown'; + const port = socket.remotePort || 0; + + const clientId = `${ip}:${port}`; // Use IP and port to identify the client + console.log(`Client connected: ${clientId}`); + + const tcpCommunicator = new TcpServerCommunicator(socket, ip, port, this.operationHandler); + this.connectionManager.addConnection(ip, port, tcpCommunicator); + + // Generate RSA key pair and start key exchange + tcpCommunicator.generateKeyPair(); + tcpCommunicator.sendPublicKey() + .then(() => tcpCommunicator.sendAesKey()) + .then(() => console.log('Public key and AES key sent successfully.')) + .catch(err => { + console.error(`Error during key exchange with client ${clientId}:`, err); + socket.end(); // Close the connection in case of any error + }); + + // Handle incoming data in chunks + socket.on('data', async (data: Buffer) => { + await this.handleData(data, ip, port); + }); + + // Handle client disconnect + socket.on('end', () => { + console.log(`Client disconnected: ${clientId}`); + this.connectionManager.removeCommunicator(ip, port); + }); + + // Handle socket errors + socket.on('error', (err: Error) => { + console.error(`Error from client ${clientId}: ${err.message}`); + this.connectionManager.removeCommunicator(ip, port); + }); + }); + + // Handle server errors + tcpServer.on('error', (err: Error) => { + console.error(`TCP server error: ${err.message}`); + }); + + // Start listening for connections + tcpServer.listen(this.port, this.host, () => { + console.log(`TCP server listening on ${this.host}:${this.port}`); + }); + } + + // Handle incoming data from a client + private async handleData(data: Buffer, ip: string, port: number): Promise { + const clientId = `${ip}:${port}`; + + // Retrieve the communicator associated with this connection + const communicator = this.connectionManager.getCommunicator(ip, port) as TcpServerCommunicator; + if (!communicator) { + console.error(`No communicator found for ${clientId}`); + return; + } + + // Handle incoming chunked message via communicator + await communicator.handleIncomingChunk(data); + + // Fetch and process result if available + const handlerResult = communicator.getHandlerResult(); + if (handlerResult) { + try { + await communicator.sendChunkedMessage( + handlerResult.operationCode, + handlerResult.metaInfo, + handlerResult.fileContent + ); + console.log(`Response sent to ${clientId}`); + } catch (err) { + console.error(`Failed to send response to ${clientId}:`, err); + } + } + } +} diff --git a/User/src/network/udp/udp_client.ts b/User/src/network/udp/udp_client.ts new file mode 100644 index 0000000..03789b2 --- /dev/null +++ b/User/src/network/udp/udp_client.ts @@ -0,0 +1,140 @@ +import dgram from 'dgram'; +import ping from 'ping'; +import { OperationHandler } from '../operations_base/operation_handler'; +import { MessageHandler } from '../message_handler'; +import { GeneralOperations } from "../operations_custom/general_operations"; +import { operationCodes } from "../operation_codes"; +import os from 'os'; + +export class UdpClient { + private udpSocket: dgram.Socket; + private readonly port: number; + private operationHandler: OperationHandler; + + constructor(port: number) { + this.port = port; + this.udpSocket = dgram.createSocket('udp4'); + this.operationHandler = OperationHandler.getInstance(); + this.operationHandler.loadPlugin(new GeneralOperations()); + } + + // Send heartbeat and get clients that respond with ALIVE, excluding the host machine's IP addresses + async getAliveClients(): Promise { + const subnet = this.getSubnet(); + const ipRange = this.getIPRange(subnet); + + // Get local machine's IP addresses to exclude + const localIPs = this.getLocalIPs(); + + // First, filter active IPs that respond to ping + const activeIps = await this.filterActiveIps(ipRange); + + // Send heartbeat to each active IP and keep only those that respond with ALIVE + const aliveClients: string[] = []; + for (const ip of activeIps) { + if (!localIPs.includes(ip)) { + const result = await this.sendHeartbeat(ip); + if (result.found) { + aliveClients.push(ip); + } + } + } + + return aliveClients; // Return the list of IPs that responded with ALIVE, excluding the host machine + } + + // Get local IP addresses of the host machine (excluding loopback) + private getLocalIPs(): string[] { + const interfaces = os.networkInterfaces(); + const localIPs: string[] = []; + + Object.values(interfaces).forEach((iface) => { + iface?.forEach((address) => { + if (address.family === 'IPv4' && !address.internal) { + localIPs.push(address.address); + } + }); + }); + + return localIPs; + } + + // Send heartbeat to an IP + private async sendHeartbeat(ip: string): Promise<{ found: boolean }> { + return new Promise((resolve) => { + const heartbeatCode = operationCodes.HEARTBEAT; + const heartbeatMessage = MessageHandler.formatMessage(heartbeatCode); + + this.udpSocket.send(heartbeatMessage, this.port, ip, (err) => { + if (err) { + resolve({ found: false }); + } else { + const timeout = setTimeout(() => { + this.dropConnection(ip); + resolve({ found: false }); + }, 1500); + + this.udpSocket.once('message', (msg, rinfo) => { + if (rinfo.address === ip) { + clearTimeout(timeout); + const parsedMessage = MessageHandler.parseMessage(msg.toString()); + if (parsedMessage?.operationCode === operationCodes.ALIVE) { + resolve({ found: true }); + } else { + resolve({ found: false }); + } + } + }); + } + }); + }); + } + + // Drop connection for a specific IP + private dropConnection(ip: string): void { + try { + this.udpSocket.removeAllListeners('message'); + } catch (err: any) { + console.error(`Error dropping connection to ${ip}: ${err.message}`); + } + } + + // Get the subnet (e.g., 192.168.1) + private getSubnet(): string { + const interfaces = os.networkInterfaces(); + for (const iface of Object.values(interfaces)) { + for (const address of iface || []) { + if (address.family === 'IPv4' && !address.internal) { + return address.address.split('.').slice(0, 3).join('.'); + } + } + } + return ''; + } + + // Get IP range (assuming /24 subnet) + private getIPRange(subnet: string): string[] { + const ipRange = []; + for (let i = 1; i < 255; i++) { + ipRange.push(`${subnet}.${i}`); + } + return ipRange; + } + + // Filter only active IPs by pinging each IP in the range + private async filterActiveIps(ipRange: string[]): Promise { + const activeIps: string[] = []; + + const pingPromises = ipRange.map(ip => ping.promise.probe(ip, { timeout: 1 })); + + const pingResults = await Promise.all(pingPromises); + + for (const result of pingResults) { + if (result.alive) { + activeIps.push(result.host); + } + } + + return activeIps; + } +} diff --git a/User/src/network/udp/udp_server.ts b/User/src/network/udp/udp_server.ts new file mode 100644 index 0000000..b87b82f --- /dev/null +++ b/User/src/network/udp/udp_server.ts @@ -0,0 +1,64 @@ +import dgram, { RemoteInfo } from 'dgram'; +import { UdpSocketCommunicator } from "../socket_communicator/udp_socket_communicator"; +import { OperationHandler } from "../operations_base/operation_handler"; +import { GeneralOperations} from "../operations_custom/general_operations"; + +export class UdpServer { + private readonly udpServer: dgram.Socket; + private readonly operationHandler: OperationHandler; + private readonly port: number; + private readonly host: string; + + constructor(host: string, port: number) { + this.udpServer = dgram.createSocket('udp4'); + this.operationHandler = OperationHandler.getInstance(); + this.host = host; + this.port = port; + + // Load only the GeneralOperations into the operation handler + this.operationHandler.loadPlugin(new GeneralOperations()); + } + + // Start the UDP server + public start(): void { + this.udpServer.on('message', this.handleUdpMessages.bind(this)); + this.udpServer.on('error', this.handleError); + this.udpServer.on('listening', this.handleListening.bind(this)); + + // Bind the server to the UDP port and host + this.udpServer.bind(this.port, this.host); + } + + // Handle incoming UDP messages + private async handleUdpMessages(msg: Buffer, rinfo: RemoteInfo): Promise { + const ip = rinfo.address; + const port = rinfo.port; + + console.log(`Received message from ${ip}:${port}`); + + // Create a temporary communicator for the incoming message + const communicator = new UdpSocketCommunicator(this.udpServer, ip, port, this.operationHandler); + + // Process the incoming message using the communicator + communicator.handleIncomingMessage(msg.toString()); + + const communicatorResult = communicator.getHandlerResult(); + if (communicatorResult) { + // Send response back to the client using the temporary communicator + await communicator.sendMessage(communicatorResult.operationCode, communicatorResult.metaInfo); + } else { + console.error(`No handler result for ${ip}:${port}`); + } + } + + // Handle UDP server errors + private handleError(err: Error): void { + console.error(`UDP server error:\n${err.stack}`); + } + + // Handle when the UDP server starts listening + private handleListening(): void { + const address = this.udpServer.address(); + console.log(`UDP server listening on ${address.address}:${address.port}`); + } +} diff --git a/User/src/renderer/assets/background.png b/User/src/renderer/assets/background.png deleted file mode 100644 index fc8f921..0000000 Binary files a/User/src/renderer/assets/background.png and /dev/null differ diff --git a/User/src/renderer/assets/hard-disk.ico b/User/src/renderer/assets/hard-disk.ico deleted file mode 100644 index 8b62600..0000000 Binary files a/User/src/renderer/assets/hard-disk.ico and /dev/null differ diff --git a/User/src/renderer/assets/hard-disk.png b/User/src/renderer/assets/hard-disk.png deleted file mode 100644 index bdf9cbf..0000000 Binary files a/User/src/renderer/assets/hard-disk.png and /dev/null differ diff --git a/User/src/renderer/assets/user_1144760.png b/User/src/renderer/assets/user_1144760.png deleted file mode 100644 index e87e7c7..0000000 Binary files a/User/src/renderer/assets/user_1144760.png and /dev/null differ diff --git a/User/src/renderer/css/change_department.css b/User/src/renderer/css/change_department.css deleted file mode 100644 index bf02450..0000000 --- a/User/src/renderer/css/change_department.css +++ /dev/null @@ -1,99 +0,0 @@ -body, html { - height: 100%; - width: 100%; - margin: 0; - padding: 0; - - font-family: "Roboto", sans-serif; - font-weight: 500; - font-style: normal; - - background-image: url('../assets/background.png'); - background-size: cover; - background-repeat: no-repeat; - -} - -.container { - opacity: 0; - - display: flex; - flex-direction: row; - justify-content: space-evenly; - align-items: center; - min-height: 100vh; -} - -.department-form { - background-color: #535C91; - opacity: 71; - padding: 8vh 3vh 5vh; - border-radius: 1rem; - box-shadow: 10px 10px 10px rgba(0, 0, 0, 0.9); - width: 25%; -} - -.department-form-title { - margin-bottom: 5vh; - color: #FFFFFF; - text-align: center; -} - -.department-form-title h2 { - color: #FFFFFF; - margin: 0; - padding: 0; - font-size: 5vh; -} - -.department-form-title hr { - width: 65%; -} - -.department-form-content { - display: flex; - margin-left: 2rem; - flex-direction: column; - align-items: start; - font-size: 1.5rem; - color: white; - font-weight: bold; -} - -.department-form-content input { - margin: 0.7rem; -} - -.department-form-footer { - margin-top: 7vh; - display: flex; - flex-wrap: wrap; - justify-content: space-between; - align-content: center; - align-items: center; -} - -button { - font-weight: bold; - width: 35%; - font-size: 1rem; - padding: 10px; - border: none; - border-radius: 5px; - margin-top: 5px; - cursor: pointer; -} - -button:hover { - filter: brightness(85%); -} - -button[name="submit"] { - background-color: #F44336; - color: white; -} - -button[name="back"] { - background-color: #23BDEE; - color: white; -} \ No newline at end of file diff --git a/User/src/renderer/css/sending_file_confirmation.css b/User/src/renderer/css/sending_file_confirmation.css deleted file mode 100644 index db2f969..0000000 --- a/User/src/renderer/css/sending_file_confirmation.css +++ /dev/null @@ -1,39 +0,0 @@ -body, html { - height: 100%; - width: 100%; - margin: 0; - padding: 0; - - font-family: "Roboto", sans-serif; - font-weight: 500; - font-style: normal; - - background-image: url('../assets/background.png'); - background-size: cover; - background-repeat: no-repeat; - -} - -.container { - opacity: 0; - - display: flex; - flex-direction: row; - justify-content: space-evenly; - align-items: center; - min-height: 100vh; - text-align: center; /* Center the text for all child elements */ -} - -.header { - color: #1B1A55; - font-size: 4vh; - text-transform: uppercase; - line-height: 2.5rem; - margin-bottom: 10rem; -} - -img { - width: 20%; - height: 20%; -} diff --git a/User/src/renderer/css/sign_up_confirmation.css b/User/src/renderer/css/sign_up_confirmation.css deleted file mode 100644 index dbd61b2..0000000 --- a/User/src/renderer/css/sign_up_confirmation.css +++ /dev/null @@ -1,34 +0,0 @@ -body, html { - height: 100%; - width: 100%; - margin: 0; - padding: 0; - - font-family: "Roboto", sans-serif; - font-weight: 500; - font-style: normal; - - background-image: url('../assets/background.png'); - background-size: cover; - background-repeat: no-repeat; - -} - -.container { - opacity: 0; - - display: flex; - flex-direction: row; - justify-content: space-evenly; - align-items: center; - min-height: 100vh; - text-align: center; /* Center the text for all child elements */ -} - -.header { - color: #1B1A55; - font-size: 4vh; - text-transform: uppercase; - line-height: 2.5rem; - margin-bottom: 10rem; -} diff --git a/User/src/renderer/css/sign_up_department.css b/User/src/renderer/css/sign_up_department.css deleted file mode 100644 index 6993c9c..0000000 --- a/User/src/renderer/css/sign_up_department.css +++ /dev/null @@ -1,129 +0,0 @@ -body, html { - height: 100%; - width: 100%; - margin: 0; - padding: 0; - - font-family: "Roboto", sans-serif; - font-weight: 500; - font-style: normal; - - background-image: url('../assets/background.png'); - background-size: cover; - background-repeat: no-repeat; - -} - -.container { - opacity: 0; - - display: flex; - flex-direction: row; - justify-content: space-evenly; - align-items: center; - min-height: 100vh; - text-align: center; -} - -.header { - color: #1B1A55; - font-size: 4vh; - text-transform: uppercase; - line-height: 2.5rem; - margin-bottom: 10rem; -} - -.signup-form { - background-color: #535C91; - opacity: 71; - padding: 8vh 3vh 5vh; - border-radius: 1rem; - box-shadow: 10px 10px 10px rgba(0, 0, 0, 0.9); - width: 25%; -} - -.signup-form-title { - margin-bottom: 5vh; -} - -.signup-form-title h2 { - color: #FFFFFF; - margin: 0; - padding: 0; - text-align: center; - font-size: 5vh; -} - -.signup-form hr { - width: 65%; -} - -input { - margin: 0 0 1rem 0; -} - -.signup-form-content { - display: flex; - margin: 1rem 2rem 2rem 3rem; - flex-direction: column; - align-items: start; - - height: 20vh; /* Fixed height */ - width: 80%; /* Full width */ - overflow: auto; /* Enable scrolling */ - - font-size: 1.2rem; - color: white; - font-weight: bold; -} - -.signup-form-content::-webkit-scrollbar { - width: 10px; /* Reduced width of the scrollbar by 2px */ -} - -.signup-form-content::-webkit-scrollbar-track { - background: #1B1A55; /* Updated track color */ -} - -.signup-form-content::-webkit-scrollbar-thumb { - background: #535C91; /* Updated handle color */ - border: 2px solid #1B1A55; /* Adding a border to effectively reduce the thumb size */ -} - -.signup-form-content::-webkit-scrollbar-thumb:hover { - background: #555; /* Updated handle color on hover */ -} - -.signup-form-footer { - margin-top: 5vh; - display: flex; - flex-wrap: wrap; - flex-direction: row; - justify-content: space-between; - align-items: center; -} - -button { - font-weight: bold; - width: 30%; - font-size: 1rem; - padding: 10px; - border: none; - border-radius: 5px; - margin-top: 5px; - cursor: pointer; -} - -button:hover { - filter: brightness(85%); -} - -button[name="submit"] { - background-color: #F44336; - color: white; -} - -button[name="back"] { - background-color: #23BDEE; - color: white; -} \ No newline at end of file diff --git a/User/src/renderer/css/transition.css b/User/src/renderer/css/transition.css deleted file mode 100644 index 442e6f8..0000000 --- a/User/src/renderer/css/transition.css +++ /dev/null @@ -1,25 +0,0 @@ -.fade-in { - animation: fadeInAnimation 0.5s ease-in forwards; -} - -.fade-out { - animation: fadeOutAnimation 0.5s ease-out forwards; -} - -@keyframes fadeInAnimation { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@keyframes fadeOutAnimation { - from { - opacity: 1; - } - to { - opacity: 0; - } -} diff --git a/User/src/renderer/html/alert_modal.html b/User/src/renderer/html/alert_modal.html deleted file mode 100644 index 34cdd4f..0000000 --- a/User/src/renderer/html/alert_modal.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - Alert Modal - - -
-
-
- -

-
- -
-
- - diff --git a/User/src/renderer/html/change_department.html b/User/src/renderer/html/change_department.html deleted file mode 100644 index 2af2131..0000000 --- a/User/src/renderer/html/change_department.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - Department Selection - - -
-
-
-

Choose your department

-
-
-
- -
- -
-
- - diff --git a/User/src/renderer/html/decrypting_backup.html b/User/src/renderer/html/decrypting_backup.html deleted file mode 100644 index fadaff9..0000000 --- a/User/src/renderer/html/decrypting_backup.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - Sending file - - -
-
-

DECRYPTING BACKUP!

-

PlEASE WAIT

- Description of GIF - -
-
- - diff --git a/User/src/renderer/html/ip_submit.html b/User/src/renderer/html/ip_submit.html deleted file mode 100644 index e4c7f0d..0000000 --- a/User/src/renderer/html/ip_submit.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - IP Submit - - -
-
-
-

IP Config

-
-
-
- -
- -
-
- - diff --git a/User/src/renderer/html/login.html b/User/src/renderer/html/login.html deleted file mode 100644 index 6305146..0000000 --- a/User/src/renderer/html/login.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - Login - - -
-
-

DO WE KNOW

-

EACH OTHER?

-
- -
- - diff --git a/User/src/renderer/html/sign_up_departments.html b/User/src/renderer/html/sign_up_departments.html deleted file mode 100644 index 3a2b6a5..0000000 --- a/User/src/renderer/html/sign_up_departments.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - Department Selection - - -
-
-

TELL ME MORE

-

ABOUT

-

YOUR WORK

-
- -
- - diff --git a/User/src/renderer/html/sign_up_profile.html b/User/src/renderer/html/sign_up_profile.html deleted file mode 100644 index 346418f..0000000 --- a/User/src/renderer/html/sign_up_profile.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - Signup - - -
-
-

LET US MEET

-

EACH OTHER

-
- -
\ No newline at end of file diff --git a/User/src/renderer/js/alert_modal.js b/User/src/renderer/js/alert_modal.js deleted file mode 100644 index 0367509..0000000 --- a/User/src/renderer/js/alert_modal.js +++ /dev/null @@ -1,11 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const closeButton = document.getElementById('closeButton'); - closeButton.addEventListener('click', () => { - window.electronAPI.closeAlertWindow(); - }); -}); - -function showAlert(message) { - const modalMessage = document.getElementById('modal-message'); - modalMessage.textContent = message; -} diff --git a/User/src/renderer/js/change_department.js b/User/src/renderer/js/change_department.js deleted file mode 100644 index d675cdd..0000000 --- a/User/src/renderer/js/change_department.js +++ /dev/null @@ -1,80 +0,0 @@ -document.addEventListener('DOMContentLoaded', async function () { - let ip = ''; - await window.electronAPI.readFile('ipConfig.json') - .then(result => { - const jsonData = JSON.parse(result.content); - ip = jsonData.ip; - }) - - await fetch(`http://${ip}/users/departments`, { - method: 'GET', - headers: { - 'x-api-key': 'uc_api' - } - }).then(async result => { - const res = await result.json(); - const data = res['data']; - - const formContent = document.querySelector('.department-form-content'); - Object.entries(data).forEach(([key, department]) => { - if(department.name !== 'CEO'){ - const label = document.createElement('label'); - label.innerHTML = `${department.name}`; - formContent.appendChild(label); - } - }); - }).catch(async error => { - await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error changing content:', error)); - }); - - document.getElementById('back').addEventListener('click', async function () { - try { - await fadeOut('main_menu.html'); - console.log('Content changed successfully'); - } catch (error) { - console.error('Error changing content:', error); - } - }); - - document.getElementById('submit').addEventListener('click', async function (e) { - e.preventDefault(); - - const selectedDept = document.querySelector('input[name="dept"]:checked').value; - if (!selectedDept) { - throw new Error('No department had been selected!.'); - } - - let result = await window.electronAPI.readFile('loginData.json'); - if (!result.success) { - throw new Error('Error reading the file. Please try again later.'); - } - - const data = JSON.parse(await result.content); - const id = data.id; - - await fetch(`http://${ip}/users/change_department`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': 'uc_api' - }, - body: JSON.stringify({ - id: id, - department: selectedDept, - }) - }).then(async response => { - if (!response.ok) { - return; - } - - fadeOut('main_menu.html') - }).catch(async error => { - console.error(error); - await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error changing content:', error)); - }); - }); -}) diff --git a/User/src/renderer/js/ip_submit.js b/User/src/renderer/js/ip_submit.js deleted file mode 100644 index bcc69e9..0000000 --- a/User/src/renderer/js/ip_submit.js +++ /dev/null @@ -1,34 +0,0 @@ -document.addEventListener('DOMContentLoaded', async function () { - const submitButton = document.getElementById('submit'); - const ipInput = document.getElementById('ipInput'); - - submitButton.addEventListener('click', async function (e) { - e.preventDefault(); - try { - const ipAddress = ipInput.value.trim(); - if (!ipAddress) { - throw new Error('Please enter an IP address.'); - } - - fetch(`http://${ipAddress}/heartbeat`) - .then(async response => { - if (!response.ok) { - throw new Error('Test failed. Check ip and server.'); - } - - await window.electronAPI.writeFile('ipConfig.json', JSON.stringify({ip: ipAddress})); - fadeOut('login.html'); - }) - .catch(async error => { - await window.electronAPI.showAlert('Can\'t reach server.') - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error)); - }); - } catch (error) { - console.error('Error:', error.message); - await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error)); - } - }); -}); diff --git a/User/src/renderer/js/login.js b/User/src/renderer/js/login.js deleted file mode 100644 index 812c401..0000000 --- a/User/src/renderer/js/login.js +++ /dev/null @@ -1,98 +0,0 @@ -document.addEventListener('DOMContentLoaded', async function () { - const signupButton = document.getElementById('signup'); - const submitButton = document.getElementById('submit'); - - const signupDataExists = await window.electronAPI.checkFileExists('signupData.json'); - if (signupDataExists) { - await window.electronAPI.deleteFile('signupData.json'); - } - - let ip = ''; - await window.electronAPI.readFile('ipConfig.json') - .then(result => { - const jsonData = JSON.parse(result.content); - ip = jsonData.ip; - }) - - const fileExists = await window.electronAPI.checkFileExists('loginData.json'); - if (fileExists) { - await window.electronAPI.readFile('loginData.json') - .then(async result => { - const loginData = JSON.parse(result.content); - console.log(loginData); - const {email, password} = loginData; - - await fetch(`http://${ip}/users/login`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': 'uc_api' - }, - body: JSON.stringify({ - email: email, - password: password - }) - }).then(async response => { - if (response.ok) { - await window.electronAPI.killBeforeLogout(); - await window.electronAPI.startMainProcesses(); - fadeOut('main_menu.html'); - } else { - await window.electronAPI.deleteFile('loginData.json'); - } - }).catch(error => { - console.error(error); - }); - }) - .catch(async error => { - console.error('Can\'t read loginData'); - await window.electronAPI.deleteFile('loginData.json'); - }); - } - - signupButton.addEventListener('click', function (e) { - e.preventDefault(); - console.log('Sign up button clicked.') - fadeOut('sign_up_profile.html'); - }); - - submitButton.addEventListener('click', async function (e) { - e.preventDefault(); - console.log('Submit button clicked'); - - const form = document.getElementById('loginForm'); - const formData = new FormData(form); - - const email = formData.get('email'); - const password = formData.get('password'); - - await fetch(`http://${ip}/users/login`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': 'uc_api' - }, - body: JSON.stringify({ - email: email, - password: password - }) - }).then(async response => { - if (!response.ok) { - const data = await response.json(); - throw new Error(data.message); - } - - return response.json(); - }).then(async data => { - data.data.password = password - await window.electronAPI.writeFile('loginData.json', JSON.stringify(data.data, null, 2)); - await window.electronAPI.startMainProcesses(); - fadeOut('main_menu.html'); - }) - .catch(async error => { - await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error)); - }) - }); -}); diff --git a/User/src/renderer/js/main_menu.js b/User/src/renderer/js/main_menu.js deleted file mode 100644 index fa47396..0000000 --- a/User/src/renderer/js/main_menu.js +++ /dev/null @@ -1,289 +0,0 @@ -document.addEventListener('DOMContentLoaded', async function () { - await insertUsername(); - setInterval(loadReceivedFiles, 3000); // Call loadReceivedFiles every 5000 ms (5 seconds) - - const backupButton = document.getElementById('backup_dir'); - const shareButton = document.getElementById('share_dir'); - const departmentButton = document.getElementById('department_dir'); - - const changeDepartmentButton = document.getElementById('change_department'); - const changeInfoButton = document.getElementById('change_info'); - const shareFileButton = document.getElementById('share_file'); - const decryptButton = document.getElementById('decrypt'); - const logoutButton = document.getElementById('logout'); - - const overlay = document.getElementById('overlay'); - const ceoBackButton = document.getElementById('back_button'); - const ceoSubmitButton = document.getElementById('submit_button'); - - let ip = ''; - await window.electronAPI.readFile('ipConfig.json') - .then(result => { - const jsonData = JSON.parse(result.content); - ip = jsonData.ip; - }) - - let triggerSource = ''; - - async function loadReceivedFiles() { - const checkFileReceived = await window.electronAPI.checkFileExists('filesReceived.json'); - if (!checkFileReceived) { - return; - } - - try { - const fileData = await window.electronAPI.readFile('filesReceived.json'); - const filesJson = JSON.parse(fileData.content); - const receivedFiles = filesJson.receivedFiles; - - const notificationsDiv = document.getElementById('notifications'); - const existingButtons = Array.from(notificationsDiv.children).map(button => button.getAttribute('data-filepath')); - - receivedFiles.forEach(filePath => { - // Check if the button for this file path already exists - if (!existingButtons.includes(filePath)) { - const button = document.createElement('button'); - button.setAttribute('data-filepath', filePath); // Set a custom attribute to track the file path - button.name = 'notification'; - button.textContent = 'You received a file'; - button.addEventListener('click', () => handleFileReceivedButtonPressed(filePath, button)); - notificationsDiv.appendChild(button); - } - }); - } catch (error) { - console.error('Error loading received files:', error); - } - } - - async function handleFileReceivedButtonPressed(filePath, button) { - console.log('Notification button clicked!'); - - // Open file in file explorer - await window.electronAPI.showFileInExplorer(filePath) - .then(() => console.log('File explorer opened')) - .catch(error => console.error('Error opening file explorer:', error)); - - // Remove the button - button.remove(); - - // Call to remove the path from the JSON file - await window.electronAPI.removePathFromReceivedFiles(filePath) - .then(() => console.log('Path removed from received files')) - .catch(error => console.error('Error removing path:', error)); - } - - function handleOverlayOpen(buttonId) { - overlay.style.display = 'block'; - triggerSource = buttonId; // Remember the button that triggered the overlay - console.log(`${buttonId} button clicked!`); - } - - ceoBackButton.addEventListener('click', function () { - overlay.style.display = 'none'; - }); - - ceoSubmitButton.addEventListener('click', async function (event) { - event.preventDefault(); - const password = document.getElementById('ceo_password').value; - - await fetch(`http://${ip}/users/validate_ceo_password`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': 'uc_api', - }, - body: JSON.stringify({ - password: password - }) - }).then(async result => { - const data = await result.json(); - if (!result.ok) { - throw new Error(data.message); - } - if (triggerSource === 'change_department') { - fadeOut('change_department.html'); - } else if (triggerSource === 'decrypt') { - console.log('astept decryptarea') - fadeOut('decrypting_backup.html'); - } - }) - - overlay.style.display = 'none'; - triggerSource = ''; - }); - - checkDirBackupFileExists() - .then(() => console.log('verificare backupDir facuta')); - - checkShareDirFileExists() - .then(() => console.log('verificare ShareDir facuta')); - - checkDepartmentDirFileExists() - .then(() => {console.log('verificare departmentDir facuta')}) - - backupButton.addEventListener('click', function () { - console.log('Set backup directory button clicked!'); - - window.electronAPI.openJsonDirConfigDialog('dirBackup.json') - .then(() => console.log('Back-up directory set')) - .catch(async error => await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error))); - }); - - shareButton.addEventListener('click', function () { - console.log('Set backup directory button clicked!'); - - window.electronAPI.openJsonDirConfigDialog('dirShare.json') - .then(() => console.log('Share directory set')) - .catch(async error => await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error))); - }); - - departmentButton.addEventListener('click', function () { - console.log('Set backup directory button clicked!'); - - window.electronAPI.openJsonDirConfigDialog('dirDepartment.json') - .then(() => console.log('Department directory set')) - .catch(async error => await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error))); - }); - - changeInfoButton.addEventListener('click', function () { - console.log('Change your info button clicked!'); - fadeOut('profile.html'); - }); - - changeDepartmentButton.addEventListener('click', function () { - handleOverlayOpen('change_department'); - }); - - decryptButton.addEventListener('click', function () { - handleOverlayOpen('decrypt'); - }); - - shareFileButton.addEventListener('click', function () { - console.log('Share a file button clicked!'); - fadeOut('share_file.html'); - }); - - logoutButton.addEventListener('click', async function () { - console.log('Logout button clicked!'); - await window.electronAPI.killBeforeLogout(); - await window.electronAPI.deleteFile('loginData.json'); - fadeOut('login.html'); - }); - - async function checkDirBackupFileExists() { - try { - // Make an IPC call to check file existence - const fileExists = await window.electronAPI.checkFileExists('dirBackup.json'); - - if (!fileExists) { - const notificationsDiv = document.getElementById('notifications'); - const button = document.createElement('button'); - button.id = 'backup_alert'; - button.name = 'alert'; - button.textContent = 'Set your backup directory!'; - button.addEventListener('click', handleBackupButtonPressed); - notificationsDiv.appendChild(button); - } - } catch (error) { - console.error('Error checking file existence:', error); - } - } - - async function checkShareDirFileExists() { - try { - const fileExists = await window.electronAPI.checkFileExists('dirShare.json'); - - if (!fileExists) { - const notificationsDiv = document.getElementById('notifications'); - const button = document.createElement('button'); - button.id = 'share_dir_alert'; - button.name = 'alert'; - button.textContent = 'Set your share directory!'; - button.addEventListener('click', handleShareButtonPressed); - notificationsDiv.appendChild(button); - } - } catch (error) { - console.error('Error checking file existence:', error); - } - } - - async function checkDepartmentDirFileExists() { - try { - const fileExists = await window.electronAPI.checkFileExists('dirDepartment.json'); - - if (!fileExists) { - const notificationsDiv = document.getElementById('notifications'); - const button = document.createElement('button'); - button.id = 'department_dir_alert'; - button.name = 'alert'; - button.textContent = 'Set your department directory!'; - button.addEventListener('click', handleDepartmentButtonPressed); - notificationsDiv.appendChild(button); - } - } catch (error) { - console.error('Error checking file existence:', error); - } - } - - async function handleBackupButtonPressed() { - console.log('Button clicked!'); - - await window.electronAPI.openJsonDirConfigDialog('dirBackup.json') - .then(() => console.log('Back-up directory set')) - .catch(async error => await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error))); - - const button = document.getElementById('backup_alert'); - button.remove(); - } - - async function handleShareButtonPressed() { - console.log('Button clicked!'); - - await window.electronAPI.openJsonDirConfigDialog('dirShare.json') - .then(() => console.log('Share directory set')) - .catch(async error => await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error))); - - const button = document.getElementById('share_dir_alert'); - button.remove(); - } - - async function handleDepartmentButtonPressed() { - console.log('Button clicked!'); - - await window.electronAPI.openJsonDirConfigDialog('dirDepartment.json') - .then(() => console.log('Department directory set')) - .catch(async error => await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error))); - - const button = document.getElementById('department_dir_alert'); - button.remove(); - } - - async function insertUsername() { - try { - const userData = await window.electronAPI.readFile('loginData.json'); - const userJson = JSON.parse(userData.content); - const username = userJson.name; - - const usernameField = document.getElementById('username_field'); - if (usernameField) { - usernameField.textContent = username + '!'; - } - } catch (error) { - console.error('Error loading username:', error); - const usernameField = document.getElementById('username_field'); - usernameField.textContent = 'User!'; - } - } -}); \ No newline at end of file diff --git a/User/src/renderer/js/profile.js b/User/src/renderer/js/profile.js deleted file mode 100644 index 1ff712f..0000000 --- a/User/src/renderer/js/profile.js +++ /dev/null @@ -1,77 +0,0 @@ -document.addEventListener('DOMContentLoaded', async function () { - - let ip = ''; - await window.electronAPI.readFile('ipConfig.json') - .then(result => { - const jsonData = JSON.parse(result.content); - ip = jsonData.ip; - }) - - const result = await window.electronAPI.readFile('loginData.json'); - const loginData = JSON.parse(result.content); - - const emailInput = document.querySelector('input[name="email"]'); - const usernameInput = document.querySelector('input[name="username"]'); - const passwordInput = document.querySelector('input[name="password"]'); - - emailInput.value = loginData.email; - usernameInput.value = loginData.name; - passwordInput.value = loginData.password; - - const backButton = document.querySelector('button[name="login"]'); - const submitButton = document.querySelector('button[name="submit"]'); - - backButton.addEventListener('click', function () { - console.log('Back button clicked!'); - fadeOut('main_menu.html') - }); - - submitButton.addEventListener('click', async function (e) { - e.preventDefault(); - console.log('Submit button clicked!'); - - const emailInput = document.querySelector('input[name="email"]'); - const usernameInput = document.querySelector('input[name="username"]'); - const passwordInput = document.querySelector('input[name="password"]'); - - const result = await window.electronAPI.readFile('loginData.json'); - const loginData = JSON.parse(result.content); - - const {id, department} = loginData; - const email = emailInput.value; - const name = usernameInput.value; - const password = passwordInput.value; - - await fetch(`http://${ip}/users`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': 'uc_api' - }, - body: JSON.stringify({ - name: name, - email: email, - password: password - }) - }).then(async result => { - const data = await result.json(); - if (!result.ok) { - throw new Error(data.message); - } - - await window.electronAPI.writeFile('loginData.json', JSON.stringify({ - id: id, - name: name, - email: email, - password: password, - department: department - })); - - fadeOut('main_menu.html'); - }).catch(async error => { - await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error)); - }) - }); -}); diff --git a/User/src/renderer/js/sending_file_confirmation.js b/User/src/renderer/js/sending_file_confirmation.js deleted file mode 100644 index 9fe8f75..0000000 --- a/User/src/renderer/js/sending_file_confirmation.js +++ /dev/null @@ -1,65 +0,0 @@ -document.addEventListener("DOMContentLoaded", async function () { - async function performUploads() { - try { - const uploadFile = await window.electronAPI.readFile('usersDestTemp.json'); - const uploadData = JSON.parse(uploadFile.content); - - const uploadPromises = uploadData.users.map(async (user) => { - try { - const fileResponse = await fetch(uploadData.filePath); - if (!fileResponse.ok) { - throw new Error(`HTTP error when trying to fetch the file: status ${fileResponse.statusText}`); - } - const fileBlob = await fileResponse.blob(); - - await timeout(5000); // Timeout to simulate delay or wait - const url = `http://${user.destIp}:${user.destPort}/share_file`; - - const uploadResponse = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/octet-stream', - 'X-IdUser': user.userId, - 'X-NameOfFile': user.fileName, - 'X-SizeOfFile': fileBlob.size - }, - body: fileBlob - }); - - if (!uploadResponse.ok) { - const response = await uploadResponse.json(); - throw new Error(`HTTP error during file upload to ${user.userId}: status ${response.message}`); - } - - return {userId: user.userId, success: true, message: `Upload successful for user ${user.userId}`}; - } catch (error) { - console.error(`Failed to upload for user ${user.userId}:`, error); - return {userId: user.userId, success: false, message: `Upload failed for user ${user.userId}`}; - } - }); - - const results = await Promise.all(uploadPromises); - results.forEach(result => { - if (result.success) { - console.log(result.message); - } else { - console.error(result.message); - } - }); - console.log('All files processed. Check the console for detailed results.'); - } catch (error) { - console.error('An error occurred during uploads:', error); - } finally { - await window.electronAPI.deleteFile('usersDestTemp.json'); - } - } - - function timeout(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - await performUploads(); - await timeout(3000); - fadeOut('main_menu.html'); // Make sure this function is properly defined or available in your context -}); - diff --git a/User/src/renderer/js/share_file.js b/User/src/renderer/js/share_file.js deleted file mode 100644 index bf2717f..0000000 --- a/User/src/renderer/js/share_file.js +++ /dev/null @@ -1,139 +0,0 @@ -document.addEventListener("DOMContentLoaded", async function () { - let pathToFile = ''; - let serverIp = ''; - await window.electronAPI.readFile('ipConfig.json') - .then(result => { - const jsonData = JSON.parse(result.content); - serverIp = jsonData.ip; - }) - - function updateFileName() { - const fileNameElement = document.getElementById('fileName'); - if (pathToFile.trim() === '') { - fileNameElement.textContent = 'No file chosen'; - } else { - fileNameElement.textContent = pathToFile.split('\\').pop().split('/').pop(); - } - } - - updateFileName(); - - async function fetchUsersAndCreateCheckboxes() { - const result = await window.electronAPI.readFile('loginData.json'); - const loginData = JSON.parse(result.content); - - const {id} = loginData; - - fetch(`http://${serverIp}/users`, { - method: 'GET', - headers: { - 'x-api-key': 'uc_api' - } - }) - .then(response => response.json()) - .then(data => { - const usersDiv = document.querySelector('.choose_user_form_content'); - usersDiv.innerHTML = ''; - data['data'].forEach(user => { - if (user.id !== id) { - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.name = 'users'; - checkbox.value = user.id; - - const label = document.createElement('label'); - label.innerHTML = `${user.name}`; - label.insertBefore(checkbox, label.firstChild); // Insert checkbox before the label's first child - - usersDiv.appendChild(label); - } - }); - }) - .catch(error => console.error('Error fetching users:', error)); - } - - document.getElementById('selectFile').addEventListener('click', async function () { - console.log('Select file button clicked'); - - try { - pathToFile = await window.electronAPI.openFileDialog(); - updateFileName(); - } catch (error) { - console.error('Error opening file dialog:', error); - } - }); - - document.getElementById('backButton').addEventListener('click', async function () { - console.log('Back button clicked'); - fadeOut('main_menu.html'); - }); - - document.getElementById('submitButton').addEventListener('click', async function (event) { - event.preventDefault(); - console.log('Submit button clicked'); - - // Check if pathToFile has content - if (!pathToFile.trim()) { - await window.electronAPI.showAlert('File not chosen!') - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error)); - return; - } - - const form = document.getElementById('userDestForm'); - const checkboxes = form.querySelectorAll('input[name="users"]'); - const selectedUserIds = Array.from(checkboxes) - .filter(checkbox => checkbox.checked) - .map(checkbox => checkbox.value); - - if (!selectedUserIds.length) { - await window.electronAPI.showAlert('No user selected!') - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error showing alert:', error)); - return; - } - - const uploadData = { - filePath: pathToFile, // Use the pathToFile variable - users: [] - }; - - for (const userId of selectedUserIds) { - try { - const ipResponse = await fetch(`http://${serverIp}/backup_schemes/${userId}`,{ - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': 'uc_api', - } - }); - if (!ipResponse.ok) { - throw new Error(`Failed to fetch IP address for user ${userId}: status ${ipResponse.status}`); - } - const { data: destIp } = await ipResponse.json(); - uploadData.users.push({ - userId, - destIp, - destPort: 3000, // Static destination port - fileName: pathToFile.split('\\').pop().split('/').pop() - }); - } catch (error) { - console.error('Error fetching user data:', error); - } - } - - window.electronAPI.writeFile('usersDestTemp.json', JSON.stringify(uploadData, null, 2)) - .then(async () => { - console.log('File saved successfully'); - fadeOut('sending_file_confirmation.html'); - }) - .catch(error => console.error('Failed to save file:', error)); - }); - - function timeout(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - - fetchUsersAndCreateCheckboxes().then(() => console.log('Page rendered')) -}); diff --git a/User/src/renderer/js/sign_up_confirmation.js b/User/src/renderer/js/sign_up_confirmation.js deleted file mode 100644 index b443e84..0000000 --- a/User/src/renderer/js/sign_up_confirmation.js +++ /dev/null @@ -1,4 +0,0 @@ -document.addEventListener('DOMContentLoaded', async function () { - await new Promise(resolve => setTimeout(resolve, 2000)); - fadeOut('login.html'); -}); diff --git a/User/src/renderer/js/sign_up_departments.js b/User/src/renderer/js/sign_up_departments.js deleted file mode 100644 index c121496..0000000 --- a/User/src/renderer/js/sign_up_departments.js +++ /dev/null @@ -1,90 +0,0 @@ -document.addEventListener('DOMContentLoaded', async function () { - let ip = ''; - await window.electronAPI.readFile('ipConfig.json') - .then(result => { - const jsonData = JSON.parse(result.content); - ip = jsonData.ip; - }) - - await fetch(`http://${ip}/users/departments`, { - method: 'GET', - headers: { - 'x-api-key': 'uc_api' - } - }).then(async result => { - const res = await result.json(); - const data = res['data']; - - const formContent = document.querySelector('.signup-form-content'); - Object.entries(data).forEach(([key, department]) => { - if(department.name !== 'CEO') { - const label = document.createElement('label'); - label.innerHTML = `${department.name}`; - formContent.appendChild(label); - } - }); - }).catch(async error => { - await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error changing content:', error)); - }); - - document.getElementById('back').addEventListener('click', async function () { - fadeOut('sign_up_profile.html'); - }); - - // Handler for the continue button - document.getElementById('submit').addEventListener('click', async function (e) { - e.preventDefault(); - - await window.electronAPI.readFile('signupData.json') - .then(async result => { - const selectedDept = document.querySelector('input[name="dept"]:checked').value; - if (!selectedDept) { - throw new Error('No department had been selected!.'); - } - - const data = JSON.parse(result.content); - - const email = data.email; - const name = data.name; - const password = data.password; - - const userData = { - name: name, - email: email, - password: password, - department: selectedDept, - } - - await fetch(`http://${ip}/users/register`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': 'uc_api' - }, - body: JSON.stringify(userData) - }) - .then(async response => { - let data = await response.json(); - - if (!response.ok) { - console.log(data); - throw new Error(data.message); - } - - data = data.data; - userData['id'] = data.id; - - await window.electronAPI.writeFile('loginData.json', JSON.stringify(userData)); - fadeOut('sign_up_confirmation.html'); - }) - }) - .catch(async error => { - await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error changing content:', error)); - }) - }); -}) - diff --git a/User/src/renderer/js/sign_up_profile.js b/User/src/renderer/js/sign_up_profile.js deleted file mode 100644 index 2578b72..0000000 --- a/User/src/renderer/js/sign_up_profile.js +++ /dev/null @@ -1,71 +0,0 @@ -document.addEventListener('DOMContentLoaded', async function () { - const cancelButton = document.getElementById('login'); - const continueButton = document.getElementById('continue'); - - const signupDataExists = await window.electronAPI.checkFileExists('signupData.json'); - if (signupDataExists) { - await window.electronAPI.readFile('signupData.json') - .then(result => { - const signupData = JSON.parse(result.content); - - // Assign the values to the form inputs - if (signupData.email) document.querySelector('input[name="email"]').value = signupData.email; - if (signupData.name) document.querySelector('input[name="name"]').value = signupData.name; - if (signupData.password) document.querySelector('input[name="password"]').value = signupData.password; - }) - .catch(error => { - console.error('Error reading signup data:', error.message); - }); - } - - let ip = ''; - await window.electronAPI.readFile('ipConfig.json') - .then(result => { - const jsonData = JSON.parse(result.content); - ip = jsonData.ip; - }) - - cancelButton.addEventListener('click', function () { - console.log(`'Login' button clicked!`); - fadeOut('login.html'); - }); - - continueButton.addEventListener('click', async function (e) { - e.preventDefault(); - console.log('Continue button clicked'); - - const form = document.getElementById('signupForm'); - const formData = new FormData(form); - - const email = formData.get('email'); - - await fetch(`http://${ip}/users/validate_email`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': 'uc_api' - }, - body: JSON.stringify({ - email: email - }) - }).then(async response => { - if (!response.ok) { - const data = await response.json(); - throw new Error(data.message); - } - - }).then(async () => { - const formDataJSON = {}; - formData.forEach((value, key) => { - formDataJSON[key] = value; - }); - - await window.electronAPI.writeFile('signupData.json', JSON.stringify(formDataJSON)); - fadeOut('sign_up_departments.html'); - }).catch(async error => { - await window.electronAPI.showAlert(error.message) - .then(() => console.log('Alert window opened')) - .catch(error => console.error('Error changing content:', error)); - }) - }); -}); \ No newline at end of file diff --git a/User/src/renderer/js/transition.js b/User/src/renderer/js/transition.js deleted file mode 100644 index b5cb1c8..0000000 --- a/User/src/renderer/js/transition.js +++ /dev/null @@ -1,16 +0,0 @@ -function fadeIn() { - document.querySelector('.container').classList.remove('fade-out'); - document.querySelector('.container').classList.add('fade-in'); -} - -function fadeOut(destination) { - const container = document.querySelector('.container'); - container.classList.remove('fade-in'); - container.classList.add('fade-out'); - - container.addEventListener('animationend', async () => { - await window.electronAPI.changeContent(destination) - .then(() => console.log('Navigated to dashboard')) - .catch(error => console.error('Error navigating:', error)); - }); -} \ No newline at end of file diff --git a/User/src/workers/backup_retrieval_worker.ts b/User/src/workers/backup_retrieval_worker.ts new file mode 100644 index 0000000..6e55b4c --- /dev/null +++ b/User/src/workers/backup_retrieval_worker.ts @@ -0,0 +1,38 @@ +import { workerData, parentPort } from 'worker_threads'; +import { BackupRetrievalWorker} from '../helpers/backup_retrieval'; // Assuming the class is in the same folder + +// Destructure the data passed from the WorkerManager +const { + userConfigPath, + applicationInfoPath, + clientPort, + destinationPath +}: { + userConfigPath: string, + applicationInfoPath: string, + clientPort: number, + destinationPath: string +} = workerData; + +// Initialize the BackupRetrievalWorker +const backupRetrievalWorker = new BackupRetrievalWorker( + userConfigPath, + applicationInfoPath, + clientPort, + destinationPath +); + +// Start the backup retrieval process +backupRetrievalWorker.start() + .then(() => { + parentPort?.postMessage({ + success: true, + message: 'Backup retrieval completed successfully.' + }); + }) + .catch((error: any) => { + parentPort?.postMessage({ + success: false, + message: `Backup retrieval failed: ${error.message}` + }); + }); diff --git a/User/src/workers/resource_coordinator_worker.ts b/User/src/workers/resource_coordinator_worker.ts new file mode 100644 index 0000000..93d5856 --- /dev/null +++ b/User/src/workers/resource_coordinator_worker.ts @@ -0,0 +1,52 @@ +import { workerData } from 'worker_threads'; +import { UsersInfoFetcher } from '../helpers/users_info_fetcher'; +import {BackupManager} from "../helpers/backup_manager"; +import {FileSharer} from "../helpers/file_sharer"; +import {DepartmentSharer} from "../helpers/department_sharer"; + +// Destructure the required information from workerData +const { usersConfigPath, applicationInfoPath, memoryManagerPath, queueManagerPath, tcpPort } = workerData; + +const usersInfoFetcher = new UsersInfoFetcher(applicationInfoPath, memoryManagerPath, tcpPort); +usersInfoFetcher.start() + .then(() => { + console.log('Users Info Fetcher started successfully'); + }) + .catch((error: any) => { + console.error('Error starting Users Info Fetcher:', error); + }); + +const backupManager = new BackupManager( + usersConfigPath, + applicationInfoPath, + memoryManagerPath, + tcpPort +); + +backupManager.start() + .then(() => { + console.log('Backup Manager started successfully'); + }) + .catch((error: any) => { + console.error('Error starting Backup Manager:', error); + }); + +const fileSharer = new FileSharer(queueManagerPath, tcpPort); +fileSharer.start() + .then(() => { + console.log('File Sharer started successfully'); + }) + .catch((error: any) => { + console.error('Error starting File Sharer:', error); + }); + + +const departmentSharer = new DepartmentSharer(usersConfigPath, applicationInfoPath, memoryManagerPath, tcpPort); +departmentSharer.start() + .then(() => { + console.log('Department Sharer started successfully'); + }) + .catch((error: any) => { + console.error('Error starting Department Sharer:', error); + }); + diff --git a/User/src/workers/servers_worker.ts b/User/src/workers/servers_worker.ts new file mode 100644 index 0000000..4e6f4b6 --- /dev/null +++ b/User/src/workers/servers_worker.ts @@ -0,0 +1,14 @@ +import {UdpServer} from "../network/udp/udp_server"; +import {TcpServer} from "../network/tcp/tcp_server"; +import {workerData} from "worker_threads"; + +let udpServer: UdpServer | null = null; +let tcpServer: TcpServer | null = null; + +const {USER_UDP_PORT, USER_TCP_PORT, HOST} = workerData; + +udpServer = new UdpServer(HOST, USER_UDP_PORT); +udpServer.start(); + +tcpServer = new TcpServer(HOST, USER_TCP_PORT); +tcpServer.start(); \ No newline at end of file diff --git a/User/src/workers/watcher_worker.ts b/User/src/workers/watcher_worker.ts new file mode 100644 index 0000000..36a7262 --- /dev/null +++ b/User/src/workers/watcher_worker.ts @@ -0,0 +1,43 @@ +import {DirectoryWatcher} from "../helpers/directory_watcher"; +import {workerData} from "worker_threads"; + +const { + memoryManagerPath, + applicationInfoPath, +} = workerData; + +const backupDirectoryManager = new DirectoryWatcher(memoryManagerPath, applicationInfoPath, 'backupDirectory'); +const departmentShareManager = new DirectoryWatcher(memoryManagerPath, applicationInfoPath, 'departmentDirectory'); +const shareFileManager = new DirectoryWatcher(memoryManagerPath, applicationInfoPath, 'shareDirectory'); + +// Backup directory manager check loop +const backupIntervalId = setInterval(async () => { + if (await backupDirectoryManager.initialize()) { + clearInterval(backupIntervalId); // Stop the loop once initialized + console.log('BackupDirectoryManager successfully initialized.'); + } else { + console.log('Retrying BackupDirectoryManager initialization...'); + } +}, 60000); // Check every 60 seconds + + +// Department share manager check loop +const departmentIntervalId = setInterval(async () => { + if (await departmentShareManager.initialize()) { + clearInterval(departmentIntervalId); // Stop the loop once initialized + console.log('DepartmentShareManager successfully initialized.'); + } else { + console.log('Retrying DepartmentShareManager initialization...'); + } +}, 60000); // Check every 60 seconds + +// Department share manager check loop +const shareIntervalId = setInterval(async () => { + if (await shareFileManager.initialize()) { + clearInterval(shareIntervalId); // Stop the loop once initialized + console.log('DepartmentShareManager successfully initialized.'); + } else { + console.log('Retrying DepartmentShareManager initialization...'); + } +}, 60000); // Check every 60 seconds + diff --git a/User/tsconfig.json b/User/tsconfig.json new file mode 100644 index 0000000..a62789c --- /dev/null +++ b/User/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "module": "commonjs", + "target": "es6", + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "moduleResolution": "node" + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/User/usersInSystem.json b/User/usersInSystem.json deleted file mode 100644 index 0ca3084..0000000 Binary files a/User/usersInSystem.json and /dev/null differ diff --git a/dirs/ceo/backup_dir/fisiere/New Microsoft Excel Worksheet.xlsx b/dirs/ceo/backup_dir/fisiere/New Microsoft Excel Worksheet.xlsx deleted file mode 100644 index 0253ced..0000000 Binary files a/dirs/ceo/backup_dir/fisiere/New Microsoft Excel Worksheet.xlsx and /dev/null differ diff --git a/dirs/ceo/backup_dir/fisiere/New Microsoft Word Document.docx b/dirs/ceo/backup_dir/fisiere/New Microsoft Word Document.docx deleted file mode 100644 index ee863bf..0000000 Binary files a/dirs/ceo/backup_dir/fisiere/New Microsoft Word Document.docx and /dev/null differ diff --git a/dirs/user/backup_dir/fisiere/New Microsoft Excel Worksheet.xlsx b/dirs/user/backup_dir/fisiere/New Microsoft Excel Worksheet.xlsx deleted file mode 100644 index 0253ced..0000000 Binary files a/dirs/user/backup_dir/fisiere/New Microsoft Excel Worksheet.xlsx and /dev/null differ diff --git a/dirs/user/backup_dir/fisiere/New Microsoft Word Document.docx b/dirs/user/backup_dir/fisiere/New Microsoft Word Document.docx deleted file mode 100644 index ee863bf..0000000 Binary files a/dirs/user/backup_dir/fisiere/New Microsoft Word Document.docx and /dev/null differ diff --git a/dirs/user/department_dir/59c712d9-e6dd-438f-aec9-31ca2ae750e6/to_backup/alte fis/New Microsoft Excel Worksheet.xlsx b/dirs/user/department_dir/59c712d9-e6dd-438f-aec9-31ca2ae750e6/to_backup/alte fis/New Microsoft Excel Worksheet.xlsx deleted file mode 100644 index f760a7b..0000000 Binary files a/dirs/user/department_dir/59c712d9-e6dd-438f-aec9-31ca2ae750e6/to_backup/alte fis/New Microsoft Excel Worksheet.xlsx and /dev/null differ diff --git a/dirs/user/department_dir/59c712d9-e6dd-438f-aec9-31ca2ae750e6/to_backup/alte fis/alt fisier.txt b/dirs/user/department_dir/59c712d9-e6dd-438f-aec9-31ca2ae750e6/to_backup/alte fis/alt fisier.txt deleted file mode 100644 index e69de29..0000000 diff --git a/dirs/user/department_dir/59c712d9-e6dd-438f-aec9-31ca2ae750e6/to_backup/alte fis/fisier now.txt b/dirs/user/department_dir/59c712d9-e6dd-438f-aec9-31ca2ae750e6/to_backup/alte fis/fisier now.txt deleted file mode 100644 index e69de29..0000000 diff --git a/dirs/user/department_dir/59c712d9-e6dd-438f-aec9-31ca2ae750e6/to_backup/fisier_random.txt b/dirs/user/department_dir/59c712d9-e6dd-438f-aec9-31ca2ae750e6/to_backup/fisier_random.txt deleted file mode 100644 index e69de29..0000000