Update: 2026-07-30 12:31:26
This commit is contained in:
@@ -11,18 +11,16 @@ $password = filterRequest('password');
|
|||||||
$audience = filterRequest('aud') ?? 'siro-driver-android'; // الافتراضي
|
$audience = filterRequest('aud') ?? 'siro-driver-android'; // الافتراضي
|
||||||
$fingerprint = filterRequest('fingerPrint') ?? filterRequest('fingerprint');
|
$fingerprint = filterRequest('fingerPrint') ?? filterRequest('fingerprint');
|
||||||
|
|
||||||
// 1. تطبيق حد معدل الطلبات (Rate Limiting) للفاحصين: 3 محاولات بالدقيقة لكل IP
|
// 1. حد معدل الطلبات مطبّق على الجميع (الحد مرفوع إلى 30/دقيقة في RateLimiter)
|
||||||
$rateLimiter = new RateLimiter($redis);
|
$rateLimiter = new RateLimiter($redis);
|
||||||
$rateLimiter->enforce(RateLimiter::identifier(), 'tester_login');
|
$rateLimiter->enforce(RateLimiter::identifier(), 'tester_login');
|
||||||
|
|
||||||
if (!$email || !$password) {
|
// 2. قائمة بيضاء صريحة لحسابات الفحص — مطابقة تامة فقط، لا مطابقة جزئية ولا مطابقة نطاق
|
||||||
echo json_encode(["status" => "failure", "message" => "Email and password are required"]);
|
$allowedTesterEmailsEnv = getenv('ALLOWED_TESTER_EMAILS') ?: ($_ENV['ALLOWED_TESTER_EMAILS'] ?? '');
|
||||||
exit();
|
$allowedEmails = array_filter(array_map(
|
||||||
}
|
fn($e) => strtolower(trim($e)),
|
||||||
|
explode(',', $allowedTesterEmailsEnv)
|
||||||
// 2. التحقق من أن الحساب مخصص للفحص فقط (isTest check)
|
));
|
||||||
$allowedTesterEmailsEnv = getenv('ALLOWED_TESTER_EMAILS') ?: '';
|
|
||||||
$allowedEmails = array_filter(array_map('trim', explode(',', $allowedTesterEmailsEnv)));
|
|
||||||
if (empty($allowedEmails)) {
|
if (empty($allowedEmails)) {
|
||||||
$allowedEmails = [
|
$allowedEmails = [
|
||||||
'driver_tester@siromove.com',
|
'driver_tester@siromove.com',
|
||||||
@@ -30,24 +28,21 @@ if (empty($allowedEmails)) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$cleanEmail = strtolower(trim($email));
|
$cleanEmail = strtolower(trim((string) $email));
|
||||||
$isTester = in_array($cleanEmail, $allowedEmails) ||
|
$isTester = in_array($cleanEmail, $allowedEmails, true);
|
||||||
substr($cleanEmail, -13) === '@siromove.com' ||
|
|
||||||
str_contains($cleanEmail, 'tester') ||
|
|
||||||
str_contains($cleanEmail, 'reviewer');
|
|
||||||
|
|
||||||
// تشفير الإيميل لاستخدامه في الاستعلام
|
if (!$email || !$password) {
|
||||||
$encryptedEmail = $encryptionHelper->encryptData($email);
|
echo json_encode(["status" => "failure", "message" => "Email and password are required"]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$con = Database::get('main');
|
$con = Database::get('main');
|
||||||
|
|
||||||
// Auto-seed/create tester driver logic removed for security
|
|
||||||
|
|
||||||
|
$encryptedEmail = $encryptionHelper->encryptData($email);
|
||||||
global $blindIndex;
|
global $blindIndex;
|
||||||
$emailBidx = $blindIndex ? $blindIndex->index('driver.email', $email) : null;
|
$emailBidx = $blindIndex ? $blindIndex->index('driver.email', $email) : null;
|
||||||
|
|
||||||
// SQL لاسترجاع المستخدم بناءً على البريد الإلكتروني المشفر أو الفهرس الأعمى
|
|
||||||
$sql = "SELECT
|
$sql = "SELECT
|
||||||
driver.*,
|
driver.*,
|
||||||
phone_verification.is_verified,
|
phone_verification.is_verified,
|
||||||
@@ -67,36 +62,30 @@ try {
|
|||||||
$data = $stmt->fetch(PDO::FETCH_ASSOC);
|
$data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if ($data) {
|
if ($data) {
|
||||||
// التحقق من أن الحساب معلم كحساب فحص في قاعدة البيانات أو البيئة
|
|
||||||
$isTestInDb = (isset($data['is_test']) && $data['is_test'] == 1) || (isset($data['isTest']) && $data['isTest'] == 1);
|
$isTestInDb = (isset($data['is_test']) && $data['is_test'] == 1) || (isset($data['isTest']) && $data['isTest'] == 1);
|
||||||
if (!$isTestInDb && !$isTester) {
|
if (!$isTestInDb && !$isTester) {
|
||||||
jsonError("Access denied. Not a tester account.");
|
jsonError("Access denied. Not a tester account.");
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
// فحص الباسورد (في نظامنا، يمكن أن يكون الباسورد هو HMAC أو نص عادي للفاحصين)
|
|
||||||
// لنفترض أن الفاحص له باسورد عادي أو مشفر بـ bcrypt
|
if (password_verify($password, $data['password'] ?? '')) {
|
||||||
if (password_verify($password, $data['password'])) {
|
|
||||||
unset($data['password']);
|
unset($data['password']);
|
||||||
|
|
||||||
// فك تشفير الحقول الحساسة
|
if(isset($data['phone'])) $data['phone'] = $encryptionHelper->decryptData($data['phone']);
|
||||||
$data['phone'] = $encryptionHelper->decryptData($data['phone']);
|
if(isset($data['email'])) $data['email'] = $encryptionHelper->decryptData($data['email']);
|
||||||
$data['email'] = $encryptionHelper->decryptData($data['email']);
|
if(isset($data['gender'])) $data['gender'] = $encryptionHelper->decryptData($data['gender']);
|
||||||
$data['gender'] = $encryptionHelper->decryptData($data['gender']);
|
if(isset($data['birthdate'])) $data['birthdate'] = $encryptionHelper->decryptData($data['birthdate']);
|
||||||
$data['birthdate'] = $encryptionHelper->decryptData($data['birthdate']);
|
if(isset($data['site'])) $data['site'] = $encryptionHelper->decryptData($data['site']);
|
||||||
$data['site'] = $encryptionHelper->decryptData($data['site']);
|
if(isset($data['first_name'])) $data['first_name'] = $encryptionHelper->decryptData($data['first_name']);
|
||||||
$data['first_name'] = $encryptionHelper->decryptData($data['first_name']);
|
if(isset($data['last_name'])) $data['last_name'] = $encryptionHelper->decryptData($data['last_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']);
|
|
||||||
|
|
||||||
// توليد الـ JWT بصلاحية (tester) لتميزهم عن السائقين الفعليين
|
|
||||||
$jwtService = new JwtService($redis);
|
$jwtService = new JwtService($redis);
|
||||||
$jwt = $jwtService->generateAccessToken($data['id'], 'tester', $audience, $fingerprint);
|
$jwt = $jwtService->generateAccessToken($data['id'], 'tester', $audience, $fingerprint);
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
"status" => "success",
|
"status" => "success",
|
||||||
"jwt" => $jwt,
|
"jwt" => $jwt,
|
||||||
"data" => [$data] // مطابق لنسق التطبيق الذي يتوقع مصفوفة
|
"data" => [$data]
|
||||||
], JSON_UNESCAPED_UNICODE);
|
], JSON_UNESCAPED_UNICODE);
|
||||||
} else {
|
} else {
|
||||||
jsonError("Incorrect password.");
|
jsonError("Incorrect password.");
|
||||||
@@ -104,8 +93,8 @@ try {
|
|||||||
} else {
|
} else {
|
||||||
jsonError("User does not exist.");
|
jsonError("User does not exist.");
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Throwable $e) {
|
||||||
error_log("[Tester Login Error] " . $e->getMessage());
|
error_log("[Tester Login Error] " . $e->getMessage() . " in " . $e->getFile() . ":" . $e->getLine());
|
||||||
jsonError("Server error occurred.");
|
jsonError("Server error occurred.");
|
||||||
} finally {
|
} finally {
|
||||||
$stmt = null;
|
$stmt = null;
|
||||||
|
|||||||
@@ -9,18 +9,16 @@ $password = filterRequest("password");
|
|||||||
$fingerprint = filterRequest('fingerPrint') ?? filterRequest('fingerprint');
|
$fingerprint = filterRequest('fingerPrint') ?? filterRequest('fingerprint');
|
||||||
$audience = filterRequest('aud') ?: 'siro_passenger';
|
$audience = filterRequest('aud') ?: 'siro_passenger';
|
||||||
|
|
||||||
// 1. تطبيق حد معدل الطلبات (Rate Limiting) للفاحصين: 3 محاولات بالدقيقة لكل IP
|
// 1. حد معدل الطلبات مطبّق على الجميع (الحد مرفوع إلى 30/دقيقة في RateLimiter)
|
||||||
$rateLimiter = new RateLimiter($redis);
|
$rateLimiter = new RateLimiter($redis);
|
||||||
$rateLimiter->enforce(RateLimiter::identifier(), 'tester_login');
|
$rateLimiter->enforce(RateLimiter::identifier(), 'tester_login');
|
||||||
|
|
||||||
if (!$email || !$password) {
|
// 2. قائمة بيضاء صريحة لحسابات الفحص — مطابقة تامة فقط، لا مطابقة جزئية ولا مطابقة نطاق
|
||||||
echo json_encode(["status" => "failure", "message" => "Email and password are required"]);
|
$allowedTesterEmailsEnv = getenv('ALLOWED_TESTER_EMAILS') ?: ($_ENV['ALLOWED_TESTER_EMAILS'] ?? '');
|
||||||
exit();
|
$allowedEmails = array_filter(array_map(
|
||||||
}
|
fn($e) => strtolower(trim($e)),
|
||||||
|
explode(',', $allowedTesterEmailsEnv)
|
||||||
// 2. التحقق من أن الحساب مخصص للفحص فقط (isTest check)
|
));
|
||||||
$allowedTesterEmailsEnv = getenv('ALLOWED_TESTER_EMAILS') ?: '';
|
|
||||||
$allowedEmails = array_filter(array_map('trim', explode(',', $allowedTesterEmailsEnv)));
|
|
||||||
if (empty($allowedEmails)) {
|
if (empty($allowedEmails)) {
|
||||||
$allowedEmails = [
|
$allowedEmails = [
|
||||||
'driver_tester@siromove.com',
|
'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));
|
if (!$email || !$password) {
|
||||||
$isTester = in_array($cleanEmail, $allowedEmails) ||
|
echo json_encode(["status" => "failure", "message" => "Email and password are required"]);
|
||||||
substr($cleanEmail, -13) === '@siromove.com' ||
|
exit();
|
||||||
str_contains($cleanEmail, 'tester') ||
|
}
|
||||||
str_contains($cleanEmail, 'reviewer');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$con = Database::get('main');
|
$con = Database::get('main');
|
||||||
@@ -121,7 +120,7 @@ try {
|
|||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
"status" => "failure",
|
"status" => "failure",
|
||||||
"message" => "Server error: " . $e->getMessage() . " in " . basename($e->getFile()) . " on line " . $e->getLine()
|
"message" => "Server error. Please try again."
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
exit();
|
exit();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class RateLimiter
|
|||||||
// حدود مختلفة لكل نوع endpoint
|
// حدود مختلفة لكل نوع endpoint
|
||||||
private const LIMITS = [
|
private const LIMITS = [
|
||||||
'login' => ['requests' => 5, 'window' => 60], // 5 محاولات / دقيقة
|
'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 دقائق
|
'otp' => ['requests' => 3, 'window' => 300], // 3 محاولات / 5 دقائق
|
||||||
'register' => ['requests' => 3, 'window' => 3600], // 3 محاولات / ساعة
|
'register' => ['requests' => 3, 'window' => 3600], // 3 محاولات / ساعة
|
||||||
'api' => ['requests' => 180, 'window' => 60], // 180 طلب / دقيقة (الإنتاج الرسمى)
|
'api' => ['requests' => 180, 'window' => 60], // 180 طلب / دقيقة (الإنتاج الرسمى)
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -27,11 +27,16 @@ class FinancialV2Page extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(color: cs.outline),
|
border: Border.all(color: cs.outline),
|
||||||
),
|
),
|
||||||
child: Icon(Icons.arrow_back_ios_new_rounded, color: cs.onSurfaceVariant, size: 16),
|
child: Icon(Icons.arrow_back_ios_new_rounded,
|
||||||
|
color: cs.onSurfaceVariant, size: 16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text('الإدارة المالية المتقدمة', style: TextStyle(color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.w700)),
|
Text('الإدارة المالية المتقدمة',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w700)),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.refresh_rounded, color: cs.onSurfaceVariant),
|
icon: Icon(Icons.refresh_rounded, color: cs.onSurfaceVariant),
|
||||||
@@ -157,8 +162,7 @@ class FinancialV2Page extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(title,
|
Text(title,
|
||||||
style: TextStyle(
|
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12)),
|
||||||
color: cs.onSurfaceVariant, fontSize: 12)),
|
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(value,
|
Text(value,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -173,7 +177,8 @@ class FinancialV2Page extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPaymentMethodBreakdown(Map<String, dynamic> stats, ColorScheme cs) {
|
Widget _buildPaymentMethodBreakdown(
|
||||||
|
Map<String, dynamic> stats, ColorScheme cs) {
|
||||||
double cash = double.tryParse(stats['cash_payments'].toString()) ?? 0;
|
double cash = double.tryParse(stats['cash_payments'].toString()) ?? 0;
|
||||||
double digital = double.tryParse(stats['digital_payments'].toString()) ?? 0;
|
double digital = double.tryParse(stats['digital_payments'].toString()) ?? 0;
|
||||||
double total = cash + digital;
|
double total = cash + digital;
|
||||||
@@ -204,8 +209,7 @@ class FinancialV2Page extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(label,
|
Text(label, style: TextStyle(color: cs.onSurface, fontSize: 13)),
|
||||||
style: TextStyle(color: cs.onSurface, fontSize: 13)),
|
|
||||||
Text('${value.toStringAsFixed(0)} ج.م',
|
Text('${value.toStringAsFixed(0)} ج.م',
|
||||||
style: TextStyle(color: color, fontWeight: FontWeight.bold)),
|
style: TextStyle(color: color, fontWeight: FontWeight.bold)),
|
||||||
],
|
],
|
||||||
@@ -253,15 +257,13 @@ class FinancialV2Page extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text('${s['first_name']} ${s['last_name']}',
|
Text('${s['first_name']} ${s['last_name']}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: cs.onSurface,
|
color: cs.onSurface, fontWeight: FontWeight.bold)),
|
||||||
fontWeight: FontWeight.bold)),
|
|
||||||
Text(s['phone'] ?? '',
|
Text(s['phone'] ?? '',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: cs.onSurfaceVariant, fontSize: 12)),
|
color: cs.onSurfaceVariant, fontSize: 12)),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text('${s['total_rides']} رحلة مكتملة',
|
Text('${s['total_rides']} رحلة مكتملة',
|
||||||
style: TextStyle(
|
style: TextStyle(color: cs.tertiary, fontSize: 11)),
|
||||||
color: cs.tertiary, fontSize: 11)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -269,8 +271,8 @@ class FinancialV2Page extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text('المستحقات',
|
Text('المستحقات',
|
||||||
style: TextStyle(
|
style:
|
||||||
color: cs.onSurfaceVariant, fontSize: 10)),
|
TextStyle(color: cs.onSurfaceVariant, fontSize: 10)),
|
||||||
Text('${s['total_earned']} ج.م',
|
Text('${s['total_earned']} ج.م',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: cs.primary,
|
color: cs.primary,
|
||||||
|
|||||||
@@ -680,10 +680,15 @@ class LoginDriverController extends GetxController {
|
|||||||
|
|
||||||
Get.off(() => HomeCaptain());
|
Get.off(() => HomeCaptain());
|
||||||
} else {
|
} else {
|
||||||
mySnackbarError('Login failed'.tr);
|
mySnackbarError(
|
||||||
|
(jsonDecoeded['message'] ?? 'Login failed').toString().tr);
|
||||||
isloading = false;
|
isloading = false;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
} else if (response.statusCode == 429) {
|
||||||
|
mySnackbarError('Too many attempts. Please wait a minute.'.tr);
|
||||||
|
isloading = false;
|
||||||
|
update();
|
||||||
} else {
|
} else {
|
||||||
mySnackbarError('Server error'.tr);
|
mySnackbarError('Server error'.tr);
|
||||||
isloading = false;
|
isloading = false;
|
||||||
|
|||||||
@@ -119,7 +119,8 @@ class RatePassenger extends StatelessWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
CurrencyHelper.currency, // Replace with your local currency symbol if needed
|
CurrencyHelper
|
||||||
|
.currency, // Replace with your local currency symbol if needed
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withOpacity(0.8),
|
color: Colors.white.withOpacity(0.8),
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
|
|||||||
@@ -17,12 +17,15 @@ class PaymentHistoryDriverPage extends StatelessWidget {
|
|||||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Payment History'.tr,
|
title: Text('Payment History'.tr,
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)),
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: FinanceDesignSystem.primaryDark)),
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: Icon(Icons.arrow_back_ios_new_rounded, color: FinanceDesignSystem.primaryDark, size: 20),
|
icon: Icon(Icons.arrow_back_ios_new_rounded,
|
||||||
|
color: FinanceDesignSystem.primaryDark, size: 20),
|
||||||
onPressed: () => Get.back(),
|
onPressed: () => Get.back(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -37,10 +40,13 @@ class PaymentHistoryDriverPage extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.history_rounded, size: 80, color: Colors.grey.shade300),
|
Icon(Icons.history_rounded,
|
||||||
|
size: 80, color: Colors.grey.shade300),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text('No transactions yet'.tr,
|
Text('No transactions yet'.tr,
|
||||||
style: TextStyle(color: Colors.grey.shade400, fontWeight: FontWeight.bold)),
|
style: TextStyle(
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -52,8 +58,9 @@ class PaymentHistoryDriverPage extends StatelessWidget {
|
|||||||
itemCount: controller.archive.length,
|
itemCount: controller.archive.length,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
final tx = controller.archive[index];
|
final tx = controller.archive[index];
|
||||||
final double amount = double.tryParse(tx['amount']?.toString() ?? '0') ?? 0;
|
final double amount =
|
||||||
|
double.tryParse(tx['amount']?.toString() ?? '0') ?? 0;
|
||||||
|
|
||||||
return AnimationConfiguration.staggeredList(
|
return AnimationConfiguration.staggeredList(
|
||||||
position: index,
|
position: index,
|
||||||
duration: const Duration(milliseconds: 375),
|
duration: const Duration(milliseconds: 375),
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import 'package:http/http.dart' as http;
|
|||||||
|
|
||||||
import 'package:siro_rider/constant/info.dart';
|
import 'package:siro_rider/constant/info.dart';
|
||||||
import 'package:siro_rider/controller/functions/add_error.dart';
|
import 'package:siro_rider/controller/functions/add_error.dart';
|
||||||
import 'package:siro_rider/views/auth/login_page.dart';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -482,7 +481,7 @@ class LoginController extends GetxController {
|
|||||||
'aud': '${AK.allowed}${Platform.isAndroid ? 'android' : 'ios'}',
|
'aud': '${AK.allowed}${Platform.isAndroid ? 'android' : 'ios'}',
|
||||||
};
|
};
|
||||||
Log.print(
|
Log.print(
|
||||||
"Tester Login Payload: email=${payload['email']}, password=${payload['password']}, aud=${payload['aud']}");
|
"Tester Login Payload: email=${payload['email']}, aud=${payload['aud']}");
|
||||||
|
|
||||||
var response = await http.post(
|
var response = await http.post(
|
||||||
Uri.parse(AppLink.loginUsingCredentialsWithoutGooglePassenger),
|
Uri.parse(AppLink.loginUsingCredentialsWithoutGooglePassenger),
|
||||||
@@ -525,19 +524,27 @@ class LoginController extends GetxController {
|
|||||||
Get.offAll(() => const MapPagePassenger());
|
Get.offAll(() => const MapPagePassenger());
|
||||||
} else {
|
} else {
|
||||||
Log.print(
|
Log.print(
|
||||||
"Tester Login Failed due to condition mismatch: status=${jsonDecoeded['status']}, verified=${jsonDecoeded['data']?[0]?['verified']}");
|
"Tester Login Condition mismatch: status=${jsonDecoeded['status']}, verified=${jsonDecoeded['data']?[0]?['verified']}");
|
||||||
Get.offAll(() => LoginPage());
|
mySnackbarError(
|
||||||
|
(jsonDecoeded['message'] ?? 'Login failed').toString().tr);
|
||||||
isloading = false;
|
isloading = false;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
} else if (response.statusCode == 429) {
|
||||||
|
Log.print("Tester Login rate limited: ${response.body}");
|
||||||
|
mySnackbarError('Too many attempts. Please wait a minute.'.tr);
|
||||||
|
isloading = false;
|
||||||
|
update();
|
||||||
} else {
|
} else {
|
||||||
Log.print(
|
Log.print(
|
||||||
"Tester Login Failed with status code: ${response.statusCode}, body: ${response.body}");
|
"Tester Login HTTP error: ${response.statusCode}, body: ${response.body}");
|
||||||
|
mySnackbarError('Server error'.tr);
|
||||||
isloading = false;
|
isloading = false;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.print("Tester Login Error: $e");
|
Log.print("Tester Login Exception: $e");
|
||||||
|
mySnackbarError('Network error'.tr);
|
||||||
isloading = false;
|
isloading = false;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,8 +56,7 @@ class PointsPageForRider extends StatelessWidget {
|
|||||||
wayPointController.wayPoints.length > 1
|
wayPointController.wayPoints.length > 1
|
||||||
? ElevatedButton(
|
? ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
locationSearch
|
locationSearch.getMapPointsForAllMethods();
|
||||||
.getMapPointsForAllMethods();
|
|
||||||
},
|
},
|
||||||
child: const Text('Get Direction'),
|
child: const Text('Get Direction'),
|
||||||
)
|
)
|
||||||
@@ -108,8 +107,8 @@ class PointsPageForRider extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(),
|
border: Border.all(),
|
||||||
color:
|
color: AppColor.accentColor
|
||||||
AppColor.accentColor.withValues(alpha: 0.5)),
|
.withValues(alpha: 0.5)),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment:
|
||||||
MainAxisAlignment.spaceBetween,
|
MainAxisAlignment.spaceBetween,
|
||||||
|
|||||||
Reference in New Issue
Block a user