BACKEND DONE FOR ALL APPS
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/proiect-licenta.iml" filepath="$PROJECT_DIR$/.idea/proiect-licenta.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="WEB_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ChangeListManager">
|
|
||||||
<list default="true" id="2152dea8-6b25-43ba-8914-cdb9ea320ee7" name="Changes" comment="">
|
|
||||||
<change beforePath="$PROJECT_DIR$/NodejsConsoleApp1.sln" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/NodejsConsoleApp1/NodejsConsoleApp1.njsproj" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/NodejsConsoleApp1/README.md" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/NodejsConsoleApp1/app.js" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/NodejsConsoleApp1/package.json" beforeDir="false" />
|
|
||||||
</list>
|
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
||||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
||||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
||||||
</component>
|
|
||||||
<component name="Git.Settings">
|
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
|
||||||
</component>
|
|
||||||
<component name="MarkdownSettingsMigration">
|
|
||||||
<option name="stateVersion" value="1" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectColorInfo"><![CDATA[{
|
|
||||||
"customColor": "",
|
|
||||||
"associatedIndex": 7
|
|
||||||
}]]></component>
|
|
||||||
<component name="ProjectId" id="2eEtibIvxHojohW7av6S4GEmw3j" />
|
|
||||||
<component name="ProjectViewState">
|
|
||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
|
||||||
<option name="showLibraryContents" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="PropertiesComponent"><![CDATA[{
|
|
||||||
"keyToString": {
|
|
||||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
||||||
"last_opened_file_path": "C:/Users/Andrei Cerbu/Documents/FACULTATE/proiect-licenta/UC",
|
|
||||||
"nodejs_package_manager_path": "npm",
|
|
||||||
"vue.rearranger.settings.migration": "true"
|
|
||||||
}
|
|
||||||
}]]></component>
|
|
||||||
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
|
||||||
<component name="TaskManager">
|
|
||||||
<task active="true" id="Default" summary="Default task">
|
|
||||||
<changelist id="2152dea8-6b25-43ba-8914-cdb9ea320ee7" name="Changes" comment="" />
|
|
||||||
<created>1711481695345</created>
|
|
||||||
<option name="number" value="Default" />
|
|
||||||
<option name="presentableId" value="Default" />
|
|
||||||
<updated>1711481695345</updated>
|
|
||||||
<workItem from="1711481698534" duration="22000" />
|
|
||||||
</task>
|
|
||||||
<servers />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -1 +1 @@
|
|||||||
sending_file_confirmation.js
|
reset_password.js
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="WEB_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="EditorConfigUnexpectedComma" enabled="true" level="ERROR" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="JSDeprecatedSymbols" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectModuleManager">
|
<component name="ProjectModuleManager">
|
||||||
<modules>
|
<modules>
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/CEO.iml" filepath="$PROJECT_DIR$/.idea/CEO.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/proiect-licenta.iml" filepath="$PROJECT_DIR$/.idea/proiect-licenta.iml" />
|
||||||
</modules>
|
</modules>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"path": "/home/andrei/Desktop/proiect-licenta/dirs/ceo/share_dir"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
riº6Û‘Ñ»Û2d²–{ŽùãÇÛë%`nT´`ÐÄs
|
|
||||||
@@ -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 };
|
|
||||||
@@ -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}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/ϋ �Ν=η9Ϋ�υuό�DζΧH€α�ΫΐΎ[
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
睇アb�アィ ・�/釞@
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -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.`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -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 <script> <filePath>');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = process.argv[2]; // The third command-line argument is the file path
|
|
||||||
let serverIp = null
|
|
||||||
|
|
||||||
decryptAndGetServerIP(path.join(__dirname, '..', 'ipConfig.json')).then(ip => {
|
|
||||||
serverIp = ip;
|
|
||||||
});
|
|
||||||
|
|
||||||
uploadFile(filePath, serverIp).catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
process.exit(1); // Ensure to exit with status 1 in case of promise rejection
|
|
||||||
});
|
|
||||||
@@ -3,29 +3,26 @@
|
|||||||
"productName": "Aplicatie",
|
"productName": "Aplicatie",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Aplicatie P2P pentru stocarea resurselor digitale",
|
"description": "Aplicatie P2P pentru stocarea resurselor digitale",
|
||||||
"main": "src/main/main.js",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "node ./jobs/clean.js",
|
"clean": "del-cli dist",
|
||||||
"start": "electron --trace-warnings ./src/main/main.js",
|
"start": "tsc && copyfiles -u 1 'src/**/*' dist && electron dist/main/main.js"
|
||||||
"dev": "electronmon --trace-warnings ./src/main/main.js"
|
|
||||||
},
|
},
|
||||||
|
"main": "dist/main/main.js",
|
||||||
"author": "Cerbu Andrei - Mihnea",
|
"author": "Cerbu Andrei - Mihnea",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.8",
|
"dotenv": "^16.4.5",
|
||||||
"bootstrap": "^5.3.3",
|
"electron": "^32.0.1",
|
||||||
"electron": "^29.1.5",
|
"ping": "^0.4.4",
|
||||||
"express": "^4.19.2",
|
"uuid": "^10.0.0"
|
||||||
"form-data": "^4.0.0",
|
|
||||||
"fs-extra": "^11.2.0",
|
|
||||||
"ip": "^2.0.1",
|
|
||||||
"joi": "^17.12.3",
|
|
||||||
"multer": "^1.4.5-lts.1",
|
|
||||||
"nodemon": "^3.1.0",
|
|
||||||
"proper-lockfile": "^4.1.2",
|
|
||||||
"toastify-js": "^1.12.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electronmon": "^2.0.2"
|
"@types/electron": "^1.6.10",
|
||||||
|
"@types/node": "^22.6.1",
|
||||||
|
"@types/ping": "^0.4.4",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
|
"copyfiles": "^2.4.1",
|
||||||
|
"del-cli": "^5.0.0",
|
||||||
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 298 KiB After Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,176 @@
|
|||||||
|
/* Global styles for body and background */
|
||||||
|
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 starts hidden */
|
||||||
|
.container {
|
||||||
|
opacity: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container styling to match your previous design */
|
||||||
|
.left_block, .right_block {
|
||||||
|
background-color: rgba(83, 92, 145, 0.71);
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 10px 10px 10px rgba(0, 0, 0, 0.9);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left_block {
|
||||||
|
width: 45%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right_block {
|
||||||
|
width: 45%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enable scrolling in the departments container */
|
||||||
|
#departments-container {
|
||||||
|
max-height: 50vh; /* Limit the height of the container */
|
||||||
|
overflow-y: auto; /* Enable vertical scrolling */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling for departments container */
|
||||||
|
#departments-container::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#departments-container::-webkit-scrollbar-track {
|
||||||
|
background: #1B1A55;
|
||||||
|
}
|
||||||
|
|
||||||
|
#departments-container::-webkit-scrollbar-thumb {
|
||||||
|
background: #535C91;
|
||||||
|
border: 2px solid #1B1A55;
|
||||||
|
}
|
||||||
|
|
||||||
|
#departments-container::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form styling */
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
padding: 0.8rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[type="submit"], #createDepartmentButton {
|
||||||
|
background-color: #23BDEE;
|
||||||
|
color: white;
|
||||||
|
padding: 0.8rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[type="submit"]:hover, #createDepartmentButton:hover {
|
||||||
|
background-color: #1da1cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Department card styling */
|
||||||
|
.department-card {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #333;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-card button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
margin-left: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modify-button {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button {
|
||||||
|
background-color: #f44336;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal styles */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: white;
|
||||||
|
margin: 15% auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #888;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
color: #aaa;
|
||||||
|
float: right;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:hover, .close:focus {
|
||||||
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer style */
|
||||||
|
.footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
@@ -11,11 +11,9 @@ body, html {
|
|||||||
background-image: url('../assets/background.png');
|
background-image: url('../assets/background.png');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
|
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -46,6 +44,13 @@ body, html {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ceo-validation-form p {
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.form-actions {
|
.form-actions {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
@@ -87,7 +92,6 @@ input {
|
|||||||
|
|
||||||
.container {
|
.container {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
@@ -141,14 +145,11 @@ button {
|
|||||||
margin: 0 1rem 0 1rem;
|
margin: 0 1rem 0 1rem;
|
||||||
width: 15vw;
|
width: 15vw;
|
||||||
height: 10vh;
|
height: 10vh;
|
||||||
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
@@ -182,31 +183,29 @@ button:hover {
|
|||||||
margin: 1rem 2rem 2rem 3rem;
|
margin: 1rem 2rem 2rem 3rem;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
|
|
||||||
height: 40vh; /* Fixed height */
|
height: 40vh; /* Fixed height */
|
||||||
width: 80%; /* Full width */
|
width: 80%; /* Full width */
|
||||||
overflow: auto; /* Enable scrolling */
|
overflow: auto; /* Enable scrolling */
|
||||||
|
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notifications::-webkit-scrollbar {
|
.notifications::-webkit-scrollbar {
|
||||||
width: 10px; /* Reduced width of the scrollbar by 2px */
|
width: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notifications::-webkit-scrollbar-track {
|
.notifications::-webkit-scrollbar-track {
|
||||||
background: #1B1A55; /* Updated track color */
|
background: #1B1A55;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notifications::-webkit-scrollbar-thumb {
|
.notifications::-webkit-scrollbar-thumb {
|
||||||
background: #535C91; /* Updated handle color */
|
background: #535C91;
|
||||||
border: 2px solid #1B1A55; /* Adding a border to effectively reduce the thumb size */
|
border: 2px solid #1B1A55;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notifications::-webkit-scrollbar-thumb:hover {
|
.notifications::-webkit-scrollbar-thumb:hover {
|
||||||
background: #555; /* Updated handle color on hover */
|
background: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[name="logout"] {
|
button[name="logout"] {
|
||||||
@@ -231,3 +230,16 @@ button[name="menu_button"] {
|
|||||||
margin: 0.7rem;
|
margin: 0.7rem;
|
||||||
background-color: #1B1A55;
|
background-color: #1B1A55;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Disclaimer Modal Styles */
|
||||||
|
#disclaimer-modal {
|
||||||
|
display: none; /* Initially hidden */
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-button {
|
||||||
|
background-color: #f44336; /* Red color for cancel */
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button {
|
||||||
|
background-color: #4CAF50; /* Green color foQQr submit */
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ body, html {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
|
||||||
background-image: url('/images/background.png');
|
background-image: url('../assets/background.png');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ body, html {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header{
|
.header {
|
||||||
color: #1B1A55;
|
color: #1B1A55;
|
||||||
font-size: 4vh;
|
font-size: 4vh;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -41,23 +41,23 @@ body, html {
|
|||||||
width: 25%;
|
width: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form-title{
|
.login-form-title {
|
||||||
margin: 0 0 5vh 0;
|
margin: 0 0 5vh 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form-title h2{
|
.login-form-title h2 {
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
font-size: 5vh;
|
font-size: 5vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form hr{
|
.login-form hr {
|
||||||
width: 40%;
|
width: 40%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form-content{
|
.login-form-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -86,11 +86,11 @@ button {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover{
|
button:hover {
|
||||||
filter: brightness(85%);
|
filter: brightness(85%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form-footer{
|
.login-form-footer {
|
||||||
margin-top: 3vh;
|
margin-top: 3vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -104,7 +104,7 @@ button[name="submit"] {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[name="signin"] {
|
button[name="signup"] {
|
||||||
background-color: #2196F3;
|
background-color: #2196F3;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
@@ -3,29 +3,43 @@ body, html {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
font-family: "Roboto", sans-serif;
|
font-family: "Roboto", sans-serif;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
|
||||||
background-image: url('../assets/background.png');
|
background-image: url('../assets/background.png');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
opacity: 0;
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
opacity: 0; /* Make steps invisible by default */
|
||||||
|
visibility: hidden; /* Hide steps by default */
|
||||||
|
transition: opacity 0.5s ease, visibility 0.5s ease; /* Smooth transition */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.step.active {
|
||||||
|
opacity: 1; /* Show the active step */
|
||||||
|
visibility: visible; /* Make it visible */
|
||||||
|
}
|
||||||
|
|
||||||
.ip-form {
|
.header {
|
||||||
|
color: #1B1A55;
|
||||||
|
font-size: 4vh;
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: 2.5rem;
|
||||||
|
margin-bottom: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-form {
|
||||||
background-color: #535C91;
|
background-color: #535C91;
|
||||||
opacity: 71;
|
opacity: 71;
|
||||||
padding: 9vh 3vh 5vh;
|
padding: 9vh 3vh 5vh;
|
||||||
@@ -34,23 +48,23 @@ body, html {
|
|||||||
width: 25%;
|
width: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ip-form-title {
|
.signup-form-title {
|
||||||
margin: 0 0 5vh 0;
|
margin: 0 0 5vh 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ip-form-title h2 {
|
.signup-form-title h2 {
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
font-size: 5vh;
|
font-size: 5vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ip-form hr {
|
.signup-form hr {
|
||||||
width: 40%;
|
width: 40%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ip-form-content {
|
.signup-form-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -83,7 +97,7 @@ button:hover {
|
|||||||
filter: brightness(85%);
|
filter: brightness(85%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ip-form-footer {
|
.signup-form-footer {
|
||||||
margin-top: 3vh;
|
margin-top: 3vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -92,7 +106,12 @@ button:hover {
|
|||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[name="submit"] {
|
button[name="back"] {
|
||||||
background-color: #F44336;
|
background-color: #F44336;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button[name="continue"] {
|
||||||
|
background-color: #2196F3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
@@ -11,10 +11,11 @@ body, html {
|
|||||||
background-image: url('../assets/background.png');
|
background-image: url('../assets/background.png');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
@@ -22,50 +23,37 @@ body, html {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main_component {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
background-color: #535C91;
|
|
||||||
opacity: 71;
|
|
||||||
padding: 2vh;
|
|
||||||
border-radius: 1rem;
|
|
||||||
box-shadow: 10px 10px 10px rgba(0, 0, 0, 0.9);
|
|
||||||
width: 90vw;
|
|
||||||
height: 80vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
color: #1B1A55;
|
color: #1B1A55;
|
||||||
font-size: 0.8rem;
|
font-size: 4vh;
|
||||||
text-align: center;
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
.loading-section {
|
||||||
font-weight: bold;
|
display: flex;
|
||||||
width: 25%;
|
flex-direction: column;
|
||||||
font-size: 1rem;
|
align-items: center;
|
||||||
padding: 10px;
|
text-align: center;
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-top: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
filter: brightness(85%);
|
|
||||||
}
|
|
||||||
|
|
||||||
button[name="close"] {
|
|
||||||
background-color: #2196F3;
|
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border: 6px solid #f3f3f3;
|
||||||
|
border-top: 6px solid #2196F3;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-section p {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: white;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
/* Global styles for body and background */
|
||||||
|
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 starts hidden */
|
||||||
|
.container {
|
||||||
|
opacity: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container styling to match your previous design */
|
||||||
|
.user-management-container {
|
||||||
|
background-color: rgba(83, 92, 145, 0.71);
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 10px 10px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
color: white;
|
||||||
|
width: 80%;
|
||||||
|
max-width: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User card styling */
|
||||||
|
.user-card {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #333;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User name */
|
||||||
|
.user-name {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User department */
|
||||||
|
.user-department {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Delete button */
|
||||||
|
.delete-button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #f44336;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button:hover {
|
||||||
|
background-color: #d32f2f;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer buttons */
|
||||||
|
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%);
|
||||||
|
}
|
||||||
|
|
||||||
|
button[name="back"] {
|
||||||
|
background-color: #23BDEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer class */
|
||||||
|
.footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollable user container */
|
||||||
|
#users-container {
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#users-container::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#users-container::-webkit-scrollbar-track {
|
||||||
|
background: #1B1A55;
|
||||||
|
}
|
||||||
|
|
||||||
|
#users-container::-webkit-scrollbar-thumb {
|
||||||
|
background: #535C91;
|
||||||
|
border: 2px solid #1B1A55;
|
||||||
|
}
|
||||||
|
|
||||||
|
#users-container::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
@@ -11,10 +11,11 @@ body, html {
|
|||||||
background-image: url('../assets/background.png');
|
background-image: url('../assets/background.png');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
@@ -22,50 +23,37 @@ body, html {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1{
|
.header {
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main_component{
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
background-color: #535C91;
|
|
||||||
opacity: 71;
|
|
||||||
padding: 2vh;
|
|
||||||
border-radius: 1rem;
|
|
||||||
box-shadow: 10px 10px 10px rgba(0, 0, 0, 0.9);
|
|
||||||
width: 90vw;
|
|
||||||
height: 80vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header{
|
|
||||||
color: #1B1A55;
|
color: #1B1A55;
|
||||||
font-size: 0.8rem;
|
font-size: 4vh;
|
||||||
text-align: center;
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
.loading-section {
|
||||||
font-weight: bold;
|
display: flex;
|
||||||
width: 25%;
|
flex-direction: column;
|
||||||
font-size: 1rem;
|
align-items: center;
|
||||||
padding: 10px;
|
text-align: center;
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-top: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover{
|
|
||||||
filter: brightness(85%);
|
|
||||||
}
|
|
||||||
|
|
||||||
button[name="close"] {
|
|
||||||
background-color: #2196F3;
|
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border: 6px solid #f3f3f3;
|
||||||
|
border-top: 6px solid #2196F3;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-section p {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: white;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
|
<link href="../assets/hard-disk.ico" rel="icon" type="image/x-icon">
|
||||||
|
|
||||||
|
<!-- Reuse the same CSS files as UC Not Found -->
|
||||||
|
<link href="../css/welcome.css" rel="stylesheet">
|
||||||
|
<link href="../css/transition.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="../js/helpers.js"></script>
|
||||||
|
<title>Welcome</title>
|
||||||
|
</head>
|
||||||
|
<body onload="fadeIn()">
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>BACKUP FETCHER</h1>
|
||||||
|
</div>
|
||||||
|
<div class="loading-section">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>Backup is not searched, please wait...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
|
<link href="../assets/hard-disk.ico" rel="icon" type="image/x-icon">
|
||||||
|
|
||||||
|
<link href="../css/departments_management.css" rel="stylesheet">
|
||||||
|
<link href="../css/transition.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="../js/departments_management.js"></script>
|
||||||
|
<script src="../js/helpers.js"></script>
|
||||||
|
|
||||||
|
<title>Manage Departments</title>
|
||||||
|
</head>
|
||||||
|
<body onload="fadeIn()">
|
||||||
|
<div class="container">
|
||||||
|
<div class="left_block">
|
||||||
|
<h1>Departments</h1>
|
||||||
|
<div id="departments-container">
|
||||||
|
<!-- Department divs will be dynamically inserted here -->
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<button id="backButton" name="back" type="button">Back</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right_block">
|
||||||
|
<h1>Create New Department</h1>
|
||||||
|
<form id="new-department-form">
|
||||||
|
<label for="departmentName">Department Name:</label>
|
||||||
|
<input type="text" id="departmentName" name="departmentName" placeholder="Enter department name" required>
|
||||||
|
<button type="submit" id="createDepartmentButton">Create Department</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal for modifying department -->
|
||||||
|
<div id="modify-department-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span id="close-modal" class="close">×</span>
|
||||||
|
<h2>Modify Department</h2>
|
||||||
|
<label for="new-department-name">New Department Name:</label>
|
||||||
|
<input type="text" id="new-department-name" placeholder="Enter new department name" />
|
||||||
|
<button id="confirm-modify">Modify</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
|
<link href="../assets/hard-disk.ico" rel="icon" type="image/x-icon">
|
||||||
|
|
||||||
|
<link href="../css/login.css" rel="stylesheet">
|
||||||
|
<link href="../css/transition.css" rel="stylesheet">
|
||||||
|
<title>Login</title>
|
||||||
|
</head>
|
||||||
|
<body onload="fadeIn()">
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>DO WE KNOW</h1>
|
||||||
|
<h1>EACH OTHER?</h1>
|
||||||
|
</div>
|
||||||
|
<form class="login-form" id="loginForm">
|
||||||
|
<div class="login-form-title">
|
||||||
|
<h2>Login</h2>
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
<div class="login-form-content">
|
||||||
|
<input name="email" placeholder="Email" type="email">
|
||||||
|
<input name="password" placeholder="Password" type="password">
|
||||||
|
</div>
|
||||||
|
<div class="login-form-footer">
|
||||||
|
<button id="submit" name="submit" type="submit">Submit</button>
|
||||||
|
<button id="resetPassword" name="resetPassword" type="button">Reset Password</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../js/login.js"></script>
|
||||||
|
<script src="../js/helpers.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
|
<link href="../assets/hard-disk.ico" rel="icon" type="image/x-icon">
|
||||||
|
|
||||||
|
<link href="../css/main_menu.css" rel="stylesheet">
|
||||||
|
<link href="../css/transition.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="../js/main_menu.js"></script>
|
||||||
|
<script src="../js/helpers.js"></script>
|
||||||
|
|
||||||
|
<title>Main Page</title>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body onload="fadeIn()">
|
||||||
|
<div id="disclaimer-modal" class="overlay">
|
||||||
|
<div class="ceo-validation-form">
|
||||||
|
<h2>Are you sure?</h2>
|
||||||
|
<p>This operation cannot be reverted. Do you want to proceed with resetting the database?</p>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button id="disclaimer-cancel" class="cancel-button">Cancel</button>
|
||||||
|
<button id="disclaimer-submit" class="submit-button">Yes, Reset</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="left_block">
|
||||||
|
<div class="left_block_top">
|
||||||
|
<div>
|
||||||
|
<h1>WELCOME BACK,</h1>
|
||||||
|
<h1 id="username-field"></h1>
|
||||||
|
<h2>Hope you have a productive day!</h2>
|
||||||
|
</div>
|
||||||
|
<img alt="" src="../assets/user_1144760.png">
|
||||||
|
</div>
|
||||||
|
<div class="left_block_content">
|
||||||
|
<div class="left_block_buttons">
|
||||||
|
<button id="change-info" name="menu_button">Change your info</button>
|
||||||
|
<button id="share-file" name="menu_button">Share a file</button>
|
||||||
|
</div>
|
||||||
|
<div class="left_block_buttons">
|
||||||
|
<button id="backup-dir" name="menu_button">Set backup directory</button>
|
||||||
|
<button id="department-dir" name="menu_button">Set department directory</button>
|
||||||
|
<button id="share-dir" name="menu_button">Set share directory</button>
|
||||||
|
</div>
|
||||||
|
<div class="left_block_buttons">
|
||||||
|
<button id="manage-users" name="menu_button">Manage users</button>
|
||||||
|
<button id="manage-departments" name="menu_button">Manage departments</button>
|
||||||
|
</div>
|
||||||
|
<div class="left_block_buttons">
|
||||||
|
<button id="reset-database" name="menu_button">Reset database</button>
|
||||||
|
</div>
|
||||||
|
<div class="left_block_buttons">
|
||||||
|
<button id="restore-backup" name="menu_button">Restore backup</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="left_block_footer">
|
||||||
|
<button id="logout" name="logout">Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right_block">
|
||||||
|
<div>
|
||||||
|
<h1>NOTIFICATIONS</h1>
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
<div class="notifications" id="notifications">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<link href="../css/transition.css" rel="stylesheet">
|
<link href="../css/transition.css" rel="stylesheet">
|
||||||
|
|
||||||
<script src="../js/profile.js"></script>
|
<script src="../js/profile.js"></script>
|
||||||
<script src="../js/transition.js"></script>
|
<script src="../js/helpers.js"></script>
|
||||||
|
|
||||||
<title>Profile</title>
|
<title>Profile</title>
|
||||||
</head>
|
</head>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
|
<link href="../assets/hard-disk.ico" rel="icon" type="image/x-icon">
|
||||||
|
<link href="../css/reset_password.css" rel="stylesheet">
|
||||||
|
<link href="../css/transition.css" rel="stylesheet">
|
||||||
|
<title>Reset Password</title>
|
||||||
|
</head>
|
||||||
|
<body onload="fadeIn()">
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>RESET</h1>
|
||||||
|
<h1>YOUR PASSWORD</h1>
|
||||||
|
</div>
|
||||||
|
<form class="login-form" id="resetPasswordForm">
|
||||||
|
<div class="login-form-title">
|
||||||
|
<h2>Reset Password</h2>
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
<div class="login-form-content">
|
||||||
|
<input name="email" placeholder="Email" type="email" required>
|
||||||
|
<input name="newPassword" placeholder="New Password" type="password" required>
|
||||||
|
</div>
|
||||||
|
<div class="login-form-footer">
|
||||||
|
<button id="backToLogin" name="backToLogin" type="button">Back to Login</button>
|
||||||
|
<button id="resetPassword" name="resetPassword" type="submit">Reset Password</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../js/reset_password.js"></script>
|
||||||
|
<script src="../js/helpers.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<link href="../css/transition.css" rel="stylesheet">
|
<link href="../css/transition.css" rel="stylesheet">
|
||||||
|
|
||||||
<script src="../js/share_file.js"></script>
|
<script src="../js/share_file.js"></script>
|
||||||
<script src="../js/transition.js"></script>
|
<script src="../js/helpers.js"></script>
|
||||||
|
|
||||||
<title>Share File</title>
|
<title>Share File</title>
|
||||||
</head>
|
</head>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
|
<link href="../assets/hard-disk.ico" rel="icon" type="image/x-icon">
|
||||||
|
<link href="../css/transition.css" rel="stylesheet">
|
||||||
|
<link href="../css/sign_up.css" rel="stylesheet">
|
||||||
|
<title>Signup Process</title>
|
||||||
|
</head>
|
||||||
|
<body onload="fadeIn()">
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<!-- Step 1: Profile Information -->
|
||||||
|
<div class="step active" id="step1">
|
||||||
|
<div class="header">
|
||||||
|
<h1>LET US MEET</h1>
|
||||||
|
<h1>EACH OTHER</h1>
|
||||||
|
</div>
|
||||||
|
<form class="signup-form" id="signupForm">
|
||||||
|
<div class="signup-form-title">
|
||||||
|
<h2>Sign Up</h2>
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
<div class="signup-form-content">
|
||||||
|
<input name="email" placeholder="Email" type="email" required>
|
||||||
|
<input name="name" placeholder="Username" type="text" required>
|
||||||
|
<input name="password" placeholder="Password" type="password" required>
|
||||||
|
</div>
|
||||||
|
<div class="signup-form-footer">
|
||||||
|
<button id="backToLogin" name="back" type="button">Cancel</button>
|
||||||
|
<button id="nextToStep2" name="continue" type="submit">Next</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: Department Selection -->
|
||||||
|
<div class="step" id="step2" style="display: none;">
|
||||||
|
<div class="header">
|
||||||
|
<h1>TELL ME MORE</h1>
|
||||||
|
<h1>ABOUT</h1>
|
||||||
|
<h1>YOUR WORK</h1>
|
||||||
|
</div>
|
||||||
|
<form class="signup-form" id="departmentForm">
|
||||||
|
<div class="signup-form-title">
|
||||||
|
<h2>Choose your department</h2>
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
<div class="signup-form-content" id="departmentList">
|
||||||
|
<!-- List of departments will be dynamically inserted here -->
|
||||||
|
</div>
|
||||||
|
<div class="signup-form-footer">
|
||||||
|
<button id="backToStep1" name="back" type="button">Back</button>
|
||||||
|
<button id="submitSignup" name="continue" type="submit">Submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../js/sign_up.js"></script>
|
||||||
|
<script src="../js/helpers.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -5,20 +5,22 @@
|
|||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
<link href="../assets/hard-disk.ico" rel="icon" type="image/x-icon">
|
<link href="../assets/hard-disk.ico" rel="icon" type="image/x-icon">
|
||||||
|
|
||||||
<link href="../css/sign_up_confirmation.css" rel="stylesheet">
|
<link href="../css/uc_not_found.css" rel="stylesheet">
|
||||||
<link href="../css/transition.css" rel="stylesheet">
|
<link href="../css/transition.css" rel="stylesheet">
|
||||||
|
|
||||||
<script src="../js/sign_up_confirmation.js"></script>
|
<script src="../js/helpers.js"></script>
|
||||||
<script src="../js/transition.js"></script>
|
|
||||||
|
|
||||||
<title>Setup Completion</title>
|
<title>UC Not Found</title>
|
||||||
</head>
|
</head>
|
||||||
<body onload="fadeIn()">
|
<body onload="fadeIn()">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>ALL THE SETUP IS DONE!</h1>
|
<h1>SEARCHING FOR THE</h1>
|
||||||
<h2>LET’S PROCEED TO THE</h2>
|
<h1>SERVER...</h1>
|
||||||
<h2>LOGIN PAGE</h2>
|
</div>
|
||||||
|
<div class="loading-section">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>Please wait while we try to connect to the server.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
|
<link href="../assets/hard-disk.ico" rel="icon" type="image/x-icon">
|
||||||
|
|
||||||
|
<link href="../css/users_management.css" rel="stylesheet">
|
||||||
|
<link href="../css/transition.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="../js/helpers.js"></script>
|
||||||
|
<script src="../js/users_management.js"></script>
|
||||||
|
|
||||||
|
<title>Manage Users</title>
|
||||||
|
</head>
|
||||||
|
<body onload="fadeIn()">
|
||||||
|
<div class="container">
|
||||||
|
<div class="user-management-container">
|
||||||
|
<h1>Manage Users</h1>
|
||||||
|
<div id="users-container">
|
||||||
|
<!-- User divs will be dynamically inserted here -->
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<button id="backButton" name="back" type="button">Back</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
|
<link href="../assets/hard-disk.ico" rel="icon" type="image/x-icon">
|
||||||
|
|
||||||
|
<!-- Reuse the same CSS files as UC Not Found -->
|
||||||
|
<link href="../css/welcome.css" rel="stylesheet">
|
||||||
|
<link href="../css/transition.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="../js/helpers.js"></script>
|
||||||
|
<title>Welcome</title>
|
||||||
|
</head>
|
||||||
|
<body onload="fadeIn()">
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>WELCOME TO THE APP</h1>
|
||||||
|
</div>
|
||||||
|
<div class="loading-section">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>Initializing the application, please wait...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
|
<link href="../assets/hard-disk.ico" rel="icon" type="image/x-icon">
|
||||||
|
|
||||||
|
<!-- Reuse the same CSS files as UC Not Found -->
|
||||||
|
<link href="../css/welcome.css" rel="stylesheet">
|
||||||
|
<link href="../css/transition.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="../js/helpers.js"></script>
|
||||||
|
<title>Welcome</title>
|
||||||
|
</head>
|
||||||
|
<body onload="fadeIn()">
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>BACKUP FETCHER</h1>
|
||||||
|
</div>
|
||||||
|
<div class="loading-section">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>Backup is not searched, please wait...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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'));
|
||||||
|
});
|
||||||
@@ -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 = `
|
||||||
|
<input type="radio" name="dept" value="${department.id}">
|
||||||
|
${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)
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
@@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
// 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<void> {
|
||||||
|
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<boolean> {
|
||||||
|
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<any> {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<void> {
|
||||||
|
// 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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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<any> {
|
||||||
|
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<boolean> {
|
||||||
|
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<any> {
|
||||||
|
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<boolean> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<boolean> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<FileSendTask>;
|
||||||
|
private readonly clientPort: number;
|
||||||
|
|
||||||
|
constructor(queueFilePath: string, clientPort: number) {
|
||||||
|
|
||||||
|
|
||||||
|
this.queueManager = new QueueManager<FileItemTask>(queueFilePath, compareFnFileItemTask);
|
||||||
|
this.clientPort = clientPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start processing the file queue
|
||||||
|
async start(): Promise<void> {
|
||||||
|
setInterval(async () => {
|
||||||
|
await this.processQueue(); // Process the queue at regular intervals
|
||||||
|
}, 10000); // 10 seconds interval
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to process the queue
|
||||||
|
private async processQueue(): Promise<void> {
|
||||||
|
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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<void> {
|
||||||
|
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<any | null> {
|
||||||
|
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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<string> {
|
||||||
|
const generate = async (): Promise<string> => {
|
||||||
|
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<string> {
|
||||||
|
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<any | null> {
|
||||||
|
return this.readValue(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update meta information by merging new data into existing data
|
||||||
|
public async updateMetaInformation(guid: string, newMetaInfo: any): Promise<boolean> {
|
||||||
|
return await this.writeValue(guid, newMetaInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove meta information using the GUID
|
||||||
|
public removeMetaInformation(guid: string): Promise<boolean> {
|
||||||
|
return this.removeValue(guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export class QueueManager<T> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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<void> {
|
||||||
|
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<boolean> {
|
||||||
|
if (!this.tcpClient || !this.tcpClient.isSocketConnected()) return true;
|
||||||
|
this.tcpClient.closeSocket();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<void> {
|
||||||
|
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<any> {
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<string | undefined> {
|
||||||
|
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<void> {
|
||||||
|
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<string | undefined> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
interface RegisteredClient {
|
||||||
|
id: string;
|
||||||
|
ip: string;
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
@@ -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.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -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<FileItemTask> | 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<void> {
|
||||||
|
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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
});
|
|
||||||
@@ -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<any> => ipcRenderer.invoke('read-user-json-files', key),
|
||||||
|
writeUserConfig: (key: string, value: any): Promise<boolean> => ipcRenderer.invoke('write-user-json-files', key, value),
|
||||||
|
removeUserConfig: (key: string) : Promise<boolean> => ipcRenderer.invoke('remove-user-json-files', key),
|
||||||
|
resetUserConfig: (): Promise<boolean> => ipcRenderer.invoke('reset-user-json-files'),
|
||||||
|
|
||||||
|
// ApplicationPreferences methods
|
||||||
|
readApplicationInfo: (key: string): Promise<any> => ipcRenderer.invoke('read-application-json-files', key),
|
||||||
|
writeApplicationInfo: (key: string, value: any): Promise<boolean> => ipcRenderer.invoke('write-application-json-files', key, value),
|
||||||
|
removeApplicationInfo: (key: string) : Promise<boolean> => ipcRenderer.invoke('remove-application-preferences', key),
|
||||||
|
resetApplicationInfo: (): Promise<boolean> => ipcRenderer.invoke('reset-application-json-files'),
|
||||||
|
|
||||||
|
// UcCommunication methods
|
||||||
|
openUcSocket: (): Promise<any> => ipcRenderer.invoke('open-socket'),
|
||||||
|
sendUcMessage: (operationCode: string, metaInfo: any, fileContent: any): Promise<any> => ipcRenderer.invoke('send-message', operationCode, metaInfo, fileContent),
|
||||||
|
closeUcSocket: (): Promise<any> => ipcRenderer.invoke('close-socket'),
|
||||||
|
hasResponseArrived: (): Promise<boolean> => ipcRenderer.invoke('has-response-arrived'),
|
||||||
|
getLastUcResult: (): Promise<any> => ipcRenderer.invoke('get-last-result'),
|
||||||
|
getOperationsCodes: (): Promise<{ data: { [key: string]: string } }> => ipcRenderer.invoke('get-operation-codes'),
|
||||||
|
|
||||||
|
// MemoryManager methods
|
||||||
|
createMemoryEntry: (): Promise<string> => ipcRenderer.invoke('memory-create-entry'),
|
||||||
|
readMemoryEntry: (id: string): Promise<any> => ipcRenderer.invoke('memory-read-entry', id),
|
||||||
|
updateMemoryEntry: (id: string, data: any): Promise<boolean> => ipcRenderer.invoke('memory-update-entry', id, data),
|
||||||
|
removeMemoryEntry: (id: string): Promise<boolean> => ipcRenderer.invoke('memory-remove-entry', id),
|
||||||
|
|
||||||
|
// UI methods
|
||||||
|
showAlert: (message: string): Promise<void> => ipcRenderer.invoke('show-alert', message),
|
||||||
|
changeContent: (destination: string): Promise<void> => ipcRenderer.invoke('change-content', destination),
|
||||||
|
selectDirectory: (): Promise<string | undefined> => ipcRenderer.invoke('select-directory'),
|
||||||
|
selectFile: (): Promise<string | undefined> => ipcRenderer.invoke('select-file'),
|
||||||
|
showFileInExplorer: (path: string): Promise<void> => ipcRenderer.invoke('show-file-in-explorer', path),
|
||||||
|
|
||||||
|
// Queue methods
|
||||||
|
addTaskToSendFileQueue: (task: FileItemTask): Promise<void> => ipcRenderer.invoke('add-task-to-send-file-queue', task),
|
||||||
|
|
||||||
|
// BackupRetrievalWorker
|
||||||
|
startBackupRetrieval: (destinationPath: string): Promise<void> => ipcRenderer.invoke('start-backup-retrieval', destinationPath)
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { UdpClient} from './udp/udp_client';
|
||||||
|
export { TcpClient } from './tcp/tcp_client'
|
||||||
@@ -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',
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// operation_plugin.ts
|
||||||
|
import { OperationHandler } from './operation_handler';
|
||||||
|
|
||||||
|
export interface OperationPlugin {
|
||||||
|
register(operationHandler: OperationHandler): void;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 = '<EOM>'; // 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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 = '<EOM>'; // 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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<boolean> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||