Commit f4e293a6 authored by Administrator's avatar Administrator

Update 12 files via Son of Anton

parent a6d47855
Pipeline #28 canceled with stage
APP_URL=https://hr.al-arcade.com
APP_DEBUG=false
APP_KEY=CHANGE-ME-TO-64-CHAR-RANDOM-STRING
APP_URL=https://hrsystem.caprover.al-arcade.com
APP_DEBUG=true
APP_KEY=al-arcade-hr-v3-2025-secure-key
DB_HOST=srv-captain--mysql-db
DB_PORT=3306
......
......@@ -11,6 +11,7 @@ RUN apt-get update && apt-get install -y \
libcurl4-openssl-dev \
zip unzip curl \
default-mysql-client \
cron \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install \
pdo_mysql \
......@@ -27,6 +28,9 @@ RUN apt-get update && apt-get install -y \
# Apache configuration
RUN a2enmod rewrite headers
# Suppress ServerName warning globally
RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf
# PHP configuration
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY docker/php-custom.ini /usr/local/etc/php/conf.d/99-custom.ini
......@@ -46,11 +50,10 @@ RUN mkdir -p \
&& chown -R www-data:www-data /var/www/html/storage \
&& chmod -R 775 /var/www/html/storage
# ─── Apache: Set DocumentRoot to /var/www/html/public ───
# Do NOT use sed — it causes double-path bugs. Use a clean vhost config instead.
# Apache vhost
COPY docker/000-default.conf /etc/apache2/sites-available/000-default.conf
# Entrypoint for DB initialization
# Entrypoint
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
......
......@@ -21,32 +21,30 @@ use Engine\Scheduler\JobRunner;
// Timezone
date_default_timezone_set('Africa/Cairo');
// Error handling
// Error handling — log everything
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) {
// Don't throw on suppressed errors
if (!(error_reporting() & $errno)) return false;
throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
});
set_exception_handler(function (\Throwable $e) {
error_log("UNCAUGHT: {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}");
if (!headers_sent()) {
http_response_code(500);
header('Content-Type: text/html');
}
$errorPage = ROOT_PATH . '/templates/errors/500.php';
if (file_exists($errorPage)) { require $errorPage; } else { echo '<h1>500 Internal Server Error</h1>'; }
exit(1);
});
// ─── CONTAINER REGISTRATION ───
$container = Container::getInstance();
// Config first — everything depends on it
$container->singleton(Config::class, fn() => new Config());
// Database — everything depends on it
$container->singleton(Connection::class, fn() => new Connection());
$container->singleton(SessionManager::class, fn() => new SessionManager());
// Auth layer
$container->singleton(PasswordHasher::class, fn() => new PasswordHasher());
$container->singleton(SessionManager::class, fn() => new SessionManager());
$container->singleton(RateLimiter::class, fn() => new RateLimiter());
$container->singleton(Authenticator::class, fn() => new Authenticator());
$container->singleton(PermissionEngine::class, fn() => new PermissionEngine());
// Services
$container->singleton(AuditLogger::class, fn() => new AuditLogger());
$container->singleton(NotificationManager::class, fn() => new NotificationManager());
$container->singleton(EventDispatcher::class, fn() => new EventDispatcher());
......@@ -61,16 +59,26 @@ $container->singleton(Router::class, fn() => new Router());
// ─── CALCULATION ENGINE ───
$calcEngine = new CalculationEngine();
$calculators = require ROOT_PATH . '/config/calculators.php';
foreach ($calculators as $name => $class) {
$calcEngine->register($name, $class);
$calculatorsFile = ROOT_PATH . '/config/calculators.php';
if (file_exists($calculatorsFile)) {
$calculators = require $calculatorsFile;
foreach ($calculators as $name => $class) {
$calcEngine->register($name, $class);
}
}
$container->instance(CalculationEngine::class, $calcEngine);
// ─── LOAD ALL MODULE ROUTES ───
$routeFiles = glob(ROOT_PATH . '/modules/*/routes.php');
foreach ($routeFiles as $routeFile) {
require_once $routeFile;
if ($routeFiles) {
foreach ($routeFiles as $routeFile) {
try {
require_once $routeFile;
} catch (\Throwable $e) {
error_log("ROUTE LOAD ERROR [{$routeFile}]: " . $e->getMessage());
// Don't die — skip broken route files and continue
}
}
}
return $container;
\ No newline at end of file
......@@ -7,40 +7,50 @@ use Engine\Core\Container;
use Engine\Database\Connection;
use Engine\Auth\PasswordHasher;
$db = Container::getInstance()->resolve(Connection::class);
$hasher = Container::getInstance()->resolve(PasswordHasher::class);
try {
$db = Container::getInstance()->resolve(Connection::class);
$hasher = Container::getInstance()->resolve(PasswordHasher::class);
} catch (\Throwable $e) {
echo "ERROR connecting to database: " . $e->getMessage() . "\n";
exit(1);
}
$username = getenv('SA_USERNAME') ?: 'admin';
$password = getenv('SA_PASSWORD') ?: 'Alarcade123#';
$nameEn = getenv('SA_NAME_EN') ?: 'Super Admin';
$nameAr = getenv('SA_NAME_AR') ?: دير النظام';
$nameEn = getenv('SA_NAME_EN') ?: 'Mahmoud Aglan';
$nameAr = getenv('SA_NAME_AR') ?: حمود عجلان';
$exists = $db->fetchOne("SELECT id FROM users WHERE username = ?", [$username]);
if ($exists) {
echo "Super admin '{$username}' already exists (ID: {$exists['id']})\n";
exit(0);
}
try {
$exists = $db->fetchOne("SELECT id FROM users WHERE username = ?", [$username]);
if ($exists) {
echo "✅ Super Admin '{$username}' already exists (ID: {$exists['id']}). Skipping.\n";
exit(0);
}
$id = $db->insert('users', [
'username' => $username,
'password_hash' => $hasher->hash($password),
'role' => 'super_admin',
'full_name_en' => $nameEn,
'full_name_ar' => $nameAr,
'national_id' => '00000000000000',
'date_of_birth' => '1990-01-01',
'phone_primary' => '+201000000000',
'address' => 'System',
'emergency_contact_name' => 'N/A',
'emergency_contact_phone' => '+201000000001',
'emergency_contact_relationship' => 'other',
'bank_name' => 'N/A',
'bank_account_number' => 'N/A',
'bank_account_holder' => $nameEn,
'status' => 'active',
'activation_date' => date('Y-m-d'),
'is_active' => 1,
'force_password_change' => 0,
]);
$id = $db->insert('users', [
'username' => $username,
'password_hash' => $hasher->hash($password),
'role' => 'super_admin',
'full_name_en' => $nameEn,
'full_name_ar' => $nameAr,
'national_id' => '00000000000000',
'date_of_birth' => '1990-01-01',
'phone_primary' => '+201000000000',
'address' => 'System Administrator',
'emergency_contact_name' => 'N/A',
'emergency_contact_phone' => '+201000000001',
'emergency_contact_relationship' => 'other',
'bank_name' => 'N/A',
'bank_account_number' => 'N/A',
'bank_account_holder' => $nameEn,
'status' => 'active',
'activation_date' => date('Y-m-d'),
'is_active' => 1,
'force_password_change' => 0,
]);
echo "✅ Super admin created: {$username} (ID: {$id})\n";
\ No newline at end of file
echo "✅ Super Admin created: {$username} (ID: {$id})\n";
} catch (\Throwable $e) {
echo "ERROR: " . $e->getMessage() . "\n";
exit(1);
}
\ No newline at end of file
......@@ -2,9 +2,9 @@
return [
'name' => 'AL-ARCADE HR Platform',
'version' => '3.0.0',
'url' => getenv('APP_URL') ?: 'https://hrsystem.caprover.al-arcade.com',
'debug' => (bool)(getenv('APP_DEBUG') ?: false),
'url' => 'https://hrsystem.caprover.al-arcade.com',
'debug' => true,
'timezone' => 'Africa/Cairo',
'locale' => 'en',
'key' => getenv('APP_KEY') ?: 'al-arcade-hr-v3-2025-secure-key-do-not-share-with-anyone-ever',
'key' => 'al-arcade-hr-v3-2025-secure-key-do-not-share',
];
\ No newline at end of file
<?php
return [
'host' => getenv('DB_HOST') ?: 'srv-captain--mysql-db',
'port' => (int)(getenv('DB_PORT') ?: 3306),
'database' => getenv('DB_NAME') ?: 'al_arcade_hr',
'username' => getenv('DB_USER') ?: 'root',
'password' => getenv('DB_PASS') ?: 'Alarcade123#',
'host' => 'srv-captain--mysql-db',
'port' => 3306,
'database' => 'al_arcade_hr',
'username' => 'root',
'password' => 'Alarcade123#',
'charset' => 'utf8mb4',
'collation'=> 'utf8mb4_unicode_ci',
'options' => [
......
<VirtualHost *:80>
ServerName hrsystem.caprover.al-arcade.com
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html/public
<Directory /var/www/html/public>
AllowOverride All
Require all granted
Options -Indexes +FollowSymLinks
# Disable MultiViews to prevent Apache from doing content negotiation
Options -MultiViews
Options -Indexes +FollowSymLinks -MultiViews
</Directory>
# Pass environment variables to PHP
PassEnv DB_HOST DB_PORT DB_NAME DB_USER DB_PASS
PassEnv SA_USERNAME SA_PASSWORD SA_NAME_EN SA_NAME_AR
PassEnv APP_URL APP_DEBUG APP_KEY
# Deny access to sensitive directories
<DirectoryMatch "^/var/www/html/(engine|config|database|cli|bootstrap|storage|modules|middleware|templates)">
Require all denied
......
......@@ -5,55 +5,63 @@ echo "==========================================="
echo " AL-ARCADE HR Platform v3.0 — Entrypoint"
echo "==========================================="
# ─── Hardcoded defaults (user doesn't use env vars) ───
export DB_HOST="${DB_HOST:-srv-captain--mysql-db}"
export DB_PORT="${DB_PORT:-3306}"
export DB_NAME="${DB_NAME:-al_arcade_hr}"
export DB_USER="${DB_USER:-root}"
export DB_PASS="${DB_PASS:-Alarcade123#}"
export SA_USERNAME="${SA_USERNAME:-admin}"
export SA_PASSWORD="${SA_PASSWORD:-Alarcade123#}"
export SA_NAME_EN="${SA_NAME_EN:-Mahmoud Aglan}"
export SA_NAME_AR="${SA_NAME_AR:-محمود عجلان}"
# ─── Wait for MySQL ───
echo "[INIT] Waiting for MySQL at ${DB_HOST:-srv-captain--mysql-db}:${DB_PORT:-3306}..."
MAX_TRIES=30
echo "[INIT] Waiting for MySQL at ${DB_HOST}:${DB_PORT}..."
MAX_TRIES=60
COUNT=0
until mysql -h "${DB_HOST:-srv-captain--mysql-db}" -P "${DB_PORT:-3306}" -u "${DB_USER:-root}" -p"${DB_PASS:-Alarcade123#}" -e "SELECT 1" &>/dev/null; do
until mysqladmin ping -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASS}" --skip-ssl --silent 2>/dev/null; do
COUNT=$((COUNT+1))
if [ $COUNT -ge $MAX_TRIES ]; then
echo "[ERROR] MySQL not available after ${MAX_TRIES} attempts. Starting Apache anyway..."
break
fi
echo "[INIT] MySQL not ready yet (attempt $COUNT/$MAX_TRIES)..."
echo "[INIT] MySQL not ready (attempt $COUNT/$MAX_TRIES)..."
sleep 2
done
echo "[INIT] MySQL is available."
# ─── Run Schema ───
DB_NAME="${DB_NAME:-al_arcade_hr}"
DB_EXISTS=$(mysql -h "${DB_HOST:-srv-captain--mysql-db}" -P "${DB_PORT:-3306}" -u "${DB_USER:-root}" -p"${DB_PASS:-Alarcade123#}" -se "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='${DB_NAME}'" 2>/dev/null || echo "0")
# ─── Ensure Database Exists ───
mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASS}" --skip-ssl \
-e "CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>/dev/null || true
if [ "$DB_EXISTS" = "0" ] || [ -z "$DB_EXISTS" ]; then
echo "[INIT] Database empty or not found. Running schema..."
mysql -h "${DB_HOST:-srv-captain--mysql-db}" -P "${DB_PORT:-3306}" -u "${DB_USER:-root}" -p"${DB_PASS:-Alarcade123#}" < /var/www/html/database/schema.sql 2>&1 || echo "[WARN] Schema import had warnings"
echo "[INIT] Schema imported."
# ─── Check if schema needs to be imported ───
TABLE_COUNT=$(mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASS}" --skip-ssl \
-se "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='${DB_NAME}'" 2>/dev/null || echo "0")
if [ "$TABLE_COUNT" = "0" ] || [ -z "$TABLE_COUNT" ]; then
echo "[INIT] Database empty. Running schema..."
mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASS}" --skip-ssl \
< /var/www/html/database/schema.sql 2>&1 || echo "[WARN] Schema import had warnings"
# Seed data
if [ -f /var/www/html/database/seed.sql ]; then
echo "[INIT] Running seed SQL..."
mysql -h "${DB_HOST:-srv-captain--mysql-db}" -P "${DB_PORT:-3306}" -u "${DB_USER:-root}" -p"${DB_PASS:-Alarcade123#}" "${DB_NAME}" < /var/www/html/database/seed.sql 2>&1 || echo "[WARN] Seed had warnings"
mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASS}" --skip-ssl "${DB_NAME}" \
< /var/www/html/database/seed.sql 2>&1 || echo "[WARN] Seed had warnings"
fi
# Run PHP seed
if [ -f /var/www/html/cli/seed.php ]; then
echo "[INIT] Running PHP seed..."
php /var/www/html/cli/seed.php 2>&1 || echo "[WARN] PHP seed had errors"
fi
# Create super admin
if [ -f /var/www/html/cli/create-superadmin.php ]; then
echo "[INIT] Creating super admin..."
php /var/www/html/cli/create-superadmin.php 2>&1 || echo "[WARN] Superadmin creation had errors"
fi
else
echo "[INIT] Database already has ${DB_EXISTS} tables. Skipping schema import."
echo "[INIT] Database has ${TABLE_COUNT} tables. Skipping schema."
fi
# Still try to create superadmin if it doesn't exist
if [ -f /var/www/html/cli/create-superadmin.php ]; then
php /var/www/html/cli/create-superadmin.php 2>&1 || true
fi
# ─── Create Super Admin ───
if [ -f /var/www/html/cli/create-superadmin.php ]; then
echo "[INIT] Ensuring super admin exists..."
php /var/www/html/cli/create-superadmin.php 2>&1 || true
fi
# ─── Fix permissions ───
......@@ -62,7 +70,6 @@ chown -R www-data:www-data /var/www/html/storage 2>/dev/null || true
chmod -R 775 /var/www/html/storage 2>/dev/null || true
# ─── Setup cron ───
echo "[INIT] Setting up cron job..."
echo "*/5 * * * * cd /var/www/html && php cron/runner.php >> /var/www/html/storage/logs/cron.log 2>&1" | crontab - 2>/dev/null || true
service cron start 2>/dev/null || true
......@@ -70,5 +77,4 @@ echo "==========================================="
echo " AL-ARCADE HR Platform — Ready!"
echo "==========================================="
# Start Apache
exec "$@"
\ No newline at end of file
......@@ -6,16 +6,34 @@ namespace Engine\Database;
final class Connection
{
private \PDO $pdo;
private static ?Connection $instance = null;
public function __construct()
{
$config = require ROOT_PATH . '/config/database.php';
$dsn = sprintf('mysql:host=%s;port=%d;dbname=%s;charset=%s',
$config['host'], $config['port'], $config['database'], $config['charset']
$dsn = sprintf(
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
$config['host'],
$config['port'],
$config['database'],
$config['charset']
);
$this->pdo = new \PDO($dsn, $config['username'], $config['password'], $config['options'] ?? []);
$options = $config['options'] ?? [];
// Fix MySQL 8.0+ SSL issues in Docker containers
if (!isset($options[\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT])) {
$options[\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false;
}
try {
$this->pdo = new \PDO($dsn, $config['username'], $config['password'], $options);
} catch (\PDOException $e) {
// If SSL fails, retry without SSL
$options[\PDO::MYSQL_ATTR_SSL_CA] = '';
$options[\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false;
$this->pdo = new \PDO($dsn, $config['username'], $config['password'], $options);
}
}
public function getPdo(): \PDO { return $this->pdo; }
......
<?php
declare(strict_types=1);
// ─── BOOTSTRAP ───
$container = require __DIR__ . '/../bootstrap/app.php';
// ─── SHOW ERRORS IN OUTPUT (until production-stable) ───
error_reporting(E_ALL);
ini_set('display_errors', '1');
ini_set('log_errors', '1');
use Engine\Core\{Router, Request, Response};
try {
// ─── BOOTSTRAP ───
$container = require __DIR__ . '/../bootstrap/app.php';
// ─── HANDLE REQUEST ───
$request = new Request();
$router = $container->resolve(Router::class);
use Engine\Core\{Router, Request, Response};
// ─── HANDLE REQUEST ───
$request = new Request();
$router = $container->resolve(Router::class);
try {
$response = $router->dispatch($request);
$response->send();
} catch (\Engine\Auth\ForbiddenException $e) {
$response = $request->wantsJson()
? Response::json(['error' => 'Forbidden'], 403)
: Response::html('<h1>403 Forbidden</h1>', 403);
http_response_code(403);
echo '<h1>403 Forbidden</h1><p>' . htmlspecialchars($e->getMessage()) . '</p>';
} catch (\Throwable $e) {
error_log("REQUEST ERROR: {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}\n{$e->getTraceAsString()}");
$response = $request->wantsJson()
? Response::json(['error' => 'Internal server error'], 500)
: Response::html(file_get_contents(ROOT_PATH . '/templates/errors/500.php') ?: '<h1>500</h1>', 500);
}
// ─── SHOW THE ACTUAL FUCKING ERROR ───
http_response_code(500);
$msg = $e->getMessage();
$file = $e->getFile();
$line = $e->getLine();
$trace = $e->getTraceAsString();
error_log("FATAL: {$msg} in {$file}:{$line}\n{$trace}");
$response->send();
\ No newline at end of file
echo <<<HTML
<!DOCTYPE html>
<html>
<head><title>500 — Server Error</title>
<style>
body{font-family:-apple-system,sans-serif;background:#0f172a;color:#e2e8f0;margin:0;padding:40px;min-height:100vh}
h1{color:#ef4444;font-size:2.5em;margin-bottom:10px}
.box{max-width:900px;margin:0 auto}
.error-msg{background:#1e293b;padding:20px;border-radius:8px;border-left:4px solid #ef4444;margin:16px 0;font-size:1.1em;color:#fbbf24;word-wrap:break-word}
.trace{background:#1e293b;padding:16px;border-radius:8px;font-family:monospace;font-size:0.8em;overflow-x:auto;white-space:pre-wrap;color:#94a3b8;margin:16px 0;max-height:500px;overflow-y:auto}
.file{color:#818cf8;font-size:0.9em}
a{color:#6366f1}
</style>
</head>
<body>
<div class="box">
<h1>500 — Application Error</h1>
<div class="error-msg">{$msg}</div>
<div class="file">{$file}:{$line}</div>
<h3 style="margin-top:20px;color:#94a3b8">Stack Trace:</h3>
<div class="trace">{$trace}</div>
<p style="margin-top:20px"><a href="/login">← Try Login</a> · <a href="/dashboard">Dashboard</a></p>
</div>
</body>
</html>
HTML;
}
\ No newline at end of file
<!DOCTYPE html><html><head><title>404</title><link rel="stylesheet" href="/assets/css/app.css"></head>
<body class="error-page"><div class="error-container"><h1>404</h1><p>Page not found.</p><a href="/dashboard" class="btn btn-primary">Go Home</a></div></body></html>
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<title>404 — Not Found</title>
<style>
body{font-family:-apple-system,sans-serif;background:#0f172a;color:#e2e8f0;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}
.box{text-align:center;max-width:500px;padding:40px}
h1{font-size:5em;margin:0;color:#6366f1}
p{color:#94a3b8;margin:16px 0}
a{color:#6366f1;text-decoration:none;padding:10px 24px;border:2px solid #6366f1;border-radius:8px;display:inline-block;margin-top:16px}
a:hover{background:#6366f1;color:white}
</style>
</head>
<body>
<div class="box">
<h1>404</h1>
<p>Page not found.</p>
<a href="/dashboard">Go Home</a>
</div>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html><head><title>500 Server Error</title><link rel="stylesheet" href="/assets/css/app.css"></head>
<body class="error-page"><div class="error-container"><h1>500</h1><p>Internal server error.</p><a href="/dashboard" class="btn btn-primary">Go Home</a></div></body></html>
\ No newline at end of file
<html>
<head>
<title>500 — Server Error</title>
<style>
body{font-family:-apple-system,sans-serif;background:#0f172a;color:#e2e8f0;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}
.box{text-align:center;max-width:500px;padding:40px}
h1{font-size:5em;margin:0;color:#6366f1}
p{color:#94a3b8;margin:16px 0}
a{color:#6366f1;text-decoration:none;padding:10px 24px;border:2px solid #6366f1;border-radius:8px;display:inline-block;margin-top:16px}
a:hover{background:#6366f1;color:white}
</style>
</head>
<body>
<div class="box">
<h1>500</h1>
<p>Internal server error. Check the logs.</p>
<a href="/login">Go to Login</a>
</div>
</body>
</html>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment