Update: 2026-07-30 12:31:26

This commit is contained in:
Hamza-Ayed
2026-07-30 12:31:27 +03:00
parent ecfe756849
commit 7a576b7327
10 changed files with 307 additions and 84 deletions
@@ -11,18 +11,16 @@ $password = filterRequest('password');
$audience = filterRequest('aud') ?? 'siro-driver-android'; // الافتراضي
$fingerprint = filterRequest('fingerPrint') ?? filterRequest('fingerprint');
// 1. تطبيق حد معدل الطلبات (Rate Limiting) للفاحصين: 3 محاولات بالدقيقة لكل IP
// 1. حد معدل الطلبات مطبّق على الجميع (الحد مرفوع إلى 30/دقيقة في RateLimiter)
$rateLimiter = new RateLimiter($redis);
$rateLimiter->enforce(RateLimiter::identifier(), 'tester_login');
if (!$email || !$password) {
echo json_encode(["status" => "failure", "message" => "Email and password are required"]);
exit();
}
// 2. التحقق من أن الحساب مخصص للفحص فقط (isTest check)
$allowedTesterEmailsEnv = getenv('ALLOWED_TESTER_EMAILS') ?: '';
$allowedEmails = array_filter(array_map('trim', explode(',', $allowedTesterEmailsEnv)));
// 2. قائمة بيضاء صريحة لحسابات الفحص — مطابقة تامة فقط، لا مطابقة جزئية ولا مطابقة نطاق
$allowedTesterEmailsEnv = getenv('ALLOWED_TESTER_EMAILS') ?: ($_ENV['ALLOWED_TESTER_EMAILS'] ?? '');
$allowedEmails = array_filter(array_map(
fn($e) => strtolower(trim($e)),
explode(',', $allowedTesterEmailsEnv)
));
if (empty($allowedEmails)) {
$allowedEmails = [
'driver_tester@siromove.com',
@@ -30,24 +28,21 @@ if (empty($allowedEmails)) {
];
}
$cleanEmail = strtolower(trim($email));
$isTester = in_array($cleanEmail, $allowedEmails) ||
substr($cleanEmail, -13) === '@siromove.com' ||
str_contains($cleanEmail, 'tester') ||
str_contains($cleanEmail, 'reviewer');
$cleanEmail = strtolower(trim((string) $email));
$isTester = in_array($cleanEmail, $allowedEmails, true);
// تشفير الإيميل لاستخدامه في الاستعلام
$encryptedEmail = $encryptionHelper->encryptData($email);
if (!$email || !$password) {
echo json_encode(["status" => "failure", "message" => "Email and password are required"]);
exit();
}
try {
$con = Database::get('main');
// Auto-seed/create tester driver logic removed for security
$encryptedEmail = $encryptionHelper->encryptData($email);
global $blindIndex;
$emailBidx = $blindIndex ? $blindIndex->index('driver.email', $email) : null;
// SQL لاسترجاع المستخدم بناءً على البريد الإلكتروني المشفر أو الفهرس الأعمى
$sql = "SELECT
driver.*,
phone_verification.is_verified,
@@ -67,36 +62,30 @@ try {
$data = $stmt->fetch(PDO::FETCH_ASSOC);
if ($data) {
// التحقق من أن الحساب معلم كحساب فحص في قاعدة البيانات أو البيئة
$isTestInDb = (isset($data['is_test']) && $data['is_test'] == 1) || (isset($data['isTest']) && $data['isTest'] == 1);
if (!$isTestInDb && !$isTester) {
jsonError("Access denied. Not a tester account.");
exit();
}
// فحص الباسورد (في نظامنا، يمكن أن يكون الباسورد هو HMAC أو نص عادي للفاحصين)
// لنفترض أن الفاحص له باسورد عادي أو مشفر بـ bcrypt
if (password_verify($password, $data['password'])) {
if (password_verify($password, $data['password'] ?? '')) {
unset($data['password']);
// فك تشفير الحقول الحساسة
$data['phone'] = $encryptionHelper->decryptData($data['phone']);
$data['email'] = $encryptionHelper->decryptData($data['email']);
$data['gender'] = $encryptionHelper->decryptData($data['gender']);
$data['birthdate'] = $encryptionHelper->decryptData($data['birthdate']);
$data['site'] = $encryptionHelper->decryptData($data['site']);
$data['first_name'] = $encryptionHelper->decryptData($data['first_name']);
$data['last_name'] = $encryptionHelper->decryptData($data['last_name']);
if(isset($data['employmentType'])) $data['employmentType'] = $encryptionHelper->decryptData($data['employmentType']);
if(isset($data['maritalStatus'])) $data['maritalStatus'] = $encryptionHelper->decryptData($data['maritalStatus']);
if(isset($data['phone'])) $data['phone'] = $encryptionHelper->decryptData($data['phone']);
if(isset($data['email'])) $data['email'] = $encryptionHelper->decryptData($data['email']);
if(isset($data['gender'])) $data['gender'] = $encryptionHelper->decryptData($data['gender']);
if(isset($data['birthdate'])) $data['birthdate'] = $encryptionHelper->decryptData($data['birthdate']);
if(isset($data['site'])) $data['site'] = $encryptionHelper->decryptData($data['site']);
if(isset($data['first_name'])) $data['first_name'] = $encryptionHelper->decryptData($data['first_name']);
if(isset($data['last_name'])) $data['last_name'] = $encryptionHelper->decryptData($data['last_name']);
// توليد الـ JWT بصلاحية (tester) لتميزهم عن السائقين الفعليين
$jwtService = new JwtService($redis);
$jwt = $jwtService->generateAccessToken($data['id'], 'tester', $audience, $fingerprint);
echo json_encode([
"status" => "success",
"jwt" => $jwt,
"data" => [$data] // مطابق لنسق التطبيق الذي يتوقع مصفوفة
"data" => [$data]
], JSON_UNESCAPED_UNICODE);
} else {
jsonError("Incorrect password.");
@@ -104,8 +93,8 @@ try {
} else {
jsonError("User does not exist.");
}
} catch (Exception $e) {
error_log("[Tester Login Error] " . $e->getMessage());
} catch (Throwable $e) {
error_log("[Tester Login Error] " . $e->getMessage() . " in " . $e->getFile() . ":" . $e->getLine());
jsonError("Server error occurred.");
} finally {
$stmt = null;
@@ -9,18 +9,16 @@ $password = filterRequest("password");
$fingerprint = filterRequest('fingerPrint') ?? filterRequest('fingerprint');
$audience = filterRequest('aud') ?: 'siro_passenger';
// 1. تطبيق حد معدل الطلبات (Rate Limiting) للفاحصين: 3 محاولات بالدقيقة لكل IP
// 1. حد معدل الطلبات مطبّق على الجميع (الحد مرفوع إلى 30/دقيقة في RateLimiter)
$rateLimiter = new RateLimiter($redis);
$rateLimiter->enforce(RateLimiter::identifier(), 'tester_login');
if (!$email || !$password) {
echo json_encode(["status" => "failure", "message" => "Email and password are required"]);
exit();
}
// 2. التحقق من أن الحساب مخصص للفحص فقط (isTest check)
$allowedTesterEmailsEnv = getenv('ALLOWED_TESTER_EMAILS') ?: '';
$allowedEmails = array_filter(array_map('trim', explode(',', $allowedTesterEmailsEnv)));
// 2. قائمة بيضاء صريحة لحسابات الفحص — مطابقة تامة فقط، لا مطابقة جزئية ولا مطابقة نطاق
$allowedTesterEmailsEnv = getenv('ALLOWED_TESTER_EMAILS') ?: ($_ENV['ALLOWED_TESTER_EMAILS'] ?? '');
$allowedEmails = array_filter(array_map(
fn($e) => strtolower(trim($e)),
explode(',', $allowedTesterEmailsEnv)
));
if (empty($allowedEmails)) {
$allowedEmails = [
'driver_tester@siromove.com',
@@ -28,12 +26,13 @@ if (empty($allowedEmails)) {
];
}
$cleanEmail = strtolower(trim((string) $email));
$isTester = in_array($cleanEmail, $allowedEmails, true);
$cleanEmail = strtolower(trim($email));
$isTester = in_array($cleanEmail, $allowedEmails) ||
substr($cleanEmail, -13) === '@siromove.com' ||
str_contains($cleanEmail, 'tester') ||
str_contains($cleanEmail, 'reviewer');
if (!$email || !$password) {
echo json_encode(["status" => "failure", "message" => "Email and password are required"]);
exit();
}
try {
$con = Database::get('main');
@@ -121,7 +120,7 @@ try {
http_response_code(500);
echo json_encode([
"status" => "failure",
"message" => "Server error: " . $e->getMessage() . " in " . basename($e->getFile()) . " on line " . $e->getLine()
"message" => "Server error. Please try again."
]);
}
exit();
+1 -1
View File
@@ -11,7 +11,7 @@ class RateLimiter
// حدود مختلفة لكل نوع endpoint
private const LIMITS = [
'login' => ['requests' => 5, 'window' => 60], // 5 محاولات / دقيقة
'tester_login' => ['requests' => 3, 'window' => 60], // 3 محاولات / دقيقة
'tester_login' => ['requests' => 30, 'window' => 60], // 30 محاولة / دقيقة (مراجعو المتاجر يكرّرون الدخول بسرعة)
'otp' => ['requests' => 3, 'window' => 300], // 3 محاولات / 5 دقائق
'register' => ['requests' => 3, 'window' => 3600], // 3 محاولات / ساعة
'api' => ['requests' => 180, 'window' => 60], // 180 طلب / دقيقة (الإنتاج الرسمى)
+214
View File
@@ -0,0 +1,214 @@
<?php
/**
* scripts/seed_tester_accounts.php
*
* ينشئ (أو يعيد تعيين كلمة مرور) حسابَي الفحص المخصصين لمراجعي المتاجر:
* راكب واحد وسائق واحد، بالبريدين الموجودين في ALLOWED_TESTER_EMAILS.
*
* الاستخدام:
* php seed_tester_accounts.php --password='...' # كلمة مرور واحدة للحسابين
* php seed_tester_accounts.php --passenger-password='...' --driver-password='...'
* php seed_tester_accounts.php --password='...' --dry-run # عرض ما سيحدث دون كتابة
*
* ملاحظات:
* - آمن لإعادة التشغيل: إن وُجد الحساب فيُحدَّث الباسورد فقط، دون إنشاء سجل ثانٍ.
* - يضبط سجل تحقق الهاتف على verified/is_verified = 1 حتى لا يُحجب الدخول.
* - كلمة المرور تُخزَّن بـ password_hash (bcrypt) لتطابق password_verify في مسار الدخول.
*/
declare(strict_types=1);
if (PHP_SAPI !== 'cli') {
http_response_code(403);
exit("This script runs from the command line only.\n");
}
require_once __DIR__ . '/../core/bootstrap.php';
$options = getopt('', ['dry-run', 'password::', 'passenger-password::', 'driver-password::']);
$dryRun = isset($options['dry-run']);
$sharedPassword = $options['password'] ?? null;
$passengerPassword = $options['passenger-password'] ?? $sharedPassword;
$driverPassword = $options['driver-password'] ?? $sharedPassword;
if (!$passengerPassword || !$driverPassword) {
exit("✘ مطلوب --password أو (--passenger-password و --driver-password).\n");
}
if (strlen($passengerPassword) < 8 || strlen($driverPassword) < 8) {
exit("✘ كلمة المرور يجب أن تكون 8 محارف على الأقل.\n");
}
/** @var EncryptionHelper $encryptionHelper */
global $encryptionHelper, $blindIndex;
// البريدان يجب أن يطابقا القائمة البيضاء في مسارَي الدخول حرفياً
$allowedEnv = getenv('ALLOWED_TESTER_EMAILS') ?: ($_ENV['ALLOWED_TESTER_EMAILS'] ?? '');
$allowed = array_values(array_filter(array_map(
fn($e) => strtolower(trim($e)),
explode(',', $allowedEnv)
)));
if (empty($allowed)) {
$allowed = ['driver_tester@siromove.com', 'passenger_tester@siromove.com'];
}
$passengerEmail = null;
$driverEmail = null;
foreach ($allowed as $e) {
if ($driverEmail === null && str_contains($e, 'driver')) {
$driverEmail = $e;
} elseif ($passengerEmail === null) {
$passengerEmail = $e;
}
}
if (!$passengerEmail || !$driverEmail) {
exit("✘ ALLOWED_TESTER_EMAILS يجب أن يحتوي بريد سائق (يتضمن 'driver') وبريد راكب.\n");
}
$passengerPhone = '+963900000000';
$driverPhone = '+963900000001';
$con = Database::get('main');
echo ($dryRun ? "— وضع المعاينة (لا كتابة) —\n" : "— تنفيذ فعلي —\n");
// ── الراكب ────────────────────────────────────────────────
$emailEnc = $encryptionHelper->encryptData($passengerEmail);
$emailBidx = $blindIndex ? $blindIndex->index('passengers.email', $passengerEmail) : null;
$hash = password_hash($passengerPassword, PASSWORD_BCRYPT);
$stmt = $con->prepare(
"SELECT id FROM passengers WHERE email = ? OR (? IS NOT NULL AND email_bidx = ?) LIMIT 1"
);
$stmt->execute([$emailEnc, $emailBidx, $emailBidx]);
$existing = $stmt->fetchColumn();
$phoneKey = otpPhoneKey($passengerPhone);
if ($existing) {
echo "• الراكب $passengerEmail موجود (id=$existing) — إعادة تعيين الباسورد.\n";
if (!$dryRun) {
$con->prepare("UPDATE passengers SET password = ?, updated_at = NOW() WHERE id = ?")
->execute([$hash, $existing]);
}
$passengerId = $existing;
} else {
$passengerId = substr(md5(uniqid((string) mt_rand(), true)), 0, 20);
echo "• إنشاء الراكب $passengerEmail (id=$passengerId).\n";
if (!$dryRun) {
$unknown = $encryptionHelper->encryptData('unknown');
$con->prepare("
INSERT INTO passengers
(id, first_name, last_name, email, phone, password, gender, birthdate, site,
sosPhone, education, employmentType, maritalStatus, status, created_at, updated_at,
phone_bidx, email_bidx, name_bidx, phone_key)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', NOW(), NOW(), ?, ?, ?, ?)
")->execute([
$passengerId,
$encryptionHelper->encryptData('Siro'),
$encryptionHelper->encryptData('Tester'),
$emailEnc,
$encryptionHelper->encryptData($passengerPhone),
$hash,
$unknown, $unknown, $unknown, $unknown, $unknown, $unknown, $unknown,
$blindIndex ? $blindIndex->index('passengers.phone', $passengerPhone) : null,
$emailBidx,
$blindIndex ? $blindIndex->index('passengers.name', 'Siro Tester') : null,
$phoneKey,
]);
}
}
// سجل تحقق الهاتف للراكب — الدخول يقرأ verified من هذا الجدول
$stmt = $con->prepare("SELECT id FROM phone_verification_passenger WHERE phone_number = ? LIMIT 1");
$stmt->execute([$phoneKey]);
$verifRow = $stmt->fetchColumn();
if ($verifRow) {
echo " ↳ تحديث سجل التحقق (verified = 1).\n";
if (!$dryRun) {
$con->prepare("UPDATE phone_verification_passenger SET verified = 1, status = 'verified' WHERE id = ?")
->execute([$verifRow]);
}
} else {
echo " ↳ إنشاء سجل التحقق (verified = 1).\n";
if (!$dryRun) {
$con->prepare("
INSERT INTO phone_verification_passenger (phone_number, verified, status, created_at)
VALUES (?, 1, 'verified', NOW())
")->execute([$phoneKey]);
}
}
// ── السائق ────────────────────────────────────────────────
$dEmailEnc = $encryptionHelper->encryptData($driverEmail);
$dEmailBidx = $blindIndex ? $blindIndex->index('driver.email', $driverEmail) : null;
$dHash = password_hash($driverPassword, PASSWORD_BCRYPT);
$stmt = $con->prepare(
"SELECT id FROM driver WHERE email = ? OR (? IS NOT NULL AND email_bidx = ?) LIMIT 1"
);
$stmt->execute([$dEmailEnc, $dEmailBidx, $dEmailBidx]);
$existingDriver = $stmt->fetchColumn();
$dPhoneKey = otpPhoneKey($driverPhone);
if ($existingDriver) {
echo "• السائق $driverEmail موجود (id=$existingDriver) — إعادة تعيين الباسورد.\n";
if (!$dryRun) {
$con->prepare("UPDATE driver SET password = ?, updated_at = NOW() WHERE id = ?")
->execute([$dHash, $existingDriver]);
}
$driverId = $existingDriver;
} else {
$driverId = substr(md5(uniqid((string) mt_rand(), true)), 0, 20);
echo "• إنشاء السائق $driverEmail (id=$driverId).\n";
if (!$dryRun) {
$con->prepare("
INSERT INTO driver
(id, phone, email, password, gender, license_type, national_number, name_arabic,
issue_date, expiry_date, license_categories, address, licenseIssueDate, status,
birthdate, site, first_name, last_name, created_at, updated_at,
phone_bidx, email_bidx, name_bidx, phone_key)
VALUES (?, ?, ?, ?, 'Male', 'private', ?, ?, '2020-01-01', '2030-01-01', 'B',
'Damascus', '2020-01-01', 'notDeleted', ?, ?, ?, ?, NOW(), NOW(), ?, ?, ?, ?)
")->execute([
$driverId,
$encryptionHelper->encryptData($driverPhone),
$dEmailEnc,
$dHash,
$encryptionHelper->encryptData('00000000'),
'سيرو فاحص',
$encryptionHelper->encryptData('1990-01-01'),
$encryptionHelper->encryptData('Damascus'),
$encryptionHelper->encryptData('Siro'),
$encryptionHelper->encryptData('Captain'),
$blindIndex ? $blindIndex->index('driver.phone', $driverPhone) : null,
$dEmailBidx,
$blindIndex ? $blindIndex->index('driver.name', 'Siro Captain') : null,
$dPhoneKey,
]);
}
}
// سجل تحقق الهاتف للسائق
$stmt = $con->prepare("SELECT id FROM phone_verification WHERE phone_number = ? LIMIT 1");
$stmt->execute([$dPhoneKey]);
$dVerifRow = $stmt->fetchColumn();
if ($dVerifRow) {
echo " ↳ تحديث سجل التحقق (is_verified = 1).\n";
if (!$dryRun) {
$con->prepare("UPDATE phone_verification SET is_verified = 1, driverId = ? WHERE id = ?")
->execute([$driverId, $dVerifRow]);
}
} else {
echo " ↳ إنشاء سجل التحقق (is_verified = 1).\n";
if (!$dryRun) {
$con->prepare("
INSERT INTO phone_verification (phone_number, driverId, email, is_verified, created_at)
VALUES (?, ?, ?, 1, NOW())
")->execute([$dPhoneKey, $driverId, $dEmailEnc]);
}
}
echo "\n✔ تم." . ($dryRun ? " (معاينة فقط — أعد التشغيل دون --dry-run للكتابة)" : "") . "\n";
echo "سلّم للمتجر: $passengerEmail و $driverEmail مع كلمتَي المرور المستخدمتين أعلاه.\n";