diff --git a/config.example.php b/config.example.php index 3c3663c5..ffa2c492 100644 --- a/config.example.php +++ b/config.example.php @@ -4,6 +4,7 @@ $CONFIG_DB = [ 'user' => '', // Username of the database server e.g. root 'password' => '', // Password of the database server e.g. password123 'database' => '', // Name of the database e.g. my_database + 'port' => 3306, // Port of the database server e.g. 3306 'ssl_mode' => 'DISABLED' // SSL mode for mysqldump: DISABLED, PREFERRED, REQUIRED, VERIFY_CA, VERIFY_IDENTITY ]; $DEBUG = true; // Set to true to enable debugging (Error messages will be shown, and this should never be used in production) @@ -33,8 +34,10 @@ $MINIO = [ $SLACK_DEFAULT_WEBHOOK = ''; // Default Slack webhook URL e.g. https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX $REDIS_CONFIG = [ 'host' => '', // Redis host (IP address) + 'user' => '', // Redis user 'database' => 0, // Redis database number (0-15) - 'password' => '' // Redis password + 'password' => '', // Redis password + 'port' => 6379 // Redis port ]; // Set the timezone @@ -50,6 +53,7 @@ if (isset($_ENV['USE_ENV']) && $_ENV['USE_ENV'] === 'true') { 'CONFIG_DB_USER' => 'user', 'CONFIG_DB_PASSWORD' => 'password', 'CONFIG_DB_DATABASE' => 'database', + 'CONFIG_DB_PORT' => 'port', 'CONFIG_DB_SSL_MODE' => 'ssl_mode', 'DEBUG' => 'DEBUG', 'ENCRYPTION_KEY' => 'ENCRYPTION_KEY', @@ -66,7 +70,9 @@ if (isset($_ENV['USE_ENV']) && $_ENV['USE_ENV'] === 'true') { 'SLACK_DEFAULT_WEBHOOK' => 'SLACK_DEFAULT_WEBHOOK', 'REDIS_CONFIG_HOST' => 'host', 'REDIS_CONFIG_DATABASE' => 'database', - 'REDIS_CONFIG_PASSWORD' => 'password' + 'REDIS_CONFIG_PASSWORD' => 'password', + 'REDIS_CONFIG_PORT' => 'port', + 'REDIS_CONFIG_USER' => 'user' ]; $dbTarget = strtolower(trim((string)($_ENV['CONFIG_DB_TARGET'] ?? 'live'))); if ($dbTarget !== 'live' && $dbTarget !== 'debug') { @@ -94,6 +100,7 @@ if (isset($_ENV['USE_ENV']) && $_ENV['USE_ENV'] === 'true') { 'user' => $resolveDbValue('USER'), 'password' => $resolveDbValue('PASSWORD'), 'database' => $resolveDbValue('DATABASE'), + 'port' => (int)($resolveDbValue('PORT') ?: 3306), 'ssl_mode' => $resolveDbValue('SSL_MODE') !== '' ? $resolveDbValue('SSL_MODE') : 'DISABLED' ]; /** @@ -140,13 +147,28 @@ if (isset($_ENV['USE_ENV']) && $_ENV['USE_ENV'] === 'true') { * Set the Slack default webhook */ $SLACK_DEFAULT_WEBHOOK = $_ENV['SLACK_DEFAULT_WEBHOOK']; + $resolveRedisValue = function (string $key) use ($dbTarget): string { + $liveKey = 'REDIS_CONFIG_' . $key; + $debugKey = 'REDIS_CONFIG_DEBUG_' . $key; + $liveValue = (string)($_ENV[$liveKey] ?? ''); + $debugValue = (string)($_ENV[$debugKey] ?? ''); + + if ($dbTarget === 'debug' && $debugValue !== '') { + return $debugValue; + } + + return $liveValue; + }; + /** * Set the Redis configuration */ $REDIS_CONFIG = [ - 'host' => $_ENV['REDIS_CONFIG_HOST'], - 'database' => $_ENV['REDIS_CONFIG_DATABASE'], - 'password' => $_ENV['REDIS_CONFIG_PASSWORD'] + 'host' => $resolveRedisValue('HOST'), + 'user' => $resolveRedisValue('USER'), + 'database' => $resolveRedisValue('DATABASE'), + 'password' => $resolveRedisValue('PASSWORD'), + 'port' => (int)($resolveRedisValue('PORT') ?: 6379) ]; // Set the timezone diff --git a/services/nginx/app/classes/db.php b/services/nginx/app/classes/db.php index 0d62b6cf..c8a0c623 100644 --- a/services/nginx/app/classes/db.php +++ b/services/nginx/app/classes/db.php @@ -13,6 +13,7 @@ class db private string $user; private string $password; private string $database; + private int $port = 3306; private string $ssl_mode = 'DISABLED'; // mysqldump SSL mode (e.g., DISABLED, PREFERRED, REQUIRED, VERIFY_CA, VERIFY_IDENTITY) public function __construct(array $config) @@ -21,6 +22,9 @@ class db $this->user = $config['user']; $this->password = $config['password']; $this->database = $config['database']; + if (isset($config['port']) && is_numeric($config['port'])) { + $this->port = (int)$config['port']; + } if (isset($config['ssl_mode']) && is_string($config['ssl_mode']) && $config['ssl_mode'] !== '') { $this->ssl_mode = $config['ssl_mode']; } @@ -28,22 +32,46 @@ class db public static function getPDO(): \PDO { - global $config; - $dsn = "mysql:host={$config['db']['host']};dbname={$config['db']['database']};charset=utf8mb4"; - return new \PDO($dsn, $config['db']['user'], $config['db']['password'], [ + global $CONFIG_DB; + $port = $CONFIG_DB['port'] ?? 3306; + $dsn = "mysql:host={$CONFIG_DB['host']};port={$port};dbname={$CONFIG_DB['database']};charset=utf8mb4"; + return new \PDO($dsn, $CONFIG_DB['user'], $CONFIG_DB['password'], [ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, \PDO::ATTR_EMULATE_PREPARES => false, ]); } + public function testConnection(): bool + { + try { + $conn = new mysqli($this->host, $this->user, $this->password, $this->database, $this->port); + if ($conn->connect_error) { + return false; + } + $this->conn = $conn; + return true; + } catch (Exception) { + return false; + } + } + public function connect(): void { global $response; try { - $this->conn = new mysqli($this->host, $this->user, $this->password, $this->database); + // Enable error reporting for mysqli to catch connection issues via exceptions + mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); + $this->conn = new mysqli($this->host, $this->user, $this->password, $this->database, $this->port); + if ($this->conn->connect_error) { + throw new Exception($this->conn->connect_error); + } } catch (Exception $e) { - $response->internal_server_error($e->getMessage()); + if ($response) { + $response->internal_server_error("Database connection failed: " . $e->getMessage()); + } else { + throw $e; + } } } @@ -170,9 +198,10 @@ class db $user = escapeshellarg($this->user); $pass = escapeshellarg($this->password); $db = escapeshellarg($this->database); + $port = (int)$this->port; $outfile = escapeshellarg($path); $sslPart = $sslFlag !== '' ? ($sslFlag . ' ') : ''; - $command = "mysqldump {$sslPart}-h $host -u $user --password=$pass $db > $outfile 2>&1"; + $command = "mysqldump {$sslPart}-h $host -P $port -u $user --password=$pass $db > $outfile 2>&1"; exec($command, $output, $return); // Check if the command was successful return $return === 0; diff --git a/services/nginx/app/classes/redis.php b/services/nginx/app/classes/redis.php index 704b14d4..cc826ec4 100644 --- a/services/nginx/app/classes/redis.php +++ b/services/nginx/app/classes/redis.php @@ -17,8 +17,12 @@ class redis implements redis_i } // Apply the Redis configuration $this->redis_host = $REDIS_CONFIG['host']; + $this->redis_user = $REDIS_CONFIG['user'] ?? ''; $this->redis_database = $REDIS_CONFIG['database']; $this->redis_password = $REDIS_CONFIG['password']; + if (isset($REDIS_CONFIG['port']) && is_numeric($REDIS_CONFIG['port'])) { + $this->redis_port = (int)$REDIS_CONFIG['port']; + } } @@ -500,4 +504,18 @@ class redis implements redis_i $this->delete('perm:' . $cache_key); return $this; } + + /** + * @throws \Exception + */ + public function ping(): bool + { + if (!self::is_connected()) { + self::connect(); + } + if (!self::is_connected()) { + return false; + } + return true; + } } diff --git a/services/nginx/app/config.php b/services/nginx/app/config.php index 6d45ccee..feae1854 100644 --- a/services/nginx/app/config.php +++ b/services/nginx/app/config.php @@ -10,6 +10,7 @@ if (isset($_ENV['USE_ENV']) && $_ENV['USE_ENV'] === 'true') { 'CONFIG_DB_USER' => 'user', 'CONFIG_DB_PASSWORD' => 'password', 'CONFIG_DB_DATABASE' => 'database', + 'CONFIG_DB_PORT' => 'port', 'DEBUG' => 'DEBUG', 'ENCRYPTION_KEY' => 'ENCRYPTION_KEY', 'CORS' => 'CORS', @@ -25,7 +26,9 @@ if (isset($_ENV['USE_ENV']) && $_ENV['USE_ENV'] === 'true') { 'SLACK_DEFAULT_WEBHOOK' => 'SLACK_DEFAULT_WEBHOOK', 'REDIS_CONFIG_HOST' => 'host', 'REDIS_CONFIG_DATABASE' => 'database', - 'REDIS_CONFIG_PASSWORD' => 'password' + 'REDIS_CONFIG_PASSWORD' => 'password', + 'REDIS_CONFIG_PORT' => 'port', + 'REDIS_CONFIG_USER' => 'user' ]; $dbTarget = strtolower(trim((string)($_ENV['CONFIG_DB_TARGET'] ?? 'live'))); if ($dbTarget !== 'live' && $dbTarget !== 'debug') { @@ -52,7 +55,9 @@ if (isset($_ENV['USE_ENV']) && $_ENV['USE_ENV'] === 'true') { 'host' => $resolveDbValue('HOST'), 'user' => $resolveDbValue('USER'), 'password' => $resolveDbValue('PASSWORD'), - 'database' => $resolveDbValue('DATABASE') + 'database' => $resolveDbValue('DATABASE'), + 'port' => (int)($resolveDbValue('PORT') ?: 3306), + 'ssl_mode' => $resolveDbValue('SSL_MODE') ?: 'DISABLED' ]; /** * Set the debug configuration @@ -98,17 +103,33 @@ if (isset($_ENV['USE_ENV']) && $_ENV['USE_ENV'] === 'true') { * Set the Slack default webhook */ $SLACK_DEFAULT_WEBHOOK = $_ENV['SLACK_DEFAULT_WEBHOOK']; + $resolveRedisValue = function (string $key) use ($dbTarget): string { + $liveKey = 'REDIS_CONFIG_' . $key; + $debugKey = 'REDIS_CONFIG_DEBUG_' . $key; + $liveValue = (string)($_ENV[$liveKey] ?? ''); + $debugValue = (string)($_ENV[$debugKey] ?? ''); + + if ($dbTarget === 'debug' && $debugValue !== '') { + return $debugValue; + } + + return $liveValue; + }; + /** * Set the Redis configuration */ $REDIS_CONFIG = [ - 'host' => $_ENV['REDIS_CONFIG_HOST'], - 'database' => $_ENV['REDIS_CONFIG_DATABASE'], - 'password' => $_ENV['REDIS_CONFIG_PASSWORD'] + 'host' => $resolveRedisValue('HOST'), + 'user' => $resolveRedisValue('USER'), + 'database' => $resolveRedisValue('DATABASE'), + 'password' => $resolveRedisValue('PASSWORD'), + 'port' => (int)($resolveRedisValue('PORT') ?: 6379) ]; // Set the timezone - date_default_timezone_set($_ENV['CONFIG_TIMEZONE']) ?? 'Europe/Copenhagen'; + $timezone = $_ENV['CONFIG_TIMEZONE'] ?? 'Europe/Copenhagen'; + date_default_timezone_set($timezone); // Set the all config $ALL_CONFIG = $_ENV; // This is used by the backup job. diff --git a/services/nginx/app/routes/workerRoute.php b/services/nginx/app/routes/workerRoute.php index 742af8c9..5bcb59bb 100644 --- a/services/nginx/app/routes/workerRoute.php +++ b/services/nginx/app/routes/workerRoute.php @@ -2,6 +2,7 @@ namespace routes; +use classes\db; use classes\economic; use classes\router; use classes\shelly; @@ -124,7 +125,7 @@ class workerRoute }); $this->get('/worker/status', function () { global /** @var router $router */ - $response, $router; + $response, $router, $db; $response->success([ 'message' => 'Worker is running', 'status' => 'OK', @@ -132,7 +133,22 @@ class workerRoute 'timezone' => date_default_timezone_get(), 'host' => gethostname(), 'version' => '1.0.1', - 'routes' => $router->countRoutes() + 'routes' => $router->countRoutes(), + 'redis' => [ + 'host' => $_ENV['REDIS_CONFIG_HOST'], + 'user' => $_ENV['REDIS_CONFIG_USER'], + 'database' => $_ENV['REDIS_CONFIG_DATABASE'], + //'password' => $_ENV['REDIS_CONFIG_PASSWORD'], + + 'port' => $_ENV['REDIS_CONFIG_PORT'], + 'status' => redis->ping() ? 'OK' : 'ERROR', + ], + 'database' => [ + 'host' => $_ENV['CONFIG_DB_HOST'], + 'database' => $_ENV['CONFIG_DB_DATABASE'], + 'user' => $_ENV['CONFIG_DB_USER'], + 'status' => $db->testConnection() ? 'OK' : 'ERROR', + ], ]); }); $this->get('/worker/debug', function () { diff --git a/services/nginx/app/tests/Integration/Database/DbConnectionTest.php b/services/nginx/app/tests/Integration/Database/DbConnectionTest.php index 81dbb7c9..fdae7e68 100644 --- a/services/nginx/app/tests/Integration/Database/DbConnectionTest.php +++ b/services/nginx/app/tests/Integration/Database/DbConnectionTest.php @@ -11,6 +11,7 @@ it('can connect to a configured MySQL instance in integration mode', function () $user = getenv('CONFIG_DB_USER') ?: null; $password = getenv('CONFIG_DB_PASSWORD') ?: ''; $database = getenv('CONFIG_DB_DATABASE') ?: null; + $port = getenv('CONFIG_DB_PORT') ?: 3306; if (!$host || !$user || !$database) { test()->markTestSkipped('Missing DB env vars: CONFIG_DB_HOST/CONFIG_DB_USER/CONFIG_DB_DATABASE.'); @@ -30,6 +31,7 @@ it('can connect to a configured MySQL instance in integration mode', function () 'user' => $user, 'password' => $password, 'database' => $database, + 'port' => $port, ]); $db->connect(); diff --git a/services/nginx/app/traits/redis_t.php b/services/nginx/app/traits/redis_t.php index 413a412a..19ee650c 100644 --- a/services/nginx/app/traits/redis_t.php +++ b/services/nginx/app/traits/redis_t.php @@ -18,6 +18,11 @@ trait redis_t * @var string */ protected string $redis_host = ''; + /** + * The Redis user + * @var string + */ + protected string $redis_user = ''; /** * The Redis port * @var int @@ -88,13 +93,17 @@ trait redis_t public function connect(): self { - $this->redis = new PredisClient([ + $params = [ 'scheme' => $this->redis_scheme, 'host' => $this->redis_host, 'port' => $this->redis_port, 'password' => $this->redis_password, 'database' => $this->redis_database - ]); + ]; + if ($this->redis_user !== '' && $this->redis_user !== 'default') { + $params['username'] = $this->redis_user; + } + $this->redis = new PredisClient($params); // Check if the connection was successful if (!self::is_connected()) {