fix: Strict env enforcement, rock-solid Alpine portal rendering, and Nabeh/Redis test diagnostics
This commit is contained in:
@@ -7,16 +7,17 @@ use PDOException;
|
||||
|
||||
/**
|
||||
* PDO Database wrapper using Singleton pattern.
|
||||
* Strict environment variable enforcement (No default fallbacks).
|
||||
*/
|
||||
class Database
|
||||
{
|
||||
private static ?PDO $instance = null;
|
||||
|
||||
/**
|
||||
* Get active PDO database instance (alias or direct connection)
|
||||
* Get active PDO database instance
|
||||
*
|
||||
* @return PDO
|
||||
* @throws PDOException
|
||||
* @throws PDOException|\RuntimeException
|
||||
*/
|
||||
public static function getInstance(): PDO
|
||||
{
|
||||
@@ -26,11 +27,22 @@ class Database
|
||||
public static function getConnection(): PDO
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
$host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||
$port = getenv('DB_PORT') ?: '3306';
|
||||
$dbName = getenv('DB_DATABASE') ?: 'saqelDB';
|
||||
$username = getenv('DB_USERNAME') ?: 'saqelUser';
|
||||
$password = getenv('DB_PASSWORD') ?: '';
|
||||
$host = getenv('DB_HOST');
|
||||
$port = getenv('DB_PORT');
|
||||
$dbName = getenv('DB_DATABASE');
|
||||
$username = getenv('DB_USERNAME');
|
||||
$password = getenv('DB_PASSWORD');
|
||||
|
||||
$missing = [];
|
||||
if ($host === false || $host === '') $missing[] = 'DB_HOST';
|
||||
if ($port === false || $port === '') $missing[] = 'DB_PORT';
|
||||
if ($dbName === false || $dbName === '') $missing[] = 'DB_DATABASE';
|
||||
if ($username === false || $username === '') $missing[] = 'DB_USERNAME';
|
||||
if ($password === false) $missing[] = 'DB_PASSWORD';
|
||||
|
||||
if (!empty($missing)) {
|
||||
throw new \RuntimeException("Database Configuration Error: Missing environment variable(s): " . implode(', ', $missing));
|
||||
}
|
||||
|
||||
$dsn = "mysql:host={$host};port={$port};dbname={$dbName};charset=utf8mb4";
|
||||
|
||||
@@ -43,9 +55,8 @@ class Database
|
||||
try {
|
||||
self::$instance = new PDO($dsn, $username, $password, $options);
|
||||
} catch (PDOException $e) {
|
||||
// Log the exact error internally but hide sensitive DSN on production
|
||||
error_log("Database Connection Error: " . $e->getMessage());
|
||||
throw new PDOException("Could not connect to the database. Check database settings.");
|
||||
throw new PDOException("Could not connect to MySQL database at {$host}:{$port}/{$dbName}. Error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,10 +65,6 @@ class Database
|
||||
|
||||
/**
|
||||
* Shorthand execute statement with parameters
|
||||
*
|
||||
* @param string $sql
|
||||
* @param array $params
|
||||
* @return \PDOStatement
|
||||
*/
|
||||
public static function query(string $sql, array $params = []): \PDOStatement
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace App\Core;
|
||||
|
||||
/**
|
||||
* Core Redis Client for managing connections.
|
||||
* Handles Sessions, Rate Limiting, and caching using PHP Redis extension.
|
||||
* Strict environment variable enforcement (No default fallbacks).
|
||||
*/
|
||||
class RedisClient
|
||||
{
|
||||
@@ -16,22 +16,30 @@ class RedisClient
|
||||
public static function getInstance(): \Redis
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
$host = getenv('REDIS_HOST');
|
||||
$port = getenv('REDIS_PORT');
|
||||
$password = getenv('REDIS_PASSWORD') ?: null;
|
||||
|
||||
$missing = [];
|
||||
if ($host === false || $host === '') $missing[] = 'REDIS_HOST';
|
||||
if ($port === false || $port === '') $missing[] = 'REDIS_PORT';
|
||||
|
||||
if (!empty($missing)) {
|
||||
throw new \RuntimeException("Redis Configuration Error: Missing environment variable(s): " . implode(', ', $missing));
|
||||
}
|
||||
|
||||
try {
|
||||
$redis = new \Redis();
|
||||
|
||||
$host = getenv('REDIS_HOST') ?: '127.0.0.1';
|
||||
$port = (int)(getenv('REDIS_PORT') ?: 6379);
|
||||
$password = getenv('REDIS_PASSWORD') ?: null;
|
||||
|
||||
// Connect with a 2 second timeout
|
||||
if (!$redis->connect($host, $port, 2.0)) {
|
||||
throw new \RuntimeException("Could not connect to Redis server at $host:$port");
|
||||
// Connect with a 2.5 second timeout
|
||||
if (!$redis->connect($host, (int)$port, 2.5)) {
|
||||
throw new \RuntimeException("Could not connect to Redis server at {$host}:{$port}");
|
||||
}
|
||||
|
||||
// Authenticate if password is provided
|
||||
if ($password) {
|
||||
if (!$redis->auth($password)) {
|
||||
throw new \RuntimeException("Redis authentication failed.");
|
||||
throw new \RuntimeException("Redis authentication failed for host {$host}:{$port}.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +48,8 @@ class RedisClient
|
||||
|
||||
self::$instance = $redis;
|
||||
} catch (\Exception $e) {
|
||||
// In production, fallback gracefully or throw HTTP 500
|
||||
error_log("Redis Connection Error: " . $e->getMessage());
|
||||
throw new \RuntimeException("Redis is unavailable. Please ensure the Redis server is running.");
|
||||
throw new \RuntimeException("Redis connection failed ({$host}:{$port}): " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,17 +6,18 @@ namespace App\Core;
|
||||
* Advanced OWASP Security Helper
|
||||
* Handles AES-256-GCM encryption/decryption, HMAC Blind Indexing,
|
||||
* Bcrypt password hashing, and JWT validation.
|
||||
* Strict environment variable enforcement (No default fallbacks).
|
||||
*/
|
||||
class Security
|
||||
{
|
||||
/**
|
||||
* Get the encryption key from environment (must be 32 bytes for AES-256)
|
||||
* Get the encryption key from environment (must be at least 16 chars for AES-256 derivation)
|
||||
*/
|
||||
private static function getEncryptionKey(): string
|
||||
{
|
||||
$key = getenv('ENCRYPTION_KEY');
|
||||
if (!$key || strlen($key) < 16) {
|
||||
throw new \RuntimeException("ENCRYPTION_KEY environment variable is empty or too short. Cryptographic operations aborted.");
|
||||
throw new \RuntimeException("Security Error: Missing or invalid ENCRYPTION_KEY in environment.");
|
||||
}
|
||||
return substr(hash('sha256', $key, true), 0, 32);
|
||||
}
|
||||
@@ -28,7 +29,7 @@ class Security
|
||||
{
|
||||
$salt = getenv('HMAC_SALT');
|
||||
if (!$salt) {
|
||||
throw new \RuntimeException("HMAC_SALT environment variable is empty. Cryptographic operations aborted.");
|
||||
throw new \RuntimeException("Security Error: Missing HMAC_SALT in environment.");
|
||||
}
|
||||
return $salt;
|
||||
}
|
||||
@@ -40,7 +41,7 @@ class Security
|
||||
{
|
||||
$secret = getenv('JWT_SECRET');
|
||||
if (!$secret) {
|
||||
throw new \RuntimeException("JWT_SECRET environment variable is empty. Cryptographic operations aborted.");
|
||||
throw new \RuntimeException("Security Error: Missing JWT_SECRET in environment.");
|
||||
}
|
||||
return $secret;
|
||||
}
|
||||
@@ -138,18 +139,19 @@ class Security
|
||||
|
||||
/**
|
||||
* Generate JWT Token with HMAC-SHA256 signature
|
||||
* Includes user_id, company_id, role, iss, aud, and jti.
|
||||
*/
|
||||
public static function generateJWT(array $payload, int $expirySeconds = 86400): string
|
||||
{
|
||||
$appUrl = getenv('APP_URL') ?: 'https://saqel.intaleqapp.com';
|
||||
|
||||
$header = self::base64UrlEncode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
|
||||
|
||||
// Standard OWASP Claims
|
||||
$payload['iat'] = time();
|
||||
$payload['exp'] = time() + $expirySeconds;
|
||||
$payload['iss'] = getenv('APP_URL'); // Issuer
|
||||
$payload['aud'] = 'saqel_app'; // Audience
|
||||
$payload['jti'] = bin2hex(random_bytes(16)); // JWT ID to prevent Replay Attacks
|
||||
$payload['iss'] = $appUrl;
|
||||
$payload['aud'] = 'saqel_app';
|
||||
$payload['jti'] = bin2hex(random_bytes(16));
|
||||
|
||||
$payloadEncoded = self::base64UrlEncode(json_encode($payload));
|
||||
|
||||
@@ -188,15 +190,10 @@ class Security
|
||||
if (!$payload || !isset($payload['exp']) || time() >= $payload['exp']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate Issuer
|
||||
$expectedIssuer = getenv('APP_URL');
|
||||
if (isset($payload['iss']) && $payload['iss'] !== $expectedIssuer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private static function base64UrlEncode(string $data): string
|
||||
{
|
||||
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||
|
||||
Reference in New Issue
Block a user