Major Refactoring and Feature Addition for Improved User Experience and Code Quality

This commit includes several significant changes aimed at improving the functionality, user experience, and code quality of our Ruby project.

1. Decryption Fix: We identified and resolved an issue where the database password was being decrypted multiple times, leading to connection problems. The code was refactored to ensure that decryption only occurs once, thereby enhancing the efficiency and reliability of our database connections.

2. Infinite Loop Resolution: We addressed a critical issue where the program would enter an infinite loop if the .env file was missing or contained incorrect information. The code was updated to handle these situations appropriately, providing meaningful feedback to the user and preventing unnecessary resource consumption.

3. .env File Handling Improvement: We improved the handling of the .env file, ensuring that the program can function correctly even in the absence of this file or if it contains incorrect data. This change enhances the robustness of our application.

4. Curses Alerts Integration: We integrated a new feature to display alerts in the terminal using the Curses library. These alerts can have different severity levels (info, warning, error), which are displayed in different colors (blue, yellow, red). This feature improves the user experience by providing clear and immediate feedback on the program's status.

5. Automatic Alert Dismissal: We implemented a feature where alerts automatically disappear after 5 seconds. This was achieved using Ruby threads, ensuring that the rest of the program is not blocked while the alert is displayed. This change enhances the user experience by preventing the screen from being cluttered with old alerts.

6. Debugging Libraries Exploration: We explored the possibility of using the Tracer and Debug libraries to trace the execution of the program and assist with debugging. While these libraries were not integrated in this commit, they remain a potential resource for future debugging efforts.

This commit represents a significant step forward in the development of our Ruby project, improving both the user experience and the quality of our codebase.
This commit is contained in:
VetheonGames 2023-06-12 15:31:34 -06:00
parent 281d5f6ebf
commit 13fa1e53e6
8 changed files with 164 additions and 95 deletions

View File

