From ab1eaec4138d8db92cccbd783f4cbd5468b310dc Mon Sep 17 00:00:00 2001 From: andrei-mihnea-cerbu Date: Mon, 21 Oct 2024 18:34:45 +0300 Subject: [PATCH] BACKEND DONE FOR ALL APPS --- .idea/modules.xml | 8 - .idea/proiect-licenta.iml | 12 - .idea/vcs.xml | 6 - .idea/workspace.xml | 52 - CEO/.env | 4 + CEO/.idea/.name | 2 +- CEO/.idea/CEO.iml | 12 - .../inspectionProfiles/Project_Default.xml | 7 - CEO/.idea/modules.xml | 2 +- CEO/.idea/vcs.xml | 1 + CEO/backupSchemes.json | 12 - CEO/dirBackup.json | 15 - CEO/dirShare.json | 3 - CEO/filesReceived.json | 1 - CEO/helpers/http_status.js | 36 - CEO/helpers/lock_mechanism.js | 33 - CEO/ipConfig.json | 1 - CEO/iv.key | 1 - CEO/jobs/backup.js | 364 --- CEO/jobs/clean.js | 28 - CEO/jobs/external_endpoints.js | 192 -- CEO/jobs/fetcher.js | 234 -- CEO/jobs/send_file.js | 90 - CEO/loginData.json | Bin 176 -> 0 bytes CEO/package-lock.json | 1555 +++-------- CEO/package.json | 31 +- .../renderer => render}/assets/background.png | Bin .../renderer => render}/assets/hard-disk.ico | Bin .../renderer => render}/assets/hard-disk.png | Bin .../renderer => render}/assets/loading.gif | Bin .../assets/user_1144760.png | Bin CEO/render/css/departments_management.css | 176 ++ .../src/renderer => CEO/render}/css/login.css | 0 .../renderer => render}/css/main_menu.css | 40 +- .../renderer => CEO/render}/css/profile.css | 0 .../render/css/reset_password.css | 18 +- .../render}/css/share_file.css | 0 .../render/css/sign_up.css | 43 +- .../renderer => render}/css/transition.css | 0 .../render/css/uc_not_found.css | 70 +- CEO/render/css/users_management.css | 141 + .../css/welcome.css} | 72 +- CEO/render/html/backup_retreive.html | 26 + CEO/render/html/departments_management.html | 48 + CEO/render/html/login.html | 37 + CEO/render/html/main_menu.html | 74 + .../renderer => CEO/render}/html/profile.html | 2 +- CEO/render/html/reset_password.html | 36 + .../render}/html/share_file.html | 2 +- CEO/render/html/sign_up.html | 63 + .../render/html/uc_not_found.html | 16 +- CEO/render/html/users_management.html | 29 + CEO/render/html/welcome.html | 26 + CEO/render/js/backup_retreive.html | 26 + CEO/render/js/departments_management.js | 216 ++ CEO/render/js/helpers.js | 35 + CEO/render/js/login.js | 144 + CEO/render/js/main_menu.js | 233 ++ CEO/render/js/profile.js | 82 + CEO/render/js/reset_password.js | 63 + CEO/render/js/share_file.js | 118 + CEO/render/js/sign_up.js | 134 + CEO/render/js/users_management.js | 142 + CEO/secret.key | Bin 32 -> 0 bytes CEO/src/helpers/backup_manager.ts | 205 ++ CEO/src/helpers/backup_retrieval.ts | 224 ++ CEO/src/helpers/department_sharer.ts | 278 ++ CEO/src/helpers/directory_watcher.ts | 129 + CEO/src/helpers/file_encryptor.ts | 55 + CEO/src/helpers/file_sharer.ts | 152 + CEO/src/helpers/json_manager.ts | 119 + CEO/src/helpers/memory_manager.ts | 47 + CEO/src/helpers/queue_manager.ts | 137 + CEO/src/helpers/task_scheduler.ts | 76 + CEO/src/helpers/tcp_communicator.ts | 81 + CEO/src/helpers/users_info_fetcher.ts | 111 + CEO/src/helpers/window_manager.ts | 92 + CEO/src/helpers/worker_manager.ts | 155 ++ CEO/src/interfaces/file_item_task.ts | 8 + CEO/src/interfaces/pool_request.ts | 15 + CEO/src/interfaces/registered_client.ts | 5 + CEO/src/main/aes_encrypt.js | 115 - CEO/src/main/main.js | 422 --- CEO/src/main/main.ts | 299 ++ CEO/src/main/preload.js | 21 - CEO/src/main/preload.ts | 43 + CEO/src/network/connection_manager.ts | 46 + CEO/src/network/message_handler.ts | 65 + CEO/src/network/network.ts | 2 + CEO/src/network/operation_codes.ts | 37 + .../network/operations_base/operation_base.ts | 43 + .../operations_base/operation_handler.ts | 58 + .../operations_base/operation_plugin.ts | 6 + .../operations_custom/general_operations.ts | 80 + .../user_to_user_operations.ts | 500 ++++ .../socket_communicator_base.ts | 21 + .../tcp_client_communicator.ts | 167 ++ .../tcp_server_communicator.ts | 161 ++ .../udp_socket_communicator.ts | 39 + CEO/src/network/tcp/tcp_client.ts | 100 + CEO/src/network/tcp/tcp_server.ts | 111 + CEO/src/network/udp/udp_client.ts | 140 + CEO/src/network/udp/udp_server.ts | 64 + CEO/src/renderer/css/ip_submit.css | 98 - CEO/src/renderer/css/manage_departments.css | 201 -- CEO/src/renderer/css/manage_users.css | 121 - .../css/sending_file_confirmation.css | 39 - CEO/src/renderer/html/alert_modal.html | 21 - CEO/src/renderer/html/decrypting_backup.html | 32 - CEO/src/renderer/html/ip_submit.html | 32 - CEO/src/renderer/html/login.html | 36 - CEO/src/renderer/html/main_menu.html | 61 - CEO/src/renderer/html/manage_departments.html | 47 - CEO/src/renderer/html/manage_users.html | 32 - CEO/src/renderer/html/profile.html | 34 - .../html/sending_file_confirmation.html | 26 - CEO/src/renderer/js/alert_modal.js | 11 - CEO/src/renderer/js/ip_submit.js | 34 - CEO/src/renderer/js/login.js | 90 - CEO/src/renderer/js/main_menu.js | 205 -- CEO/src/renderer/js/manage_departments.js | 175 -- CEO/src/renderer/js/manage_users.js | 94 - CEO/src/renderer/js/profile.js | 74 - .../renderer/js/sending_file_confirmation.js | 65 - CEO/src/renderer/js/share_file.js | 139 - CEO/src/renderer/js/transition.js | 16 - CEO/src/workers/backup_retrieval_worker.ts | 38 + .../workers/resource_coordinator_worker.ts | 52 + CEO/src/workers/servers_worker.ts | 14 + CEO/src/workers/watcher_worker.ts | 43 + CEO/tsconfig.json | 17 + CEO/usersInSystem.json | Bin 336 -> 0 bytes UC/.env | 6 + UC/backend/.idea/.gitignore | 5 - UC/backend/.idea/.name | 1 - UC/backend/.idea/UC.iml | 12 - UC/backend/.idea/backend.iml | 12 - UC/backend/.idea/jsLibraryMappings.xml | 6 - UC/backend/.idea/misc.xml | 6 - UC/backend/.idea/modules.xml | 8 - .../.idea/runConfigurations/bin_www.xml | 11 - UC/backend/.idea/vcs.xml | 8 - UC/backend/README.md | 45 - UC/backend/package-lock.json | 1811 ------------ UC/backend/package.json | 30 - UC/backend/src/app.js | 70 - UC/backend/src/config.json | 3 - UC/backend/src/db/backup_schemes.json | 12 - UC/backend/src/db/ceo.json | 7 - UC/backend/src/db/departments.json | 10 - UC/backend/src/db/jsonDatabaseManager.js | 91 - UC/backend/src/db/users.json | 9 - UC/backend/src/helpers/httpResponses.js | 38 - .../src/middlewares/apiKeyValidation.js | 11 - UC/backend/src/middlewares/checkJson.js | 16 - UC/backend/src/models/ceoModifyModel.js | 15 - UC/backend/src/models/ceoPasswordModel.js | 10 - .../src/models/departmentRegisterModel.js | 10 - UC/backend/src/models/dirStructureModel.js | 23 - .../src/models/emailVerificationModel.js | 11 - UC/backend/src/models/schemaMapper.js | 25 - UC/backend/src/models/securityLevelsModel.js | 19 - .../src/models/userDepartmentPatchModel.js | 15 - UC/backend/src/models/usersLoginModel.js | 15 - UC/backend/src/models/usersModifyModel.js | 21 - UC/backend/src/models/usersRegisterModel.js | 25 - UC/backend/src/routes/admin.js | 85 - UC/backend/src/routes/backup_schemes.js | 63 - UC/backend/src/routes/ceo.js | 196 -- UC/backend/src/routes/users.js | 210 -- UC/backend/task_uri.md | 21 - UC/db/database.db | Bin 0 -> 36864 bytes UC/frontend/.idea/.gitignore | 5 - UC/frontend/.idea/frontend.iml | 12 - UC/frontend/.idea/jsLibraryMappings.xml | 6 - UC/frontend/.idea/modules.xml | 8 - .../.idea/runConfigurations/bin_www.xml | 11 - UC/frontend/.idea/vcs.xml | 6 - UC/frontend/README.md | 32 - UC/frontend/app.js | 55 - UC/frontend/config/credentials.json | 6 - UC/frontend/helpers/status_codes.js | 11 - UC/frontend/middlewares/checkSession.js | 22 - UC/frontend/models/login.js | 8 - UC/frontend/models/schemas.js | 7 - UC/frontend/package-lock.json | 1151 -------- UC/frontend/package.json | 22 - UC/frontend/public/css/main_menu.css | 118 - UC/frontend/public/html/login.html | 50 - UC/frontend/public/html/main_menu.html | 59 - UC/frontend/public/js/login.js | 38 - UC/frontend/public/js/main_menu.js | 199 -- UC/frontend/routers/login.js | 41 - UC/frontend/routers/mainMenu.js | 46 - UC/package-lock.json | 2088 ++++++++++++++ UC/package.json | 31 + UC/src/db_managers/db.ts | 15 + UC/src/db_managers/department_database.ts | 116 + UC/src/db_managers/key_database.ts | 164 ++ UC/src/db_managers/sql_lite_database.ts | 36 + UC/src/db_managers/user_database.ts | 235 ++ UC/src/index.ts | 25 + UC/src/network/connection_manager.ts | 46 + UC/src/network/message_handler.ts | 65 + .../network/operations_base/operation_base.ts | 43 + .../operations_base/operation_handler.ts | 63 + .../operations_base/operation_plugin.ts | 6 + .../operations_custom/auth_operations.ts | 183 ++ .../operations_custom/ceo_operations.ts | 42 + .../department_operations.ts | 129 + .../operations_custom/general_operations.ts | 79 + .../operations_custom/key_operations.ts | 90 + .../operations_custom/user_operations.ts | 136 + .../socket_communicator_base.ts | 21 + .../tcp_server_communicator.ts | 161 ++ .../udp_socket_communicator.ts | 39 + UC/src/tcp_server.ts | 121 + UC/src/udp_server.ts | 67 + UC/tsconfig.json | 110 + User/.env | 4 + User/.idea/.name | 2 +- User/backupSchemes.json | 12 - User/dirBackup.json | 15 - User/dirDepartment.json | 3 - User/dirShare.json | 3 - User/helpers/http_status.js | 36 - User/helpers/lock_mechanism.js | 33 - User/ipConfig.json | 1 - User/iv.key | 1 - User/jobs/backup.js | 365 --- User/jobs/clean.js | 28 - User/jobs/external_endpoints.js | 192 -- User/jobs/fetcher.js | 233 -- User/jobs/send_file.js | 90 - User/loginData.json | 1 - User/package-lock.json | 2474 +++++++++-------- User/package.json | 34 +- .../render/assets}/background.png | Bin .../render/assets}/hard-disk.ico | Bin .../render/assets}/hard-disk.png | Bin .../renderer => render}/assets/loading.gif | Bin .../render/assets}/user_1144760.png | Bin .../renderer => User/render}/css/login.css | 18 +- .../renderer => render}/css/main_menu.css | 0 .../renderer => User/render}/css/profile.css | 12 +- .../css/reset_password.css} | 65 +- .../render}/css/share_file.css | 38 +- User/render/css/sign_up.css | 117 + .../public => User/render}/css/transition.css | 0 User/render/css/uc_not_found.css | 59 + User/render/css/welcome.css | 59 + User/render/html/backup_retreive.html | 26 + User/render/html/login.html | 38 + .../renderer => render}/html/main_menu.html | 27 +- User/render/html/profile.html | 35 + User/render/html/reset_password.html | 36 + .../render}/html/share_file.html | 19 +- User/render/html/sign_up.html | 63 + .../html/uc_not_found.html} | 17 +- User/render/html/welcome.html | 26 + User/render/js/helpers.js | 36 + User/render/js/login.js | 150 + User/render/js/main_menu.js | 216 ++ User/render/js/profile.js | 82 + User/render/js/reset_password.js | 63 + User/render/js/share_file.js | 118 + User/render/js/sign_up.js | 134 + User/secret.key | 1 - User/src/helpers/backup_manager.ts | 205 ++ User/src/helpers/backup_retrieval.ts | 224 ++ User/src/helpers/department_sharer.ts | 278 ++ User/src/helpers/directory_watcher.ts | 129 + User/src/helpers/file_encryptor.ts | 55 + User/src/helpers/file_sharer.ts | 152 + User/src/helpers/json_manager.ts | 119 + User/src/helpers/memory_manager.ts | 47 + User/src/helpers/queue_manager.ts | 137 + User/src/helpers/task_scheduler.ts | 76 + User/src/helpers/tcp_communicator.ts | 81 + User/src/helpers/users_info_fetcher.ts | 111 + User/src/helpers/window_manager.ts | 92 + User/src/helpers/worker_manager.ts | 155 ++ User/src/interfaces/file_item_task.ts | 8 + User/src/interfaces/pool_request.ts | 15 + User/src/interfaces/registered_client.ts | 5 + User/src/main/aes_encrypt.js | 115 - User/src/main/main.js | 422 --- User/src/main/main.ts | 299 ++ User/src/main/preload.js | 21 - User/src/main/preload.ts | 43 + User/src/network/connection_manager.ts | 46 + User/src/network/message_handler.ts | 65 + User/src/network/network.ts | 2 + User/src/network/operation_codes.ts | 39 + .../network/operations_base/operation_base.ts | 43 + .../operations_base/operation_handler.ts | 58 + .../operations_base/operation_plugin.ts | 6 + .../operations_custom/general_operations.ts | 80 + .../user_to_user_operations.ts | 500 ++++ .../socket_communicator_base.ts | 21 + .../tcp_client_communicator.ts | 167 ++ .../tcp_server_communicator.ts | 161 ++ .../udp_socket_communicator.ts | 39 + User/src/network/tcp/tcp_client.ts | 100 + User/src/network/tcp/tcp_server.ts | 111 + User/src/network/udp/udp_client.ts | 140 + User/src/network/udp/udp_server.ts | 64 + User/src/renderer/assets/background.png | Bin 304850 -> 0 bytes User/src/renderer/assets/hard-disk.ico | Bin 4286 -> 0 bytes User/src/renderer/assets/hard-disk.png | Bin 19479 -> 0 bytes User/src/renderer/assets/user_1144760.png | Bin 30529 -> 0 bytes User/src/renderer/css/change_department.css | 99 - .../css/sending_file_confirmation.css | 39 - .../src/renderer/css/sign_up_confirmation.css | 34 - User/src/renderer/css/sign_up_department.css | 129 - User/src/renderer/css/transition.css | 25 - User/src/renderer/html/alert_modal.html | 21 - User/src/renderer/html/change_department.html | 33 - User/src/renderer/html/decrypting_backup.html | 32 - User/src/renderer/html/ip_submit.html | 32 - User/src/renderer/html/login.html | 38 - .../renderer/html/sign_up_departments.html | 38 - User/src/renderer/html/sign_up_profile.html | 37 - User/src/renderer/js/alert_modal.js | 11 - User/src/renderer/js/change_department.js | 80 - User/src/renderer/js/ip_submit.js | 34 - User/src/renderer/js/login.js | 98 - User/src/renderer/js/main_menu.js | 289 -- User/src/renderer/js/profile.js | 77 - .../renderer/js/sending_file_confirmation.js | 65 - User/src/renderer/js/share_file.js | 139 - User/src/renderer/js/sign_up_confirmation.js | 4 - User/src/renderer/js/sign_up_departments.js | 90 - User/src/renderer/js/sign_up_profile.js | 71 - User/src/renderer/js/transition.js | 16 - User/src/workers/backup_retrieval_worker.ts | 38 + .../workers/resource_coordinator_worker.ts | 52 + User/src/workers/servers_worker.ts | 14 + User/src/workers/watcher_worker.ts | 43 + User/tsconfig.json | 17 + User/usersInSystem.json | Bin 336 -> 0 bytes .../New Microsoft Excel Worksheet.xlsx | Bin 6191 -> 0 bytes .../fisiere/New Microsoft Word Document.docx | Bin 13172 -> 0 bytes .../New Microsoft Excel Worksheet.xlsx | Bin 6191 -> 0 bytes .../fisiere/New Microsoft Word Document.docx | Bin 13172 -> 0 bytes .../New Microsoft Excel Worksheet.xlsx | Bin 8482 -> 0 bytes .../to_backup/alte fis/alt fisier.txt | 0 .../to_backup/alte fis/fisier now.txt | 0 .../to_backup/fisier_random.txt | 0 349 files changed, 17199 insertions(+), 14015 deletions(-) delete mode 100644 .idea/modules.xml delete mode 100644 .idea/proiect-licenta.iml delete mode 100644 .idea/vcs.xml delete mode 100644 .idea/workspace.xml create mode 100644 CEO/.env delete mode 100644 CEO/.idea/CEO.iml delete mode 100644 CEO/.idea/inspectionProfiles/Project_Default.xml delete mode 100644 CEO/backupSchemes.json delete mode 100644 CEO/dirBackup.json delete mode 100644 CEO/dirShare.json delete mode 100644 CEO/filesReceived.json delete mode 100644 CEO/helpers/http_status.js delete mode 100644 CEO/helpers/lock_mechanism.js delete mode 100644 CEO/ipConfig.json delete mode 100644 CEO/iv.key delete mode 100644 CEO/jobs/backup.js delete mode 100644 CEO/jobs/clean.js delete mode 100644 CEO/jobs/external_endpoints.js delete mode 100644 CEO/jobs/fetcher.js delete mode 100644 CEO/jobs/send_file.js delete mode 100644 CEO/loginData.json rename CEO/{src/renderer => render}/assets/background.png (100%) rename CEO/{src/renderer => render}/assets/hard-disk.ico (100%) rename CEO/{src/renderer => render}/assets/hard-disk.png (100%) rename CEO/{src/renderer => render}/assets/loading.gif (100%) rename CEO/{src/renderer => render}/assets/user_1144760.png (100%) create mode 100644 CEO/render/css/departments_management.css rename {User/src/renderer => CEO/render}/css/login.css (100%) rename CEO/{src/renderer => render}/css/main_menu.css (88%) rename {User/src/renderer => CEO/render}/css/profile.css (100%) rename UC/frontend/public/css/login.css => CEO/render/css/reset_password.css (88%) rename {User/src/renderer => CEO/render}/css/share_file.css (100%) rename User/src/renderer/css/ip_submit.css => CEO/render/css/sign_up.css (66%) rename CEO/{src/renderer => render}/css/transition.css (100%) rename User/src/renderer/css/alert_modal.css => CEO/render/css/uc_not_found.css (51%) create mode 100644 CEO/render/css/users_management.css rename CEO/{src/renderer/css/alert_modal.css => render/css/welcome.css} (51%) create mode 100644 CEO/render/html/backup_retreive.html create mode 100644 CEO/render/html/departments_management.html create mode 100644 CEO/render/html/login.html create mode 100644 CEO/render/html/main_menu.html rename {User/src/renderer => CEO/render}/html/profile.html (95%) create mode 100644 CEO/render/html/reset_password.html rename {User/src/renderer => CEO/render}/html/share_file.html (97%) create mode 100644 CEO/render/html/sign_up.html rename User/src/renderer/html/sign_up_confirmation.html => CEO/render/html/uc_not_found.html (53%) create mode 100644 CEO/render/html/users_management.html create mode 100644 CEO/render/html/welcome.html create mode 100644 CEO/render/js/backup_retreive.html create mode 100644 CEO/render/js/departments_management.js create mode 100644 CEO/render/js/helpers.js create mode 100644 CEO/render/js/login.js create mode 100644 CEO/render/js/main_menu.js create mode 100644 CEO/render/js/profile.js create mode 100644 CEO/render/js/reset_password.js create mode 100644 CEO/render/js/share_file.js create mode 100644 CEO/render/js/sign_up.js create mode 100644 CEO/render/js/users_management.js delete mode 100644 CEO/secret.key create mode 100644 CEO/src/helpers/backup_manager.ts create mode 100644 CEO/src/helpers/backup_retrieval.ts create mode 100644 CEO/src/helpers/department_sharer.ts create mode 100644 CEO/src/helpers/directory_watcher.ts create mode 100644 CEO/src/helpers/file_encryptor.ts create mode 100644 CEO/src/helpers/file_sharer.ts create mode 100644 CEO/src/helpers/json_manager.ts create mode 100644 CEO/src/helpers/memory_manager.ts create mode 100644 CEO/src/helpers/queue_manager.ts create mode 100644 CEO/src/helpers/task_scheduler.ts create mode 100644 CEO/src/helpers/tcp_communicator.ts create mode 100644 CEO/src/helpers/users_info_fetcher.ts create mode 100644 CEO/src/helpers/window_manager.ts create mode 100644 CEO/src/helpers/worker_manager.ts create mode 100644 CEO/src/interfaces/file_item_task.ts create mode 100644 CEO/src/interfaces/pool_request.ts create mode 100644 CEO/src/interfaces/registered_client.ts delete mode 100644 CEO/src/main/aes_encrypt.js delete mode 100644 CEO/src/main/main.js create mode 100644 CEO/src/main/main.ts delete mode 100644 CEO/src/main/preload.js create mode 100644 CEO/src/main/preload.ts create mode 100644 CEO/src/network/connection_manager.ts create mode 100644 CEO/src/network/message_handler.ts create mode 100644 CEO/src/network/network.ts create mode 100644 CEO/src/network/operation_codes.ts create mode 100644 CEO/src/network/operations_base/operation_base.ts create mode 100644 CEO/src/network/operations_base/operation_handler.ts create mode 100644 CEO/src/network/operations_base/operation_plugin.ts create mode 100644 CEO/src/network/operations_custom/general_operations.ts create mode 100644 CEO/src/network/operations_custom/user_to_user_operations.ts create mode 100644 CEO/src/network/socket_communicator/socket_communicator_base.ts create mode 100644 CEO/src/network/socket_communicator/tcp_client_communicator.ts create mode 100644 CEO/src/network/socket_communicator/tcp_server_communicator.ts create mode 100644 CEO/src/network/socket_communicator/udp_socket_communicator.ts create mode 100644 CEO/src/network/tcp/tcp_client.ts create mode 100644 CEO/src/network/tcp/tcp_server.ts create mode 100644 CEO/src/network/udp/udp_client.ts create mode 100644 CEO/src/network/udp/udp_server.ts delete mode 100644 CEO/src/renderer/css/ip_submit.css delete mode 100644 CEO/src/renderer/css/manage_departments.css delete mode 100644 CEO/src/renderer/css/manage_users.css delete mode 100644 CEO/src/renderer/css/sending_file_confirmation.css delete mode 100644 CEO/src/renderer/html/alert_modal.html delete mode 100644 CEO/src/renderer/html/decrypting_backup.html delete mode 100644 CEO/src/renderer/html/ip_submit.html delete mode 100644 CEO/src/renderer/html/login.html delete mode 100644 CEO/src/renderer/html/main_menu.html delete mode 100644 CEO/src/renderer/html/manage_departments.html delete mode 100644 CEO/src/renderer/html/manage_users.html delete mode 100644 CEO/src/renderer/html/profile.html delete mode 100644 CEO/src/renderer/html/sending_file_confirmation.html delete mode 100644 CEO/src/renderer/js/alert_modal.js delete mode 100644 CEO/src/renderer/js/ip_submit.js delete mode 100644 CEO/src/renderer/js/login.js delete mode 100644 CEO/src/renderer/js/main_menu.js delete mode 100644 CEO/src/renderer/js/manage_departments.js delete mode 100644 CEO/src/renderer/js/manage_users.js delete mode 100644 CEO/src/renderer/js/profile.js delete mode 100644 CEO/src/renderer/js/sending_file_confirmation.js delete mode 100644 CEO/src/renderer/js/share_file.js delete mode 100644 CEO/src/renderer/js/transition.js create mode 100644 CEO/src/workers/backup_retrieval_worker.ts create mode 100644 CEO/src/workers/resource_coordinator_worker.ts create mode 100644 CEO/src/workers/servers_worker.ts create mode 100644 CEO/src/workers/watcher_worker.ts create mode 100644 CEO/tsconfig.json delete mode 100644 CEO/usersInSystem.json create mode 100644 UC/.env delete mode 100644 UC/backend/.idea/.gitignore delete mode 100644 UC/backend/.idea/.name delete mode 100644 UC/backend/.idea/UC.iml delete mode 100644 UC/backend/.idea/backend.iml delete mode 100644 UC/backend/.idea/jsLibraryMappings.xml delete mode 100644 UC/backend/.idea/misc.xml delete mode 100644 UC/backend/.idea/modules.xml delete mode 100644 UC/backend/.idea/runConfigurations/bin_www.xml delete mode 100644 UC/backend/.idea/vcs.xml delete mode 100644 UC/backend/README.md delete mode 100644 UC/backend/package-lock.json delete mode 100644 UC/backend/package.json delete mode 100644 UC/backend/src/app.js delete mode 100644 UC/backend/src/config.json delete mode 100644 UC/backend/src/db/backup_schemes.json delete mode 100644 UC/backend/src/db/ceo.json delete mode 100644 UC/backend/src/db/departments.json delete mode 100644 UC/backend/src/db/jsonDatabaseManager.js delete mode 100644 UC/backend/src/db/users.json delete mode 100644 UC/backend/src/helpers/httpResponses.js delete mode 100644 UC/backend/src/middlewares/apiKeyValidation.js delete mode 100644 UC/backend/src/middlewares/checkJson.js delete mode 100644 UC/backend/src/models/ceoModifyModel.js delete mode 100644 UC/backend/src/models/ceoPasswordModel.js delete mode 100644 UC/backend/src/models/departmentRegisterModel.js delete mode 100644 UC/backend/src/models/dirStructureModel.js delete mode 100644 UC/backend/src/models/emailVerificationModel.js delete mode 100644 UC/backend/src/models/schemaMapper.js delete mode 100644 UC/backend/src/models/securityLevelsModel.js delete mode 100644 UC/backend/src/models/userDepartmentPatchModel.js delete mode 100644 UC/backend/src/models/usersLoginModel.js delete mode 100644 UC/backend/src/models/usersModifyModel.js delete mode 100644 UC/backend/src/models/usersRegisterModel.js delete mode 100644 UC/backend/src/routes/admin.js delete mode 100644 UC/backend/src/routes/backup_schemes.js delete mode 100644 UC/backend/src/routes/ceo.js delete mode 100644 UC/backend/src/routes/users.js delete mode 100644 UC/backend/task_uri.md create mode 100644 UC/db/database.db delete mode 100644 UC/frontend/.idea/.gitignore delete mode 100644 UC/frontend/.idea/frontend.iml delete mode 100644 UC/frontend/.idea/jsLibraryMappings.xml delete mode 100644 UC/frontend/.idea/modules.xml delete mode 100644 UC/frontend/.idea/runConfigurations/bin_www.xml delete mode 100644 UC/frontend/.idea/vcs.xml delete mode 100644 UC/frontend/README.md delete mode 100644 UC/frontend/app.js delete mode 100644 UC/frontend/config/credentials.json delete mode 100644 UC/frontend/helpers/status_codes.js delete mode 100644 UC/frontend/middlewares/checkSession.js delete mode 100644 UC/frontend/models/login.js delete mode 100644 UC/frontend/models/schemas.js delete mode 100644 UC/frontend/package-lock.json delete mode 100644 UC/frontend/package.json delete mode 100644 UC/frontend/public/css/main_menu.css delete mode 100644 UC/frontend/public/html/login.html delete mode 100644 UC/frontend/public/html/main_menu.html delete mode 100644 UC/frontend/public/js/login.js delete mode 100644 UC/frontend/public/js/main_menu.js delete mode 100644 UC/frontend/routers/login.js delete mode 100644 UC/frontend/routers/mainMenu.js create mode 100644 UC/package-lock.json create mode 100644 UC/package.json create mode 100644 UC/src/db_managers/db.ts create mode 100644 UC/src/db_managers/department_database.ts create mode 100644 UC/src/db_managers/key_database.ts create mode 100644 UC/src/db_managers/sql_lite_database.ts create mode 100644 UC/src/db_managers/user_database.ts create mode 100644 UC/src/index.ts create mode 100644 UC/src/network/connection_manager.ts create mode 100644 UC/src/network/message_handler.ts create mode 100644 UC/src/network/operations_base/operation_base.ts create mode 100644 UC/src/network/operations_base/operation_handler.ts create mode 100644 UC/src/network/operations_base/operation_plugin.ts create mode 100644 UC/src/network/operations_custom/auth_operations.ts create mode 100644 UC/src/network/operations_custom/ceo_operations.ts create mode 100644 UC/src/network/operations_custom/department_operations.ts create mode 100644 UC/src/network/operations_custom/general_operations.ts create mode 100644 UC/src/network/operations_custom/key_operations.ts create mode 100644 UC/src/network/operations_custom/user_operations.ts create mode 100644 UC/src/network/socket_communicator/socket_communicator_base.ts create mode 100644 UC/src/network/socket_communicator/tcp_server_communicator.ts create mode 100644 UC/src/network/socket_communicator/udp_socket_communicator.ts create mode 100644 UC/src/tcp_server.ts create mode 100644 UC/src/udp_server.ts create mode 100644 UC/tsconfig.json create mode 100644 User/.env delete mode 100644 User/backupSchemes.json delete mode 100644 User/dirBackup.json delete mode 100644 User/dirDepartment.json delete mode 100644 User/dirShare.json delete mode 100644 User/helpers/http_status.js delete mode 100644 User/helpers/lock_mechanism.js delete mode 100644 User/ipConfig.json delete mode 100644 User/iv.key delete mode 100644 User/jobs/backup.js delete mode 100644 User/jobs/clean.js delete mode 100644 User/jobs/external_endpoints.js delete mode 100644 User/jobs/fetcher.js delete mode 100644 User/jobs/send_file.js delete mode 100644 User/loginData.json rename {UC/frontend/public/images => User/render/assets}/background.png (100%) rename {UC/frontend/public/images => User/render/assets}/hard-disk.ico (100%) rename {UC/frontend/public/images => User/render/assets}/hard-disk.png (100%) rename User/{src/renderer => render}/assets/loading.gif (100%) rename {UC/frontend/public/images => User/render/assets}/user_1144760.png (100%) rename {CEO/src/renderer => User/render}/css/login.css (90%) rename User/{src/renderer => render}/css/main_menu.css (100%) rename {CEO/src/renderer => User/render}/css/profile.css (93%) rename User/{src/renderer/css/sing_up_profile.css => render/css/reset_password.css} (84%) rename {CEO/src/renderer => User/render}/css/share_file.css (89%) create mode 100644 User/render/css/sign_up.css rename {UC/frontend/public => User/render}/css/transition.css (100%) create mode 100644 User/render/css/uc_not_found.css create mode 100644 User/render/css/welcome.css create mode 100644 User/render/html/backup_retreive.html create mode 100644 User/render/html/login.html rename User/{src/renderer => render}/html/main_menu.html (58%) create mode 100644 User/render/html/profile.html create mode 100644 User/render/html/reset_password.html rename {CEO/src/renderer => User/render}/html/share_file.html (65%) create mode 100644 User/render/html/sign_up.html rename User/{src/renderer/html/sending_file_confirmation.html => render/html/uc_not_found.html} (52%) create mode 100644 User/render/html/welcome.html create mode 100644 User/render/js/helpers.js create mode 100644 User/render/js/login.js create mode 100644 User/render/js/main_menu.js create mode 100644 User/render/js/profile.js create mode 100644 User/render/js/reset_password.js create mode 100644 User/render/js/share_file.js create mode 100644 User/render/js/sign_up.js delete mode 100644 User/secret.key create mode 100644 User/src/helpers/backup_manager.ts create mode 100644 User/src/helpers/backup_retrieval.ts create mode 100644 User/src/helpers/department_sharer.ts create mode 100644 User/src/helpers/directory_watcher.ts create mode 100644 User/src/helpers/file_encryptor.ts create mode 100644 User/src/helpers/file_sharer.ts create mode 100644 User/src/helpers/json_manager.ts create mode 100644 User/src/helpers/memory_manager.ts create mode 100644 User/src/helpers/queue_manager.ts create mode 100644 User/src/helpers/task_scheduler.ts create mode 100644 User/src/helpers/tcp_communicator.ts create mode 100644 User/src/helpers/users_info_fetcher.ts create mode 100644 User/src/helpers/window_manager.ts create mode 100644 User/src/helpers/worker_manager.ts create mode 100644 User/src/interfaces/file_item_task.ts create mode 100644 User/src/interfaces/pool_request.ts create mode 100644 User/src/interfaces/registered_client.ts delete mode 100644 User/src/main/aes_encrypt.js delete mode 100644 User/src/main/main.js create mode 100644 User/src/main/main.ts delete mode 100644 User/src/main/preload.js create mode 100644 User/src/main/preload.ts create mode 100644 User/src/network/connection_manager.ts create mode 100644 User/src/network/message_handler.ts create mode 100644 User/src/network/network.ts create mode 100644 User/src/network/operation_codes.ts create mode 100644 User/src/network/operations_base/operation_base.ts create mode 100644 User/src/network/operations_base/operation_handler.ts create mode 100644 User/src/network/operations_base/operation_plugin.ts create mode 100644 User/src/network/operations_custom/general_operations.ts create mode 100644 User/src/network/operations_custom/user_to_user_operations.ts create mode 100644 User/src/network/socket_communicator/socket_communicator_base.ts create mode 100644 User/src/network/socket_communicator/tcp_client_communicator.ts create mode 100644 User/src/network/socket_communicator/tcp_server_communicator.ts create mode 100644 User/src/network/socket_communicator/udp_socket_communicator.ts create mode 100644 User/src/network/tcp/tcp_client.ts create mode 100644 User/src/network/tcp/tcp_server.ts create mode 100644 User/src/network/udp/udp_client.ts create mode 100644 User/src/network/udp/udp_server.ts delete mode 100644 User/src/renderer/assets/background.png delete mode 100644 User/src/renderer/assets/hard-disk.ico delete mode 100644 User/src/renderer/assets/hard-disk.png delete mode 100644 User/src/renderer/assets/user_1144760.png delete mode 100644 User/src/renderer/css/change_department.css delete mode 100644 User/src/renderer/css/sending_file_confirmation.css delete mode 100644 User/src/renderer/css/sign_up_confirmation.css delete mode 100644 User/src/renderer/css/sign_up_department.css delete mode 100644 User/src/renderer/css/transition.css delete mode 100644 User/src/renderer/html/alert_modal.html delete mode 100644 User/src/renderer/html/change_department.html delete mode 100644 User/src/renderer/html/decrypting_backup.html delete mode 100644 User/src/renderer/html/ip_submit.html delete mode 100644 User/src/renderer/html/login.html delete mode 100644 User/src/renderer/html/sign_up_departments.html delete mode 100644 User/src/renderer/html/sign_up_profile.html delete mode 100644 User/src/renderer/js/alert_modal.js delete mode 100644 User/src/renderer/js/change_department.js delete mode 100644 User/src/renderer/js/ip_submit.js delete mode 100644 User/src/renderer/js/login.js delete mode 100644 User/src/renderer/js/main_menu.js delete mode 100644 User/src/renderer/js/profile.js delete mode 100644 User/src/renderer/js/sending_file_confirmation.js delete mode 100644 User/src/renderer/js/share_file.js delete mode 100644 User/src/renderer/js/sign_up_confirmation.js delete mode 100644 User/src/renderer/js/sign_up_departments.js delete mode 100644 User/src/renderer/js/sign_up_profile.js delete mode 100644 User/src/renderer/js/transition.js create mode 100644 User/src/workers/backup_retrieval_worker.ts create mode 100644 User/src/workers/resource_coordinator_worker.ts create mode 100644 User/src/workers/servers_worker.ts create mode 100644 User/src/workers/watcher_worker.ts create mode 100644 User/tsconfig.json delete mode 100644 User/usersInSystem.json delete mode 100644 dirs/ceo/backup_dir/fisiere/New Microsoft Excel Worksheet.xlsx delete mode 100644 dirs/ceo/backup_dir/fisiere/New Microsoft Word Document.docx delete mode 100644 dirs/user/backup_dir/fisiere/New Microsoft Excel Worksheet.xlsx delete mode 100644 dirs/user/backup_dir/fisiere/New Microsoft Word Document.docx delete mode 100644 dirs/user/department_dir/59c712d9-e6dd-438f-aec9-31ca2ae750e6/to_backup/alte fis/New Microsoft Excel Worksheet.xlsx delete mode 100644 dirs/user/department_dir/59c712d9-e6dd-438f-aec9-31ca2ae750e6/to_backup/alte fis/alt fisier.txt delete mode 100644 dirs/user/department_dir/59c712d9-e6dd-438f-aec9-31ca2ae750e6/to_backup/alte fis/fisier now.txt delete mode 100644 dirs/user/department_dir/59c712d9-e6dd-438f-aec9-31ca2ae750e6/to_backup/fisier_random.txt diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 202146e..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/proiect-licenta.iml b/.idea/proiect-licenta.iml deleted file mode 100644 index 24643cc..0000000 --- a/.idea/proiect-licenta.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index a9b0bd6..0000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - 1711481695345 - - - - \ No newline at end of file diff --git a/CEO/.env b/CEO/.env new file mode 100644 index 0000000..c411426 --- /dev/null +++ b/CEO/.env @@ -0,0 +1,4 @@ +UDP_PORT=41234 +TCP_PORT=41233 + +HOST=0.0.0.0 \ No newline at end of file diff --git a/CEO/.idea/.name b/CEO/.idea/.name index 12fdd11..d8fcf6c 100644 --- a/CEO/.idea/.name +++ b/CEO/.idea/.name @@ -1 +1 @@ -sending_file_confirmation.js \ No newline at end of file +reset_password.js \ No newline at end of file diff --git a/CEO/.idea/CEO.iml b/CEO/.idea/CEO.iml deleted file mode 100644 index 24643cc..0000000 --- a/CEO/.idea/CEO.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/CEO/.idea/inspectionProfiles/Project_Default.xml b/CEO/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 80292f2..0000000 --- a/CEO/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/CEO/.idea/modules.xml b/CEO/.idea/modules.xml index 24cc0a4..202146e 100644 --- a/CEO/.idea/modules.xml +++ b/CEO/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/CEO/.idea/vcs.xml b/CEO/.idea/vcs.xml index 6c0b863..62bd7a0 100644 --- a/CEO/.idea/vcs.xml +++ b/CEO/.idea/vcs.xml @@ -1,6 +1,7 @@ + \ No newline at end of file diff --git a/CEO/backupSchemes.json b/CEO/backupSchemes.json deleted file mode 100644 index b9cbe93..0000000 --- a/CEO/backupSchemes.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "7c0ef9d5-23ed-47a6-bfb4-fdcfce436904": { - "ip": "192.168.169.19", - "directoryStructure": "{\"backup_dir\":{\"files\":[],\"fisiere\":{\"files\":[\"New Microsoft Excel Worksheet.xlsx\",\"New Microsoft Word Document.docx\"]}}}", - "totalSize": 19363 - }, - "59c712d9-e6dd-438f-aec9-31ca2ae750e6": { - "ip": "192.168.169.53", - "directoryStructure": "{\"backup_dir\":{\"files\":[],\"fisiere\":{\"files\":[\"New Microsoft Excel Worksheet.xlsx\",\"New Microsoft Word Document.docx\"]}}}", - "totalSize": 19363 - } -} \ No newline at end of file diff --git a/CEO/dirBackup.json b/CEO/dirBackup.json deleted file mode 100644 index a1c8356..0000000 --- a/CEO/dirBackup.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "path": "/home/andrei/Desktop/proiect-licenta/dirs/ceo/backup_dir", - "structure": { - "backup_dir": { - "files": [], - "fisiere": { - "files": [ - "New Microsoft Excel Worksheet.xlsx", - "New Microsoft Word Document.docx" - ] - } - } - }, - "size": 19363 -} \ No newline at end of file diff --git a/CEO/dirShare.json b/CEO/dirShare.json deleted file mode 100644 index f07ccf8..0000000 --- a/CEO/dirShare.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "path": "/home/andrei/Desktop/proiect-licenta/dirs/ceo/share_dir" -} \ No newline at end of file diff --git a/CEO/filesReceived.json b/CEO/filesReceived.json deleted file mode 100644 index 45ae4b1..0000000 --- a/CEO/filesReceived.json +++ /dev/null @@ -1 +0,0 @@ -ri6ۑѻ2d{%`nT`s \ No newline at end of file diff --git a/CEO/helpers/http_status.js b/CEO/helpers/http_status.js deleted file mode 100644 index b03796e..0000000 --- a/CEO/helpers/http_status.js +++ /dev/null @@ -1,36 +0,0 @@ -const httpStatus = { - // Informational - CONTINUE: 100, - SWITCHING_PROTOCOLS: 101, - PROCESSING: 102, - - // Success - OK: 200, - CREATED: 201, - ACCEPTED: 202, - NO_CONTENT: 204, - - // Redirection - MOVED_PERMANENTLY: 301, - FOUND: 302, - SEE_OTHER: 303, - NOT_MODIFIED: 304, - TEMPORARY_REDIRECT: 307, - - // Client Error - BAD_REQUEST: 400, - UNAUTHORIZED: 401, - FORBIDDEN: 403, - NOT_FOUND: 404, - METHOD_NOT_ALLOWED: 405, - CONFLICT: 409, - GONE: 410, - UNSUPPORTED_MEDIA_TYPE: 415, - - // Server Error - INTERNAL_SERVER_ERROR: 500, - NOT_IMPLEMENTED: 501, - SERVICE_UNAVAILABLE: 503 -}; - -module.exports = { httpStatus }; diff --git a/CEO/helpers/lock_mechanism.js b/CEO/helpers/lock_mechanism.js deleted file mode 100644 index 2d55fc5..0000000 --- a/CEO/helpers/lock_mechanism.js +++ /dev/null @@ -1,33 +0,0 @@ -const lockfile = require('proper-lockfile'); - -async function lockFile(filePath) { - try { - await lockfile.lock(filePath, { - realpath: false, - retries: { - retries: 10, // Number of retries - factor: 2, // The exponential factor - minTimeout: 1000, // The number of milliseconds before starting the first retry - maxTimeout: 5000, // The maximum number of milliseconds between two retries - randomize: true, // Randomizes the timeouts by multiplying with a factor between 1 to 2 - } - }); - console.log(`File locked: ${filePath}`); - } catch (error) { - console.error(`Error locking file ${filePath}: ${error.message}`); - throw error; // Propagate the error if unable to lock after retries - } -} - -async function unlockFile(filePath) { - try { - await lockfile.unlock(filePath); - console.log(`File unlocked: ${filePath}`); - } catch (error) { - console.error(`Error unlocking file ${filePath}: ${error.message}`); - // Decide whether to throw the error or not, based on your error handling strategy - throw error; - } -} - -module.exports = {lockFile, unlockFile} \ No newline at end of file diff --git a/CEO/ipConfig.json b/CEO/ipConfig.json deleted file mode 100644 index adbb4e0..0000000 --- a/CEO/ipConfig.json +++ /dev/null @@ -1 +0,0 @@ -/ =9 uDH[ \ No newline at end of file diff --git a/CEO/iv.key b/CEO/iv.key deleted file mode 100644 index a8859e3..0000000 --- a/CEO/iv.key +++ /dev/null @@ -1 +0,0 @@ -DZb /@ \ No newline at end of file diff --git a/CEO/jobs/backup.js b/CEO/jobs/backup.js deleted file mode 100644 index 16cedcf..0000000 --- a/CEO/jobs/backup.js +++ /dev/null @@ -1,364 +0,0 @@ -const fsPromises = require('fs').promises; // Ensure you use the promise-based API -const fs = require('fs'); -const path = require('path'); -const { decryptFileInPlace, encryptFileInPlace, encryptFileWithKey, decryptFileWithKey} = require("../src/main/aes_encrypt"); -const {lockFile, unlockFile} = require("../helpers/lock_mechanism"); - - -async function decryptAndGetServerIP(filePath) { - try { - await lockFile(filePath); - await decryptFileInPlace(filePath); - - const fileContent = await fs.promises.readFile(filePath, 'utf8'); - const jsonData = JSON.parse(fileContent); - const serverIP = jsonData.ip; - - await encryptFileInPlace(filePath) - await unlockFile(filePath); - return serverIP; - } catch (error) { - console.error('An error occurred:', error); - return null; - } -} - -function extractFilePaths(directoryStructure) { - let paths = []; - const traverse = (dir, currentPath) => { - Object.keys(dir).forEach(key => { - if (key === 'files') { - dir[key].forEach(file => paths.push(path.join(currentPath, file))); - } else { - traverse(dir[key], path.join(currentPath, key)); - } - }); - }; - traverse(directoryStructure, ''); - return paths; -} - -async function fetchFiles(ip, filePaths) { - let results = {}; - - for (const filePath of filePaths) { - const url = `http://${ip}:3000/file_path`; - try { - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ filePath }) - }); - - if (!response.ok) throw new Error(`HTTP status ${response.status}`); - - const arrayBuffer = await response.arrayBuffer(); // Fetch the response as an ArrayBuffer - results[filePath] = Buffer.from(arrayBuffer); - } catch (error) { - console.error(`Error fetching ${filePath}:`, error); - results[filePath] = null; - } - } - - return results; -} - -async function removeDirectory(directoryPath) { - try { - // Check if the directory exists - const stats = await fsPromises.stat(directoryPath); - if (!stats.isDirectory()) { - console.log('The specified path is not a directory.'); - return; - } - - // Read all the contents of the directory - const files = await fsPromises.readdir(directoryPath); - - // Loop through each file/directory and delete them - for (const file of files) { - const currentPath = path.join(directoryPath, file); - const currentStats = await fsPromises.stat(currentPath); - - if (currentStats.isDirectory()) { - // Recursive call for directories - await removeDirectory(currentPath); - } else { - await lockFile(currentPath); - await unlockFile(currentPath); - await fsPromises.unlink(currentPath); - } - } - - // Finally, delete the directory itself - await fsPromises.rmdir(directoryPath); - //console.log(`Directory removed: ${directoryPath}`); - } catch (error) { - console.error(`Error removing directory: ${error.message}`); - } -} - -async function decryptUserSystemConfig() { - const configPath = path.join(__dirname, '..', 'usersInSystem.json'); - await lockFile(configPath); - await decryptFileInPlace(configPath); -} - -async function encryptUserSystemConfig(){ - const configPath = path.join(__dirname, '..', 'usersInSystem.json'); - await encryptFileInPlace(configPath); - await unlockFile(configPath); -} - -async function processBackup() { - const usersConfigPath = path.join(__dirname, '..', 'usersInSystem.json'); - const backupConfigPath = path.join(__dirname, '..', 'backupSchemes.json'); - const baseBackupDirPath = path.join(__dirname, '..', 'backup_directories'); - if(!fs.existsSync(backupConfigPath) || !fs.existsSync(usersConfigPath)){ - console.log('Can\'t start backup system: necessary files missing!'); - return; - } - - await decryptUserSystemConfig(); - - try { - // Read and parse the users configuration - const usersData = await fsPromises.readFile(usersConfigPath, 'utf8'); - const usersConfig = JSON.parse(usersData); - - // Read and parse the backup configuration - const backupData = await fsPromises.readFile(backupConfigPath, 'utf8'); - const jsonData = JSON.parse(backupData); - - for (const key in jsonData) { - const userBackupDir = path.join(baseBackupDirPath, key); - await removeDirectory(userBackupDir); - - const node = jsonData[key]; - const directoryStructure = JSON.parse(node.directoryStructure); - const filePaths = extractFilePaths(directoryStructure); - const files = await fetchFiles(node.ip, filePaths); - - await fsPromises.mkdir(userBackupDir, { recursive: true }); - - // Find the encryption key for the user's department - let encryptionKey = ''; - for (const dept in usersConfig) { - if (usersConfig[dept].users.includes(key)) { - encryptionKey = usersConfig[dept].key; - break; - } - } - - for (const file in files) { - if (files[file]) { - const fullPath = path.join(userBackupDir, file); - await fsPromises.mkdir(path.dirname(fullPath), { recursive: true }); - await fsPromises.writeFile(fullPath, files[file]); // Write the binary data - - // Encrypt the file in place after writing it - if (encryptionKey) { - await encryptFileWithKey(fullPath, encryptionKey); - } - } - } - } - } catch (error) { - console.error('Failed to read or process the JSON file:', error); - } - - await encryptUserSystemConfig(); -} - -async function getAllFilePaths(dirPath) { - let filePaths = []; - async function recurse(currentPath) { - const entries = await fsPromises.readdir(currentPath, { withFileTypes: true }); - // Create promises for each entry and process them in parallel - const entryPromises = entries.map(async (entry) => { - const resolvedPath = path.join(currentPath, entry.name); - if (entry.isDirectory()) { - // If it's a directory, recurse into it - await recurse(resolvedPath); - } else { - // If it's a file, add it to the file paths array - filePaths.push(resolvedPath); - } - }); - // Wait for all promises to complete - await Promise.all(entryPromises); - } - await recurse(dirPath); - return filePaths; -} - -async function decryptBackupFilesToDirectory(destinationDir) { - await decryptUserSystemConfig(); - const usersConfigPath = path.join(__dirname, '..', 'usersInSystem.json'); - - if(!fs.existsSync(usersConfigPath)){ - return false; - } - - const usersConfig = JSON.parse(await fsPromises.readFile(usersConfigPath, 'utf8')); - const sourceDir = path.join(__dirname, '..', 'backup_directories'); - - try { - for (const department in usersConfig) { - const { key, users } = usersConfig[department]; - for (const userId of users) { - const userDir = path.join(sourceDir, userId); - try { - const files = await getAllFilePaths(userDir); - for (const filePath of files) { - const stats = await fsPromises.stat(filePath); - if (stats.isFile()) { - const relativePath = path.relative(userDir, filePath); - - // Construct the destination file path - const destinationFilePath = path.join(destinationDir, userId, relativePath); - await fsPromises.mkdir(path.dirname(destinationFilePath), { recursive: true }); - await lockFile(filePath); - - // Decrypt the file in its original location - await decryptFileWithKey(filePath, key); - - // Copy the decrypted file to the destination directory - await fsPromises.copyFile(filePath, destinationFilePath); - console.log(`Decrypted file copied to: ${destinationFilePath}`); - - // Re-encrypt the file in its original location (optional) - await encryptFileWithKey(filePath, key); - await unlockFile(filePath); - } - } - } catch (error) { - console.error(`Error processing files for user ${userId}: ${error.message}`); - await encryptUserSystemConfig(); - return false; - } - } - } - } catch (error) { - console.error(`Error processing decryption: ${error.message}`); - await encryptUserSystemConfig(); - return false; - } - - await encryptUserSystemConfig(); - return true; -} - -async function decryptDepartmentFilesToDirectory() { - const dirDepartmentPath = path.join(__dirname, '..', 'dirDepartment.json'); - const loginDataPath = path.join(__dirname, '..', 'loginData.json'); - const usersConfigPath = path.join(__dirname, '..', 'usersInSystem.json'); - const sourceDir = path.join(__dirname, '..', 'backup_directories'); - - if (!fs.existsSync(usersConfigPath) || !fs.existsSync(loginDataPath) || !fs.existsSync(dirDepartmentPath)) { - console.error("Missing one or more essential configuration files."); - return false; - } - - await lockFile(loginDataPath); - await lockFile(usersConfigPath); - - await decryptFileInPlace(loginDataPath); - await decryptFileInPlace(usersConfigPath); - - const { path: destinationDir } = JSON.parse(await fsPromises.readFile(dirDepartmentPath, 'utf8')); - const { department } = JSON.parse(await fsPromises.readFile(loginDataPath, 'utf8')); - const usersConfig = JSON.parse(await fsPromises.readFile(usersConfigPath, 'utf8')); - - // Check if department exists in users configuration - if (!usersConfig[department]) { - console.error(`No configuration found for department: ${department}`); - return false; - } - - const { key, users } = usersConfig[department]; - - await encryptFileInPlace(loginDataPath); - await encryptFileInPlace(usersConfigPath); - - await unlockFile(loginDataPath); - await unlockFile(usersConfigPath); - - await removeDirectory(destinationDir); - - for (const userId of users) { - const userDir = path.join(sourceDir, userId); - - try { - const files = await getAllFilePaths(userDir); - console.log(`Processing files for user ${userId}: ${files}`); - - for (const filePath of files) { - const stats = await fsPromises.stat(filePath); - if (stats.isFile()) { - const relativePath = path.relative(userDir, filePath); - - // Construct the destination file path - const destinationFilePath = path.join(destinationDir, userId, relativePath); - await fsPromises.mkdir(path.dirname(destinationFilePath), { recursive: true }); - - // Decrypt the file in its original location - await decryptFileWithKey(filePath, key); - - // Copy the decrypted file to the destination directory - await fsPromises.copyFile(filePath, destinationFilePath); - console.log(`Decrypted file copied to: ${destinationFilePath}`); - - // Re-encrypt the file in its original location (optional) - await encryptFileWithKey(filePath, key); - } - } - } catch (error) { - console.error(`Error processing files for user ${userId}: ${error.message}`); - return false; - } - } - - console.log("All files processed successfully."); - return true; -} - -let serverIp = null; -decryptAndGetServerIP(path.join(__dirname, '..', 'ipConfig.json')).then(ip => { - serverIp = ip; -}); - -const timeoutInterval = 7 * 60 * 1000 //minutes * seconds * miliseconds - -setInterval(async () => { - await decryptDepartmentFilesToDirectory(); -}, timeoutInterval); - -console.log('backup.js process started!'); -process.on('SIGINT', async () => { - console.log('Shutdown signal received in \'backup.js\'. Cleaning up...'); - - try { - console.log('Performing cleanup tasks...'); - console.log('Cleanup completed successfully.'); - } catch (error) { - console.error('An error occurred during cleanup:', error); - } finally { - console.log('Process terminated'); - process.exit(0); - } -}); - -process.on('message', async (message) => { - if (message.type === 'startBackup') { - console.log('BackupProcess: Received command to start backup.'); - await processBackup() - } - if(message.type === 'decryptBackup'){ - console.log(`BackupProcess: Received command to decrypt backup to ${message.decryptDestPath}`); - console.log(message.decryptDestPath); - await decryptBackupFilesToDirectory(message.decryptDestPath); - } -}); \ No newline at end of file diff --git a/CEO/jobs/clean.js b/CEO/jobs/clean.js deleted file mode 100644 index 236f48a..0000000 --- a/CEO/jobs/clean.js +++ /dev/null @@ -1,28 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -// List of files you want to check and delete -const filesToDelete = [ - path.join(__dirname, '..', 'backupSchemes.json'), - path.join(__dirname, '..', 'dirBackup.json'), - path.join(__dirname, '..', 'dirShare.json'), - path.join(__dirname, '..', 'usersInSystem.json'), - path.join(__dirname, '..', 'loginData.json'), - path.join(__dirname, '..', 'ipConfig.json') -]; - -filesToDelete.forEach(file => { - fs.access(file, fs.constants.F_OK, (err) => { - if (!err) { - fs.unlink(file, err => { - if (err) { - console.error(`Failed to delete ${file}: ${err}`); - } else { - console.log(`${file} was deleted successfully.`); - } - }); - } else { - console.log(`${file} does not exist.`); - } - }); -}); diff --git a/CEO/jobs/external_endpoints.js b/CEO/jobs/external_endpoints.js deleted file mode 100644 index 73d3dc4..0000000 --- a/CEO/jobs/external_endpoints.js +++ /dev/null @@ -1,192 +0,0 @@ -const express = require('express'); -const fs = require('fs'); -const fsExtra = require('fs-extra'); -const path = require('path'); -const Joi = require('joi'); -const { httpStatus } = require('../helpers/http_status'); -const {lockFile, unlockFile} = require("../helpers/lock_mechanism"); -const {decryptFileInPlace, encryptFileInPlace} = require("../src/main/aes_encrypt"); - -const app = express(); -app.use(express.json()); - - -const server = require('http').createServer(app); -const connections = []; - -// Store active connections to close them on shutdown -server.on('connection', (conn) => { - connections.push(conn); - conn.on('close', () => { - connections.splice(connections.indexOf(conn), 1); - }); -}); - -const filePathSchema = Joi.object({ - filePath: Joi.string().required() -}); - -async function getBaseDirectory() { - const filePath = path.join(__dirname, '..', 'dirBackup.json'); - try { - const data = await fs.promises.readFile(filePath, 'utf8'); - const jsonData = JSON.parse(data); - return jsonData.path; - } catch (error) { - console.error('Error reading the base directory:', error); - throw error; - } -} - -app.post('/file_path', async (req, res) => { - const { error, value } = filePathSchema.validate(req.body); - if (error) { - return res.status(httpStatus.BAD_REQUEST).json({ - message: 'Invalid input', - data: error.details - }); - } - - try { - const basePath = await getBaseDirectory(); - let { filePath } = value; - const baseDirName = path.basename(basePath); - if (filePath.startsWith(baseDirName + path.sep)) { - filePath = filePath.slice(baseDirName.length + 1); - } - const fullPath = path.resolve(basePath, filePath); - - fs.stat(fullPath, (err, stats) => { - if (err) { - if (err.code === 'ENOENT') { - return res.status(httpStatus.NOT_FOUND).json({ - message: 'File not found' - }); - } - return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ - message: 'Error accessing the file' - }); - } - - if (!stats.isFile()) { - return res.status(httpStatus.BAD_REQUEST).json({ - message: 'Path is not a file' - }); - } - - // Stream the file as binary data - res.writeHead(httpStatus.OK, { - 'Content-Type': 'application/octet-stream', - 'Content-Disposition': `attachment; filename="${path.basename(fullPath)}"` - }); - const fileStream = fs.createReadStream(fullPath); - fileStream.pipe(res); - }); - } catch (error) { - return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ - message: 'Error processing request' - }); - } -}); - - - -async function addFilePathToJson(filePath) { - const jsonFilePath = path.join(__dirname, '..', 'filesReceived.json'); - let data = { receivedFiles: [] }; - - try { - // Check if the JSON file exists - const fileExists = await fsExtra.pathExists(jsonFilePath); - - if (fileExists) { - await lockFile(jsonFilePath); - await decryptFileInPlace(jsonFilePath); - data = await fsExtra.readJson(jsonFilePath); - } else { - await fsExtra.writeJson(jsonFilePath, data, { spaces: 2 }); - await lockFile(jsonFilePath); - } - - if (data.receivedFiles.findIndex(existingFilePath => existingFilePath === filePath) === -1) { - data.receivedFiles.push(filePath); - } - - // Write the updated data back to the file - await fsExtra.writeJson(jsonFilePath, data, { spaces: 2 }); - // Unlock the file after updating - await unlockFile(jsonFilePath); - await encryptFileInPlace(jsonFilePath); - console.log('File path added successfully.'); - } catch (error) { - console.error('Error updating JSON file:', error); - await encryptFileInPlace(jsonFilePath); - await unlockFile(jsonFilePath); - } -} - -app.use('/share_file', express.raw({ - type: 'application/octet-stream', - limit: '50mb' -})); - -async function getShareDir(filePath) { - try { - await lockFile(filePath); - - const fileContent = await fs.promises.readFile(filePath, 'utf8'); - const jsonData = JSON.parse(fileContent); - const path = jsonData.path; - - await unlockFile(filePath); - return path; - } catch (error) { - console.error('An error occurred:', error); - return null; - } -} - - -app.post('/share_file', async (req, res) => { - // Extract metadata from headers - const idUser = req.headers['x-iduser']; - const nameOfFile = req.headers['x-nameoffile']; - const sizeOfFile = req.headers['x-sizeoffile']; - - // You might want to validate the metadata here - if (!idUser || !nameOfFile || !sizeOfFile) { - return res.status(httpStatus.BAD_REQUEST).json({message: 'Missing metadata headers'}); - } - - // Construct the file path using the metadata - const shareDirPath = await getShareDir(path.join(__dirname, '..', 'dirShare.json')); - const baseDir = path.join(shareDirPath, idUser); - fsExtra.ensureDirSync(baseDir); - - const filePath = path.join(baseDir, nameOfFile); - - try { - fs.writeFileSync(filePath, req.body); - await addFilePathToJson(filePath); - res.status(httpStatus.OK).json({ message: 'Upload successful.' }); - } catch (error) { - console.error('Error during file upload:', error); - res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ message: 'Internal server error.' }); - } -}); - - -console.log('external_endpoints.json started.'); -const PORT = process.env.PORT || 3000; -server.listen(PORT, () => console.log(`Server running on port ${PORT}`)); - -process.on('SIGINT', () => { - console.log('SIGINT signal received. Shutting down gracefully.'); - server.close(() => { - console.log('Server closed.'); - // Ensure all connections are closed - connections.forEach(conn => conn.end()); - setTimeout(() => connections.forEach(conn => conn.destroy()), 5000); - process.exit(0); - }); -}); diff --git a/CEO/jobs/fetcher.js b/CEO/jobs/fetcher.js deleted file mode 100644 index 45bb116..0000000 --- a/CEO/jobs/fetcher.js +++ /dev/null @@ -1,234 +0,0 @@ -const fsPromises = require('fs').promises; -const fs = require('fs'); -const path = require('path'); -const ip = require('ip'); -const {encryptFileInPlace, decryptFileInPlace} = require('../src/main/aes_encrypt'); -const {lockFile, unlockFile} = require("../helpers/lock_mechanism"); - -async function fetchBackupSchemes(serverIp) { - // Fetching backup schemes from the server - const depResponse = await fetch(`http://${serverIp}/backup_schemes`, { - method: 'GET', - headers: {'x-api-key': 'uc_api'} - }); - const backupSchemesJson = await depResponse.json(); - const backupSchemes = backupSchemesJson['data']; - - // Specify the path where the backup schemes will be saved - const destPath = path.join(__dirname, '..', 'backupSchemes.json'); - - // Write the fetched backup schemes to the specified file - try { - await lockFile(destPath); - await fsPromises.writeFile(destPath, JSON.stringify(backupSchemes, null, 2)); // Use null, 2 for pretty formatting - await unlockFile(destPath); - console.log('Backup schemes saved successfully to', destPath); - } catch (error) { - console.error('Failed to save backup schemes:', error); - } -} - -async function fetchUsersAndDepartments(serverIp) { - try { - console.log('se asteapta depart'); - // Fetch departments - const depResponse = await fetch(`http://${serverIp}/users/departments`, { - method: 'GET', - headers: {'x-api-key': 'uc_api'} - }); - - const departmentsJson = await depResponse.json(); - const departments = departmentsJson['data']; - - console.log('se asteapta users') - // Fetch users - const userResponse = await fetch(`http://${serverIp}/users`, { - method: 'GET', - headers: {'x-api-key': 'uc_api'} - }); - const usersJson = await userResponse.json(); - const users = usersJson['data']; - - // Combine the JSON data - const combinedData = Object.values(departments).reduce((acc, {name, key}) => { - acc[name] = { - key, - users: users.filter(user => user.department === name).map(user => user.id) - }; - return acc; - }, {}); - - // Write combined data to a file - const filePath = path.join(__dirname, '..', 'usersInSystem.json'); - if(!fs.existsSync(filePath)){ - await fsPromises.writeFile(filePath, ''); - } - await lockFile(filePath); - await fsPromises.writeFile(filePath, JSON.stringify(combinedData, null, 2), 'utf8'); - - await encryptFileInPlace(filePath); - await unlockFile(filePath); - console.log('Data has been encrypted and saved.'); - } catch (error) { - console.error('Failed to fetch or process data:', error); - } -} - -async function decryptAndGetServerIP(filePath) { - try { - await lockFile(filePath); - await decryptFileInPlace(filePath); - - const fileContent = await fsPromises.readFile(filePath, 'utf8'); - const jsonData = JSON.parse(fileContent); - const serverIP = jsonData.ip; - - await encryptFileInPlace(filePath) - await unlockFile(filePath); - return serverIP; - } catch (error) { - console.error('An error occurred:', error); - return null; - } -} - -async function getDirectoryStructure(dirPath) { - const baseName = path.basename(dirPath); - const entries = await fsPromises.readdir(dirPath, {withFileTypes: true}); - const result = {}; - result[baseName] = {files: []}; - let totalSize = 0; - - for (let entry of entries) { - const entryPath = path.join(dirPath, entry.name); - if (entry.isDirectory()) { - // Recursively get structure for subdirectories - const {structure, size: subSize} = await getDirectoryStructure(entryPath); - result[baseName][entry.name] = structure[Object.keys(structure)[0]]; // Object.keys to fetch the first key name - totalSize += subSize; - } else { - // Add file name to the 'files' array and calculate total size - const stats = await fsPromises.stat(entryPath); - result[baseName].files.push(entry.name); - totalSize += stats.size; - } - } - return {structure: result, size: totalSize}; -} - -async function createBackupScheme(serverIp, structure, size) { - let filePath = path.join(__dirname, '..', 'loginData.json'); - await lockFile(filePath); - await decryptFileInPlace(filePath); - - const userContent = await fsPromises.readFile(filePath, 'utf8'); - const userJson = JSON.parse(userContent); - - await encryptFileInPlace(filePath) - await unlockFile(filePath); - - // Find the IP address of the machine - const machineIP = ip.address(); - - // Prepare the data to be sent - const dataToSend = { - id: userJson.id, - ip: machineIP, - directoryStructure: JSON.stringify(structure), - totalSize: size - }; - - const response = await fetch(`http://${serverIp}/backup_schemes`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': 'uc_api' - }, - body: JSON.stringify(dataToSend) - }); - - if (response.ok) { - console.log("Backup dir structure submitted.") - } else { - const data = await response.json() - console.log(data.message); - } -} - -async function watchDirectoryChanges(serverIp) { - async function readBackupConfig(filePath) { - await lockFile(filePath); - const fileContent = await fsPromises.readFile(filePath, 'utf8'); - await unlockFile(filePath); - return JSON.parse(fileContent); - } - - async function updateBackupConfig(filePath, structure, size) { - const data = { - path: filePath, - structure: structure, - size: size - }; - await lockFile(filePath); - await fsPromises.writeFile(path.join(__dirname, '..', 'dirBackup.json'), JSON.stringify(data, null, 2)); - await unlockFile(filePath); - } - - const configPath = path.join(__dirname, '..', 'dirBackup.json'); - if(!fs.existsSync(configPath)){ - console.log('backupDir file doesn\'t exists'); - return; - } - const backupConfig = await readBackupConfig(configPath); - const storedStructure = backupConfig.structure; - - const {structure: currentStructure, size: currentSize} = await getDirectoryStructure(backupConfig.path); - - if (JSON.stringify(storedStructure) !== JSON.stringify(currentStructure)) { - console.log("Directory structure has changed. Updating backup..."); - await createBackupScheme(serverIp, currentStructure, currentSize); - - await updateBackupConfig(backupConfig.path, currentStructure, currentSize); - console.log('FetcherProcess: start backup.'); - process.send({type: 'startBackup'}); - } else { - console.log("No changes detected in the directory structure."); - } -} - -let serverIp = null -decryptAndGetServerIP(path.join(__dirname, '..', 'ipConfig.json')).then(ip => { - serverIp = ip; -}); - -const timeoutInterval = 1 * 20 * 1000 //minutes * seconds * miliseconds - -setInterval(() => { - fetchUsersAndDepartments(serverIp); -}, timeoutInterval); - -setInterval(() => { - watchDirectoryChanges(serverIp); -}, timeoutInterval); - -setInterval(() => { - fetchBackupSchemes(serverIp); -}, timeoutInterval); - - -console.log("fetcher.js process started."); - -process.on('SIGINT', async () => { - console.log('Shutdown signal received in \'fetcher.js\'. Cleaning up...'); - - try { - console.log('Performing cleanup tasks...'); - console.log('Cleanup completed successfully.'); - } catch (error) { - console.error('An error occurred during cleanup:', error); - } finally { - console.log('Process terminated'); - process.exit(0); - } -}); - diff --git a/CEO/jobs/send_file.js b/CEO/jobs/send_file.js deleted file mode 100644 index b37520f..0000000 --- a/CEO/jobs/send_file.js +++ /dev/null @@ -1,90 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const axios = require('axios'); -const FormData = require('form-data'); -const {lockFile, unlockFile} = require("../helpers/lock_mechanism"); -const {decryptFileInPlace, encryptFileInPlace} = require("../src/main/aes_encrypt"); - -async function decryptAndGetServerIP(filePath) { - try { - await lockFile(filePath); - await decryptFileInPlace(filePath); - - const fileContent = await fs.promises.readFile(filePath, 'utf8'); - const jsonData = JSON.parse(fileContent); - const serverIP = jsonData.ip; - - await encryptFileInPlace(filePath) - await unlockFile(filePath); - return serverIP; - } catch (error) { - console.error('An error occurred:', error); - return null; - } -} - -async function extractId() { - try { - // Path to the JSON file - const jsonFilePath = path.join(__dirname, '..', 'loginData.json'); - - // Read and parse the JSON file - const jsonData = await fs.promises.readFile(jsonFilePath, 'utf-8'); - const { id } = JSON.parse(jsonData); - - return id; - } catch (error) { - console.error('Failed to read file or parse data:', error); - throw error; - } -} - -async function uploadFile(filePath, serverIp) { - try { - const idUser = await extractId(); // Fetch ID from JSON - const nameOfFile = path.basename(filePath); // Extract the file name from the path - - // Ensure the file exists and get its size - if (!fs.existsSync(filePath)) { - console.error("File does not exist: " + filePath); - process.exit(1); - } - const fileSizeInBytes = fs.statSync(filePath).size; - - const form = new FormData(); - form.append('idUser', idUser); - form.append('nameOfFile', nameOfFile); - form.append('sizeOfFile', fileSizeInBytes); - form.append('file', fs.createReadStream(filePath)); - - const response = await axios.post(`http://${serverIp}/upload`, form, { - headers: { - ...form.getHeaders(), - }, - }); - - console.log('File uploaded successfully:', response.data); - process.exit(0); // Exit with status 0 (success) - } catch (error) { - console.error('Error uploading file:', error.message); - process.exit(1); // Exit with status 1 (error) - } -} - -// Command-line argument for file path -if (process.argv.length < 3) { - console.log('Usage: node + Welcome + + +
+
+

BACKUP FETCHER

+
+
+
+

Backup is not searched, please wait...

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

Departments

+
+ +
+ +
+
+

Create New Department

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

DO WE KNOW

+

EACH OTHER?

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

Are you sure?

+

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

+
+ + +
+
+
+
+
+
+
+

WELCOME BACK,

+

+

Hope you have a productive day!

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

NOTIFICATIONS

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

RESET

+

YOUR PASSWORD

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

LET US MEET

+

EACH OTHER

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

ALL THE SETUP IS DONE!

-

LET’S PROCEED TO THE

-

LOGIN PAGE

+

SEARCHING FOR THE

+

SERVER...

+
+
+
+

Please wait while we try to connect to the server.

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

Manage Users

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

WELCOME TO THE APP

+
+
+
+

Initializing the application, please wait...

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

BACKUP FETCHER

+
+
+
+

Backup is not searched, please wait...

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

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

DECRYPTING BACKUP!

-

PlEASE WAIT

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

IP Config

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

DO WE KNOW

-

EACH OTHER?

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

WELCOME BACK,

-

-

Hope you have a productive day!

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

NOTIFICATIONS

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

CREATE NEW DEPARTMENT

-
-
-
- -
- -
-
-
-

SECURITY

-

LEVELS

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

Profile

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

SENDING THE FILE!

-

PlEASE WAIT

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

DO WE KNOW

-

EACH OTHER?

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

WELCOME BACK,

-

-

Hope you have a productive day!

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

BACKUP FETCHER

+
+
+
+

Backup is not searched, please wait...

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

DO WE KNOW

+

EACH OTHER?

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

CEO Authentication

- -
- - -
-
-

WELCOME BACK,

-

+

Hope you have a productive day!

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

USERS


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

LET US MEET

+

EACH OTHER

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

SENDING THE FILE!

-

PlEASE WAIT

- Description of GIF - +

SEARCHING FOR THE

+

SERVER...

+
+
+
+

Please wait while we try to connect to the server.

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

WELCOME TO THE APP

+
+
+
+

Initializing the application, please wait...

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

a552%I$VSeGKa;?$Jnb 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

JD9WQ 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 diff --git a/User/src/renderer/assets/hard-disk.ico b/User/src/renderer/assets/hard-disk.ico deleted file mode 100644 index 8b626007569b2144c8f06188708322f9b8430732..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 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 ML0!I7&h*9q0*2| - - - - - - - Alert Modal - - -
-
-
- -

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

Choose your department

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

DECRYPTING BACKUP!

-

PlEASE WAIT

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

IP Config

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

DO WE KNOW

-

EACH OTHER?

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

TELL ME MORE

-

ABOUT

-

YOUR WORK

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

LET US MEET

-

EACH OTHER

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