require_once '../../includes/session.php'; app_session_start(); require_once '../../includes/db_connect.php'; require_once '../../includes/app_settings.php'; require_once '../../includes/csrf.php'; /** * Verify password against modern hashes and legacy formats. * * @return array{ok: bool, needs_rehash: bool} */ function verify_password_compat(string $inputPassword, string $storedValue): array { if ($storedValue === '') { return ['ok' => false, 'needs_rehash' => false]; } if (password_verify($inputPassword, $storedValue)) { return [ 'ok' => true, 'needs_rehash' => password_needs_rehash($storedValue, PASSWORD_BCRYPT), ]; } $isMd5 = preg_match('/^[a-f0-9]{32}$/i', $storedValue) === 1; if ($isMd5 && hash_equals(strtolower($storedValue), md5($inputPassword))) { return ['ok' => true, 'needs_rehash' => true]; } $isSha1 = preg_match('/^[a-f0-9]{40}$/i', $storedValue) === 1; if ($isSha1 && hash_equals(strtolower($storedValue), sha1($inputPassword))) { return ['ok' => true, 'needs_rehash' => true]; } if (hash_equals($storedValue, $inputPassword)) { return ['ok' => true, 'needs_rehash' => true]; } return ['ok' => false, 'needs_rehash' => false]; } function ensure_login_attempts_table(PDO $pdo): void { $pdo->exec( "CREATE TABLE IF NOT EXISTS login_attempts ( attempt_id bigint unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY, username varchar(191) NOT NULL, ip_address varchar(64) NOT NULL, attempt_count int unsigned NOT NULL DEFAULT 0, locked_until datetime NULL DEFAULT NULL, updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uniq_login_attempt (username, ip_address) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); } /** * @return array{attempt_count: int, locked_until: string}|null */ function get_login_attempt(PDO $pdo, string $username, string $ipAddress): ?array { $stmt = $pdo->prepare( 'SELECT attempt_count, locked_until FROM login_attempts WHERE username = :username AND ip_address = :ip_address LIMIT 1' ); $stmt->execute([ ':username' => $username, ':ip_address' => $ipAddress, ]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row) { return null; } return [ 'attempt_count' => (int) ($row['attempt_count'] ?? 0), 'locked_until' => (string) ($row['locked_until'] ?? ''), ]; } function login_attempt_remaining_seconds(string $lockedUntil): int { if ($lockedUntil === '') { return 0; } $timestamp = strtotime($lockedUntil); if ($timestamp === false) { return 0; } $remaining = $timestamp - time(); return $remaining > 0 ? $remaining : 0; } function clear_login_attempt(PDO $pdo, string $username, string $ipAddress): void { $stmt = $pdo->prepare( 'DELETE FROM login_attempts WHERE username = :username AND ip_address = :ip_address' ); $stmt->execute([ ':username' => $username, ':ip_address' => $ipAddress, ]); } /** * Register a failed login attempt. * * @return int Remaining lockout seconds after this failure. 0 means not locked. */ function register_failed_login_attempt( PDO $pdo, string $username, string $ipAddress, int $maxAttempts, int $lockoutMinutes ): int { $existing = get_login_attempt($pdo, $username, $ipAddress); $now = time(); $attemptCount = 1; $lockedUntil = ''; if ($existing !== null) { $currentLockRemaining = login_attempt_remaining_seconds($existing['locked_until']); if ($currentLockRemaining > 0) { return $currentLockRemaining; } if ($existing['locked_until'] !== '') { $attemptCount = 1; } else { $attemptCount = $existing['attempt_count'] + 1; } } if ($attemptCount >= $maxAttempts) { $lockedUntil = date('Y-m-d H:i:s', $now + ($lockoutMinutes * 60)); } $upsert = $pdo->prepare( 'INSERT INTO login_attempts (username, ip_address, attempt_count, locked_until) VALUES (:username, :ip_address, :attempt_count, :locked_until) ON DUPLICATE KEY UPDATE attempt_count = VALUES(attempt_count), locked_until = VALUES(locked_until)' ); $upsert->execute([ ':username' => $username, ':ip_address' => $ipAddress, ':attempt_count' => $attemptCount, ':locked_until' => $lockedUntil !== '' ? $lockedUntil : null, ]); if ($lockedUntil === '') { return 0; } return max(0, strtotime($lockedUntil) - $now); } $error = ''; $passwordColumn = 'password_hash'; $hasUserRoleColumn = true; $loginAttemptTrackingAvailable = true; $appSettings = app_settings_load_safe($pdo); $siteTitle = trim((string) ($appSettings['site_title'] ?? 'منظومة التراخيص')); if ($siteTitle === '') { $siteTitle = 'منظومة التراخيص'; } $siteSubtitle = trim((string) ($appSettings['site_subtitle'] ?? 'منح الشهادة السلبية وإذن المزاولة للمكاتب والشركات السياحية')); $maintenanceMode = app_settings_is_enabled($appSettings, 'maintenance_mode'); $allowUserLogin = app_settings_is_enabled($appSettings, 'allow_user_login', true); $maintenanceMessage = trim((string) ($appSettings['maintenance_message'] ?? '')); if ($maintenanceMessage === '') { $maintenanceMessage = 'النظام في وضع الصيانة حالياً. يرجى المحاولة لاحقاً.'; } $maxLoginAttempts = app_settings_to_int($appSettings['max_login_attempts'] ?? '5', 5, 1, 20); $loginLockoutMinutes = app_settings_to_int($appSettings['login_lockout_minutes'] ?? '15', 15, 1, 1440); $sessionTimeoutMinutes = app_settings_to_int($appSettings['session_timeout_minutes'] ?? '480', 480, 5, 1440); $timezone = trim((string) ($appSettings['timezone'] ?? 'Africa/Tripoli')); if ($timezone !== '' && in_array($timezone, timezone_identifiers_list(), true)) { date_default_timezone_set($timezone); } try { $columnStmt = $pdo->query('SHOW COLUMNS FROM users'); $columns = $columnStmt ? $columnStmt->fetchAll(PDO::FETCH_COLUMN, 0) : []; $columns = array_map(static fn($col) => strtolower((string) $col), $columns); $hasPasswordHashColumn = in_array('password_hash', $columns, true); $hasLegacyPasswordColumn = in_array('password', $columns, true); $hasUserRoleColumn = in_array('user_role', $columns, true); if ($hasPasswordHashColumn) { $passwordColumn = 'password_hash'; } elseif ($hasLegacyPasswordColumn) { $passwordColumn = 'password'; } else { throw new RuntimeException('No password column found in users table.'); } } catch (Throwable $e) { $error = 'حدث خطأ في إعداد تسجيل الدخول.'; } if ($error === '') { try { ensure_login_attempts_table($pdo); } catch (Throwable $e) { $loginAttemptTrackingAvailable = false; } } if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!csrf_validate_post()) { $error = 'طلب غير صالح، يرجى إعادة المحاولة.'; } else { $username = trim((string) ($_POST['username'] ?? '')); $password = (string) ($_POST['password'] ?? ''); $ipAddress = substr((string) ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'), 0, 64); if ($username !== '' && $password !== '') { try { if ($error !== '') { throw new RuntimeException($error); } if ($loginAttemptTrackingAvailable) { $attempt = get_login_attempt($pdo, $username, $ipAddress); if ($attempt !== null) { $remaining = login_attempt_remaining_seconds($attempt['locked_until']); if ($remaining > 0) { $minutesLeft = (int) ceil($remaining / 60); $error = 'تم إيقاف تسجيل الدخول مؤقتاً. يرجى المحاولة بعد ' . $minutesLeft . ' دقيقة.'; throw new RuntimeException($error); } } } $roleSelect = $hasUserRoleColumn ? 'user_role' : "'' AS user_role"; $stmt = $pdo->prepare( "SELECT user_id, username, {$passwordColumn} AS password_hash, {$roleSelect} FROM users WHERE username = :username LIMIT 1" ); $stmt->execute([':username' => $username]); $user = $stmt->fetch(PDO::FETCH_ASSOC); $storedPassword = (string) ($user['password_hash'] ?? ''); $passwordCheck = $user ? verify_password_compat($password, $storedPassword) : ['ok' => false, 'needs_rehash' => false]; if ($user && $passwordCheck['ok']) { $roleCsv = (string) ($user['user_role'] ?? ''); $roles = array_values(array_filter(array_map( static fn($role) => trim($role), explode(',', $roleCsv) ), static fn($role) => $role !== '')); $isAdmin = in_array('admin', $roles, true); if ($maintenanceMode && !$isAdmin) { $error = $maintenanceMessage; throw new RuntimeException($error); } if (!$allowUserLogin && !$isAdmin) { $error = 'تم إيقاف تسجيل الدخول مؤقتاً من قبل الإدارة.'; throw new RuntimeException($error); } if ($passwordCheck['needs_rehash']) { try { $newHash = password_hash($password, PASSWORD_BCRYPT); $updateStmt = $pdo->prepare( "UPDATE users SET {$passwordColumn} = :password_hash WHERE user_id = :user_id" ); $updateStmt->execute([ ':password_hash' => $newHash, ':user_id' => (int) $user['user_id'], ]); } catch (Throwable $e) { // Do not block login if hash migration fails. } } if ($loginAttemptTrackingAvailable) { clear_login_attempt($pdo, $username, $ipAddress); } session_regenerate_id(true); $_SESSION['user_id'] = (int) $user['user_id']; $_SESSION['username'] = (string) $user['username']; $_SESSION['auth_logged_in'] = true; $_SESSION['last_activity'] = time(); $_SESSION['user_perms'] = $roles; $_SESSION['session_timeout_minutes'] = $sessionTimeoutMinutes; $_SESSION['app_timezone'] = $timezone; $_SESSION['site_title'] = $siteTitle; $_SESSION['site_subtitle'] = $siteSubtitle; $_SESSION['footer_text'] = (string) ($appSettings['footer_text'] ?? ''); $nextUrl = (string) ($_POST['next'] ?? $_GET['next'] ?? ''); $isSafeNext = $nextUrl !== '' && substr($nextUrl, 0, 1) === '/' && strpos($nextUrl, "\n") === false && strpos($nextUrl, "\r") === false && substr($nextUrl, 0, 2) !== '//'; if ($isSafeNext) { header('Location: ' . $nextUrl); } else { header('Location: ../dashboard.php'); } exit; } if ($loginAttemptTrackingAvailable) { $remainingAfterFailure = register_failed_login_attempt( $pdo, $username, $ipAddress, $maxLoginAttempts, $loginLockoutMinutes ); if ($remainingAfterFailure > 0) { $minutesLeft = (int) ceil($remainingAfterFailure / 60); $error = 'تم تجاوز عدد المحاولات المسموح. يرجى المحاولة بعد ' . $minutesLeft . ' دقيقة.'; } else { $error = 'اسم المستخدم أو كلمة المرور غير صحيحة.'; } } else { $error = 'اسم المستخدم أو كلمة المرور غير صحيحة.'; } } catch (Throwable $e) { if ($error === '') { $error = 'حدث خطأ أثناء التحقق من بيانات تسجيل الدخول.'; } } } else { $error = 'الرجاء إدخال اسم المستخدم وكلمة المرور.'; } } } ?>
الرجاء تسجيل الدخول للمتابعة