@ -2,13 +2,17 @@
# frozen_string_literal: true
require 'dotenv'
require 'debug'
require_relative '../lib/utils/system_information_gather'
require_relative '../lib/utils/database_manager'
require_relative '../lib/utils/first_run_init'
require_relative '../lib/utils/utilities'
require_relative '../lib/utils/logg_man'
include Utilities # rubocop:disable Style/MixinUsage
loggman = LoggMan.new
# Create .env file if it doesn't exist
File.open('.env', 'w') {} unless File.exist?('.env')
@ -28,36 +32,30 @@ db_details = {
# If any of the necessary details are missing, run the first run setup
if db_details.values.any?(&:nil?)
puts 'Missing or incomplete configuration. Running first run setup.'
loggman.log_warn('Missing or incomplete configuration. Running first run setup.')
first_run_init = FirstRunInit.new(db_manager)
first_run_init.run
# Reload environment variables after first run setup
Dotenv.load
db_details = {
username: ENV['DB_USERNAME'],
password: ENV['DB_PASSWORD'],
key: ENV['DB_SECRET_KEY'],
database: ENV['DB_DATABASE']
}
end
# Decrypt the password
if db_details[:password] && db_details[:key]
db_details[:password] = decrypt_string_chacha20(db_details[:password], db_details[:key])
username = ENV['DB_USERNAME']
password = ENV['DB_PASSWORD']
key = ENV['DB_SECRET_KEY']
database = ENV['DB_DATABASE']
end
# Test connection
unless db_manager.test_db_connection(db_details)
puts 'Failed to connect to the database with existing configuration. Please re-enter your details.'
dec_pass = decrypt_string_chacha20(password, key)
unless db_manager.test_db_connection(username, dec_pass, database)
loggman.log_warn('Failed to connect to the database with existing configuration. Please re-enter your details.')
first_run_init = FirstRunInit.new(db_manager)
first_run_init.run
end
# Test connection again after potentially updating config
if db_manager.test_db_connection(db_details)
puts 'Successfully connected to the database.'
if db_manager.test_db_connection(username, dec_pass, database)
loggman.log_info('Successfully connected to the database.')
else
puts 'Failed to connect to the database. Please check your configuration.'
loggman.log_error('Failed to connect to the database. Please check your configuration.')
exit 1
end

View File

@ -30,3 +30,5 @@ gem 'base64', '~> 0.1.1'
gem 'securerandom', '~> 0.2.2'
gem 'dotenv', '~> 2.8'
gem "tracer", "~> 0.2.2"

View File

@ -110,6 +110,7 @@ GEM
yard (~> 0.9, >= 0.9.24)
thor (1.2.2)
tilt (2.2.0)
tracer (0.2.2)
unicode-display_width (2.4.2)
yaml (0.2.1)
yard (0.9.34)
@ -131,6 +132,7 @@ DEPENDENCIES
securerandom (~> 0.2.2)
sequel (~> 5.69)
solargraph (~> 0.49.0)
tracer (~> 0.2.2)
yaml (~> 0.2.1)
BUNDLED WITH

View File

@ -4,6 +4,7 @@ require 'sequel'
require 'mysql2'
require_relative 'system_information_gather'
require_relative '../utils/utilities'
require_relative 'logg_man'
# database manager
class DatabaseManager
@ -11,25 +12,49 @@ class DatabaseManager
def initialize
@db = nil
@loggman = LoggMan.new
end
def test_db_connection(db_details) # rubocop:disable Metrics/MethodLength
# Decrypt the password before using it
if db_details[:password] && db_details[:key]
decrypted_password = decrypt_string_chacha20(db_details[:password], db_details[:key])
connection_string = "mysql2://#{db_details[:username]}:#{decrypted_password}@localhost/#{db_details[:database]}"
@db = Sequel.connect(connection_string)
# Try a simple query to test the connection
@db.run 'SELECT 1'
true
else
false
end
rescue Sequel::DatabaseConnectionError
def test_db_connection(username, password, database) # rubocop:disable Metrics/MethodLength
loggman = LoggMan.new
loggman.log_info('Attempting to connect to the database...')
display_alert('Attempting to connect to the database...', :info)
# Create the connection string
connection_string = "mysql2://#{username}:#{password}@localhost/#{database}"
@db = Sequel.connect(connection_string)
# Try a simple query to test the connection
@db.run 'SELECT 1'
loggman.log_info('Successfully connected to the database.')
display_alert('Successfully connected to the database.', :info)
true
rescue Sequel::DatabaseConnectionError => e
loggman.log_error("Failed to connect to the database: #{e.message}")
display_alert('Failed to connect to the database!', :error)
false
end
def create_system_info_table
def create_system_info_table # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
if @db.nil?
# Attempt to establish a connection
username = ENV['DB_USERNAME']
password = decrypt_string_chacha20(ENV['DB_PASSWORD'], ENV['DB_SECRET_KEY'])
database = ENV['DB_DATABASE']
if test_db_connection(username, password, database)
@loggman.log_info('Successfully connected to the database.')
display_alert('Successfully connected to the Database1', :info)
else
# If the connection attempt fails, log an error and return
@loggman.log_error('Failed to connect to the database.')
return
end
end
if @db.nil?
@loggman.log_error('@db is still nil after attempting to connect to the database.')
return
end
@db.create_table? :system_info do
primary_key :id
Integer :uplink_speed

View File

@ -17,20 +17,25 @@ class FirstRunInit
@db_manager = db_manager || DatabaseManager.new
@info_gatherer = SystemInformationGather.new(@db_manager)
@loggman = LoggMan.new
Dotenv.load
end
def run
first_run_setup
end
def first_run_setup # rubocop:disable Metrics/MethodLength
def first_run_setup # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
db_details = ask_for_db_details
until @db_manager.test_db_connection(db_details)
enc_pass = ENV['DB_PASSWORD']
enc_key = ENV['DB_SECRET_KEY']
dec_pass = decrypt_string_chacha20(enc_pass, enc_key)
until @db_manager.test_db_connection(ENV['DB_USERNAME'], dec_pass.to_s, ENV['DB_DATABASE'])
Curses.setpos(4, 0)
Curses.addstr("Whoops! We couldn't connect to the database with the details you provided. Please try again!")
Curses.refresh
db_details = ask_for_db_details
new_db_details = ask_for_db_details
db_details.merge!(new_db_details) # Update db_details with new details
end
@db_manager.create_system_info_table

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'gtk3'
# GUI launcher
@ -16,10 +18,10 @@ class GUILauncher
width = screen.width
height = screen.height
if width >= 3840 and height >= 2160
if (width >= 3840) && (height >= 2160)
# 4K resolution
window.set_default_size(1200, 1000)
elsif width >= 1920 and height >= 1080
elsif (width >= 1920) && (height >= 1080)
# 1080p resolution
window.set_default_size(1080, 800)
else

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'curses'
require 'yaml'
require_relative 'utilities'
@ -25,52 +27,44 @@ class SystemInformationGather
total_bandwidth:
}
# Check if the system_info table exists, if not, create it
@db_manager.create_system_info_table unless @db_manager.table_exists?(:system_info)
# Store the gathered system info in the database
@db_manager.store_system_info(system_info)
# Check if the services table exists, if not, create it
@db_manager.create_services_table unless @db_manager.table_exists?(:services)
# Store the services in the services table
@db_manager.store_services(services)
end
def ask_for_uplink_speed # rubocop:disable Metrics/MethodLength
Curses.clear
Curses.addstr("Please enter your uplink speed (upload speed, e.g., 1000Mbps or 1Gbps).\n" \
"This is typically the maximum upload speed provided by your ISP.\n" \
"You can check your ISP bill, use an online speed test, or contact your ISP if you're unsure.\n\n")
Curses.refresh
Curses.addstr('Uplink Speed: ')
speed = DCI.catch_input(true)
if valid_speed?(speed)
speed.end_with?('gbps') ? convert_speed_to_mbps(speed) : speed.to_i
else
loop do
Curses.clear
Curses.addstr("Please enter your uplink speed (upload speed, e.g., 1000Mbps or 1Gbps).\n" \
"This is typically the maximum upload speed provided by your ISP.\n" \
"You can check your ISP bill, use an online speed test, or contact your ISP if you're unsure.\n\n")
Curses.refresh
Curses.addstr('Uplink Speed: ')
speed = DCI.catch_input(true)
return speed.end_with?('gbps') ? convert_speed_to_mbps(speed) : speed.to_i if valid_speed?(speed)
Curses.setpos(5, 0)
Curses.addstr("Whoops! That didn't appear to be a valid speed. Please try again!")
Curses.refresh
ask_for_uplink_speed
end
end
def ask_for_downlink_speed # rubocop:disable Metrics/MethodLength
Curses.clear
Curses.addstr("Please enter your downlink speed (download speed, e.g., 1000Mbps or 1Gbps).\n" \
"This is typically the maximum download speed provided by your ISP.\n"\
"You can check your ISP bill, use an online speed test, or contact your ISP if you're unsure.\n\n")
Curses.refresh
Curses.addstr('Downlink Speed: ')
speed = DCI.catch_input(true)
if valid_speed?(speed)
speed.end_with?('gbps') ? convert_speed_to_mbps(speed) : speed.to_i
else
loop do
Curses.clear
Curses.addstr("Please enter your downlink speed (download speed, e.g., 1000Mbps or 1Gbps).\n" \
"This is typically the maximum download speed provided by your ISP.\n"\
"You can check your ISP bill, use an online speed test, or contact your ISP if you're unsure.\n\n")
Curses.refresh
Curses.addstr('Downlink Speed: ')
speed = DCI.catch_input(true)
return speed.end_with?('gbps') ? convert_speed_to_mbps(speed) : speed.to_i if valid_speed?(speed)
Curses.setpos(5, 0)
Curses.addstr("Whoops! That didn't appear to be a valid speed. Please try again!")
Curses.refresh
ask_for_downlink_speed
end
end
@ -79,26 +73,26 @@ class SystemInformationGather
end
def ask_for_services # rubocop:disable Metrics/MethodLength
Curses.clear
Curses.addstr("Please enter the services the system should be aware of (e.g., webserver or database).\n" \
"Enter the services as a comma-separated list (e.g., webserver,database).\n\n")
Curses.refresh
Curses.addstr('Services: ')
services = DCI.catch_input(true)
services_arr = services.strip.downcase.split(',').map(&:strip)
if valid_services?(services_arr)
services_arr # return the array of services directly
else
Curses.setpos(7, 0)
Curses.addstr("Whoops! That didn't appear to be a valid list of services. Please try again!")
loop do
Curses.clear
Curses.addstr("Please enter the services the system should be aware of (e.g., webserver or database).\n" \
"Enter the services as a comma-separated list (e.g., webserver,database).\n\n")
Curses.refresh
Curses.addstr('Services: ')
services = DCI.catch_input(true)
services_arr = services.strip.downcase.split(',').map(&:strip)
return services_arr if valid_services?(services_arr)
Curses.setpos(7, 0)
Curses.addstr("Whoops! Thatdidn't appear to be a valid list of services. Please try again!")
Curses.refresh
ask_for_services
end
end
def valid_services?(_services)
def valid_services?(services)
# TODO: Validate the services
true
# For now, just check if the array is not empty
!services.empty?
end
end

View File

@ -10,11 +10,9 @@ require_relative 'logg_man'
module Utilities
# Converts speed from Gbps to Mbps if necessary
def convert_speed_to_mbps(speed)
if speed.end_with?('gbps')
speed.to_i * 1000
else
speed.to_i
end
return nil unless speed.is_a?(String) && speed.match?(/\A\d+(gbps|mbps)\z/i)
speed.end_with?('gbps') ? speed.to_i * 1000 : speed.to_i
end
# Converts an array of services into a hash
@ -34,25 +32,68 @@ module Utilities
end
def encrypt_string_chacha20(data, key)
return nil if data.nil? || key.nil?
cipher = OpenSSL::Cipher.new('chacha20')
cipher.encrypt
cipher.key = Base64.decode64(key) # Decode the key from Base64
cipher.key = Base64.decode64(key) # Decode the key from Base64
encrypted_data = cipher.update(data) + cipher.final
loggman = LoggMan.new
loggman.log_debug("Data to be encrypted: #{data}")
loggman.log_debug("Key: #{key}")
loggman.log_debug("Encrypted data: #{encrypted_data}")
Base64.encode64(encrypted_data).chomp
rescue OpenSSL::Cipher::CipherError => e
loggman = LoggMan.new
loggman.log_error("Failed to encrypt data: #{e.message}")
nil
end
def decrypt_string_chacha20(encrypted_data, key)
return nil if encrypted_data.nil?
def decrypt_string_chacha20(encrypted_data, key) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
return nil if encrypted_data.nil? || key.nil?
cipher = OpenSSL::Cipher.new('chacha20')
cipher.decrypt
cipher.key = Base64.decode64(key) # Decode the key from Base64
cipher.update(Base64.decode64(encrypted_data)) + cipher.final
cipher.key = Base64.decode64(key) # Decode the key from Base64
decrypted_data = cipher.update(Base64.decode64(encrypted_data)) + cipher.final
# Check if the decrypted data is valid ASCII
decrypted_data.force_encoding('UTF-8')
if decrypted_data.valid_encoding?
decrypted_data
else
loggman = LoggMan.new
loggman.log_error("Decrypted data is not valid ASCII: #{decrypted_data.inspect}")
nil
end
rescue OpenSSL::Cipher::CipherError => e
loggman = LoggMan.new
loggman.log_error("Failed to decrypt data: #{e.message}")
nil
end
def display_alert(message, severity) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
case severity
when :info
Curses.attron(Curses.color_pair(1)) # Blue color
when :warning
Curses.attron(Curses.color_pair(3)) # Yellow color
when :error
Curses.attron(Curses.color_pair(2)) # Red color
end
Curses.setpos(Curses.lines - 1, 0)
Curses.addstr(message)
Curses.refresh
Thread.new do
sleep(5) # Pause for 5 seconds
# Clear the alert
Curses.setpos(Curses.lines - 1, 0)
Curses.clrtoeol
Curses.refresh
end
Curses.attroff(Curses.color_pair(1)) if severity == :info
Curses.attroff(Curses.color_pair(3)) if severity == :warning
Curses.attroff(Curses.color_pair(2)) if severity == :error
end
end