Update: 2026-06-16 01:17:28

This commit is contained in:
Hamza-Ayed
2026-06-16 01:17:29 +03:00
parent 04943e3d52
commit fc58529b09
56 changed files with 1149 additions and 1314 deletions

View File

@@ -7,20 +7,27 @@ require_once __DIR__ . '/../../core/bootstrap.php';
$con = Database::get('main');
// التحقق من الصلاحيات: فقط المشرفين (super_admin أو admin) يمكنهم الإضافة
// التحقق من الصلاحيات: فقط المشرف العام يمكنه إضافة مشرفين جدد
$jwtService = new JwtService($redis);
$auth = $jwtService->authenticate();
$authRole = $auth->role ?? '';
if ($authRole !== 'super_admin' && $authRole !== 'admin') {
jsonError("غير مصرح لك. فقط المشرفون يمكنهم إضافة موظفين.");
exit;
}
$name = filterRequest("name");
$phone = filterRequest("phone");
$email = filterRequest("email");
$password = filterRequest("password");
$role = filterRequest("role"); // 'admin' or 'service'
// ✅ FIX H-01: تقييد إضافة المشرفين لـ super_admin فقط
if ($role === 'admin' && $authRole !== 'super_admin') {
jsonError("غير مصرح لك. فقط المشرف العام يمكنه إضافة مشرفين جدد.");
exit;
}
if ($authRole !== 'super_admin' && $authRole !== 'admin') {
jsonError("غير مصرح لك. فقط المشرفون يمكنهم إضافة موظفين.");
exit;
}
$fingerprint = filterRequest("fingerprint") ?: '';
$gender = filterRequest("gender") ?? 'Male';
$birthdate = filterRequest("birthdate") ?? date('Y-m-d');
@@ -45,12 +52,9 @@ try {
$uniqueId = bin2hex(random_bytes(16));
if ($role === 'admin') {
// الإضافة لجدول المديرين
// التأكد من وجود عمود phone في الجدول (كإجراء احترازي لتجنب الأخطاء إذا لم يكن موجوداً)
try {
$con->exec("ALTER TABLE adminUser ADD COLUMN phone VARCHAR(255) NULL AFTER name");
} catch (Exception $e) { /* العمود موجود مسبقاً */ }
// ✅ FIX R4: إزالة ALTER TABLE من كود الإنتاج — يجب تشغيل migration منفصل
// قبل استخدام هذا الكود، تأكد من تشغيل:
// ALTER TABLE adminUser ADD COLUMN IF NOT EXISTS phone VARCHAR(255) NULL AFTER name;
$sql = "INSERT INTO adminUser (id, fingerprint, fingerprint_hash, name, phone, password, role, created_at)
VALUES (:id, :fp, :fp_hash, :name, :phone, :pass, :role, NOW())";
$stmt = $con->prepare($sql);

View File

@@ -2,36 +2,45 @@
/**
* Admin/Staff/setup.php
* سكربت إعداد المسؤول الأول (Super Admin)
* يستخدم لمرة واحدة فقط عندما تكون الجداول فارغة
* ⚠️ للاستخدام لمرة واحدة فقط. يحمي نفسه بـ MIGRATION_ADMIN_KEY.
* بعد أول تشغيل ناجح، امسح الملف من السيرفر.
*/
require_once __DIR__ . '/../../core/bootstrap.php';
// ── حماية بمفتاح الترحيل ────────────────────────────────
$adminKey = filterRequest('admin_key') ?? '';
$expectedAdminKey = getenv('MIGRATION_ADMIN_KEY');
if (empty($adminKey) || empty($expectedAdminKey) || !hash_equals($expectedAdminKey, $adminKey)) {
http_response_code(403);
exit(json_encode(['error' => 'Access denied. Admin key required.']));
}
$con = Database::get('main');
// تم تعطيل التحقق للسماح بإعادة التهيئة
// $count = $con->query("SELECT COUNT(*) FROM adminUser")->fetchColumn();
// if ($count > 0) {
// die("Access Denied: Admin already initialized.");
// }
// ── منع إعادة التهيئة إذا كان هناك مشرفون مسبقاً ─────────
$count = $con->query("SELECT COUNT(*) FROM adminUser")->fetchColumn();
if ($count > 0) {
http_response_code(403);
exit(json_encode(['error' => 'Admin already initialized. This script runs only once.']));
}
$password = "malDev@2101"; // كلمة المرور المؤقتة
// ── كلمة المرور من البيئة أو تُنشأ عشوائياً ──────────────
$password = getenv('SETUP_SUPER_ADMIN_PASSWORD');
if (!$password) {
$password = bin2hex(random_bytes(12));
}
$hashedPass = password_hash($password, PASSWORD_DEFAULT);
// قائمة بالمسؤولين الأوائل (بصمات أجهزتك)
// ── بصمات افتراضية (تُستبدل عند أول تسجيل دخول فعلي) ───
$admins = [
[
'name' => 'Hamza (iPhone)',
'fp' => 'D386663E-51E1-4322-B1E2-F469C7E58063_iPhone', // مثال بناءً على وصفك (deviceId_model)
'role' => 'admin'
],
[
'name' => 'Hamza (MacBook)',
'fp' => '5449E3D3-E427-50D7-91A6-D86D973DC6E0_Mac15,3', // مثال للماك بوك
'role' => 'admin'
'name' => 'Super Admin',
'fp' => 'SETUP_DEFAULT_FP_001',
'role' => 'super_admin'
]
];
try {
$con->exec("DELETE FROM adminUser");
foreach ($admins as $admin) {
$encName = $encryptionHelper->encryptData($admin['name']);
$encFp = $encryptionHelper->encryptData($admin['fp']);

View File

@@ -1,74 +0,0 @@
<?php
/**
* Admin/auth/debug_login.php
* ملف تشخيصي مؤقت — يُحذف بعد التحقق
*/
header('Content-Type: application/json; charset=UTF-8');
error_reporting(E_ALL);
ini_set('display_errors', '1');
$checks = [];
// 1. التحقق من ملف bootstrap
$bootstrapPath = __DIR__ . '/../../core/bootstrap.php';
$checks['bootstrap_exists'] = file_exists($bootstrapPath);
// 2. محاولة تحميل bootstrap مع التقاط الأخطاء
try {
ob_start();
require_once $bootstrapPath;
$bootstrapOutput = ob_get_clean();
$checks['bootstrap_loaded'] = true;
$checks['bootstrap_output'] = $bootstrapOutput ?: '(clean)';
} catch (Throwable $e) {
ob_end_clean();
$checks['bootstrap_loaded'] = false;
$checks['bootstrap_error'] = $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine();
}
// 3. التحقق من الدوال المطلوبة
$checks['filterRequest_exists'] = function_exists('filterRequest');
$checks['jsonError_exists'] = function_exists('jsonError');
$checks['sendWhatsAppFromServer_exists'] = function_exists('sendWhatsAppFromServer');
// 4. التحقق من قاعدة البيانات
try {
$con = Database::get('main');
$checks['db_connected'] = true;
// التحقق من بنية الجدول
$stmt = $con->query("DESCRIBE adminUser");
$columns = $stmt->fetchAll(PDO::FETCH_COLUMN);
$checks['adminUser_columns'] = $columns;
// هل يوجد جدول token_verification_admin؟
$stmt2 = $con->query("SHOW TABLES LIKE 'token_verification_admin'");
$checks['token_verification_admin_exists'] = $stmt2->rowCount() > 0;
if (!$checks['token_verification_admin_exists']) {
$checks['CRITICAL'] = 'Table token_verification_admin does NOT exist! This is why login fails.';
}
} catch (Throwable $e) {
$checks['db_connected'] = false;
$checks['db_error'] = $e->getMessage();
}
// 5. التحقق من PHP error log الأخير
$logPath = '/home/siro-api/logs/php_errors.log';
if (file_exists($logPath)) {
$lines = file($logPath);
$checks['last_5_errors'] = array_map('trim', array_slice($lines, -5));
} else {
$logPath2 = __DIR__ . '/../../logs/php_errors.log';
if (file_exists($logPath2)) {
$lines = file($logPath2);
$checks['last_5_errors'] = array_map('trim', array_slice($lines, -5));
} else {
$checks['error_log'] = 'Log file not found';
}
}
// 6. نسخة PHP
$checks['php_version'] = PHP_VERSION;
echo json_encode($checks, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

View File

@@ -17,9 +17,33 @@ if (empty($fingerprint) || empty($password)) {
exit;
}
// Rate Limiting: 5 محاولات في الدقيقة لكل IP
// Rate Limiting محسَّن مع Exponential Backoff
$rateLimiter = new RateLimiter($redis);
$rateLimiter->enforce(RateLimiter::identifier(), 'login');
$rateLimiter->enforce(RateLimiter::identifier(), 'login', maxAttempts: 5, windowSeconds: 60);
// تتبع المحاولات الفاشلة لكل حساب لمنع credential stuffing عبر IPs متعددة
if ($redis && !empty($phone)) {
$accountKey = "login_attempts:account:" . hash('sha256', $phone);
$accountAttempts = (int) $redis->get($accountKey);
if ($accountAttempts >= 5) {
$ttl = $redis->ttl($accountKey);
$waitMinutes = ceil($ttl / 60);
jsonError("تم تعليق تسجيل الدخول لهذا الحساب مؤقتاً. يرجى المحاولة بعد {$waitMinutes} دقيقة.");
exit;
}
}
// تسجيل محاولة تسجيل الدخول للتدقيق
$loginAuditData = [
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'fingerprint_hash' => $fpHash ?? null,
'phone_hash' => !empty($phone) ? hash('sha256', $phone) : null,
'timestamp' => date('Y-m-d H:i:s'),
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
'result' => 'pending'
];
error_log("[LOGIN_AUDIT] " . json_encode($loginAuditData));
try {
$con = Database::get('main');
@@ -96,8 +120,8 @@ try {
exit;
}
// 3. توليد رمز تحقق OTP (6 أرقام) وإرساله عبر WhatsApp
$otp = rand(100000, 999999);
// 3. توليد رمز تحقق OTP (3 أرقام) وإرساله عبر WhatsApp
$otp = random_int(100, 999);
$encryptedPhone = $admin['phone'] ?? '';
if (empty($encryptedPhone)) {
@@ -116,13 +140,11 @@ try {
$success = sendWhatsAppFromServer($phone, $messageBody);
if ($success) {
// حفظ الرمز مشفراً في قاعدة البيانات (وحفظ رقم الهاتف مشفراً أيضاً)
$encryptedOtp = $encryptionHelper->encryptData((string)$otp);
// حفظ الرمز كما هو في قاعدة البيانات (بدون تشفير)
$stmt = $con->prepare("INSERT INTO token_verification_admin (phone_number, token, expiration_time)
VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 10 MINUTE))
ON DUPLICATE KEY UPDATE token = VALUES(token), expiration_time = VALUES(expiration_time)");
$stmt->execute([$encryptedPhone, $encryptedOtp]);
$stmt->execute([$encryptedPhone, $otp]);
// إخفاء جزء من الرقم في الاستجابة للأمان
$maskedPhone = substr($phone, 0, 4) . '****' . substr($phone, -3);
@@ -143,5 +165,6 @@ try {
}
} catch (Exception $e) {
error_log("[Admin Login Error] " . $e->getMessage());
jsonError("خطأ في السيرفر: " . $e->getMessage());
// لا تسرب رسالة الخطأ الداخلية في الإنتاج
jsonError("حدث خطأ في السيرفر. يرجى المحاولة لاحقاً.");
}

View File

@@ -0,0 +1,10 @@
# 🔒 SECURITY: Block all access to debug files
# This directory contains sensitive debugging scripts
# DO NOT remove this file in production
<RequireAll>
Require all denied
</RequireAll>
# Alternative for older Apache:
# Deny from all

View File

@@ -6,7 +6,7 @@
require_once __DIR__ . '/../core/bootstrap.php';
header('Content-Type: application/json');
header("Access-Control-Allow-Origin: https://intaleqapp.com");
header("Access-Control-Allow-Origin: https://siromove.com");
header("Access-Control-Allow-Methods: POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");

View File

@@ -6,7 +6,7 @@
require_once __DIR__ . '/../../connect.php'; // تأكد أن هذا الملف يحتوي على $con_tracking
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Origin: https://siromove.com");
header("Content-Type: application/json; charset=UTF-8");
try {

View File

@@ -1,7 +1,7 @@
<?php
require_once __DIR__ . '/../../connect.php';
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Origin: https://siromove.com");
header('Content-Type: application/json; charset=utf-8');
try {

View File

@@ -219,8 +219,32 @@ Therefore, do NOT assume a specific field is on the front or the back of a card.
["role" => "user", "parts" => [["text" => $promptBase]]]
];
// ✅ SSRF Protection: Allowlist for document URLs
$allowedHosts = array_filter([
parse_url($PUBLIC_BASE, PHP_URL_HOST),
getenv('ALLOWED_UPLOAD_HOST'),
]);
$maxFileSize = 10 * 1024 * 1024; // 10MB max per image
foreach ($docUrls as $key => $url) {
$imgData = @file_get_contents($url);
$urlHost = parse_url($url, PHP_URL_HOST);
$allowed = false;
foreach ($allowedHosts as $host) {
if ($host && str_ends_with($urlHost, $host)) {
$allowed = true;
break;
}
}
if (!$allowed) {
error_log("[SSRF_BLOCKED] Doc URL not in allowlist: $urlHost ($key)");
continue;
}
$ctx = stream_context_create(['http' => [
'timeout' => 10,
'ignore_errors' => true,
]]);
$imgData = @file_get_contents($url, false, $ctx, 0, $maxFileSize);
if ($imgData !== false) {
$base64 = base64_encode($imgData);
$ext = strtolower(pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION));
@@ -528,8 +552,8 @@ $pwdHashed = password_hash($rawSecret, PASSWORD_DEFAULT);
curl_setopt($ch, CURLOPT_POSTFIELDS, $notificationPayload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_exec($ch);
curl_close($ch);

View File

@@ -108,8 +108,13 @@ if (!is_dir($destDir)) { @mkdir($destDir, 0700, true); }
$serverName = "{$driverIdSafe}__{$docType}{$ext}";
$destPath = $destDir . '/' . $serverName;
// استبدال أي نسخة قديمة عن قصد (overwrite)
if (is_file($destPath)) { @unlink($destPath); }
// استبدال أي نسخة قديمة عن قصد (overwrite) - مع حماية ضد path traversal
$resolvedDest = realpath($destPath) ?: $destPath;
$resolvedRoot = realpath(UPLOAD_ROOT) ?: UPLOAD_ROOT;
if (is_file($destPath) && str_starts_with($resolvedDest, $resolvedRoot)) {
@unlink($destPath);
}
// نقل الملف
if (!move_uploaded_file($tmpPath, $destPath)) {

View File

@@ -1,5 +1,6 @@
{
"require": {
"vlucas/phpdotenv": "^5.6"
"vlucas/phpdotenv": "^5.6",
"firebase/php-jwt": "^6.0"
}
}

View File

@@ -8,20 +8,15 @@ require_once __DIR__ . '/core/bootstrap.php';
require_once __DIR__ . '/functions.php';
// 1. Rate Limiting and JWT Authentication
if (!defined('TESTING_BYPASS_AUTH')) {
$limiter = new RateLimiter($redis);
$limiter->enforce(RateLimiter::identifier(), 'api');
$limiter = new RateLimiter($redis);
$limiter->enforce(RateLimiter::identifier(), 'api');
$jwtService = new JwtService($redis);
$decoded = $jwtService->authenticate();
$jwtService = new JwtService($redis);
$decoded = $jwtService->authenticate();
// متغيرات مساعدة للمطور
$user_id = $decoded->user_id ?? null;
$role = $decoded->role ?? 'passenger';
} else {
$user_id = $_POST['driver_id'] ?? '2085';
$role = 'driver';
}
// متغيرات مساعدة للمطور
$user_id = $decoded->user_id ?? null;
$role = $decoded->role ?? 'passenger';
// 3. Database Connection
try {

View File

@@ -36,18 +36,18 @@ class JwtService
public function __construct(?Redis $redis = null)
{
$this->secretKey = trim(file_get_contents('/home/siro-api/.secret_key'));
// ✅ FIX C-02: استخدام getenv بدلاً من file_get_contents الثابت
$keyPath = getenv('JWT_SECRET_KEY_PATH');
if ($keyPath && file_exists($keyPath)) {
$this->secretKey = trim(file_get_contents($keyPath));
} else {
$this->secretKey = getenv('JWT_SECRET_KEY') ?: '';
}
$this->hmacSecret = getenv('SECRET_KEY_HMAC') ?: '';
$this->fpPepper = getenv('FP_PEPPER') ?: '';
$this->issuer = (string)(getenv('APP_ISSUER') ?: '');
$this->redis = $redis;
// Debugging fpPepper
if (empty($this->fpPepper)) {
error_log("[JWT_DEBUG] fpPepper is EMPTY in constructor");
} else {
error_log("[JWT_DEBUG] fpPepper is SET (length: " . strlen($this->fpPepper) . ")");
}
}
@@ -144,19 +144,8 @@ class JwtService
} catch (ExpiredException $e) {
self::abort(401, 'Token expired');
} catch (SignatureInvalidException $e) {
// محاولة فك التشفير بمفتاح المحفظة (Wallet secret fallback)
$payKeyPath = '/home/siro-api/.secret_key_pay';
$payKey = file_exists($payKeyPath) ? trim(file_get_contents($payKeyPath)) : '';
if ($payKey) {
try {
$decoded = JWT::decode($token, new Key($payKey, self::ALGO));
} catch (Exception $e2) {
self::abort(401, 'Invalid token signature');
}
} else {
self::abort(401, 'Invalid token signature');
}
// ممنوع استخدام أي مفتاح آخر - مفتاح JWT واحد فقط
self::abort(401, 'Invalid token signature');
} catch (BeforeValidException $e) {
self::abort(401, 'Token not yet valid');
} catch (Exception $e) {
@@ -262,12 +251,11 @@ class JwtService
$expectedHmac = hash_hmac('sha256', $payloadToSign, $userSecret);
if (!hash_equals($expectedHmac, $hmacHeader)) {
$debugMsg = "User: $userId | Expected: $expectedHmac | Got: $hmacHeader | DerivedSecret: $userSecret | MasterSecret(4): " . substr($this->hmacSecret, 0, 4) . " | Body($bodyLen): '$body' | TS: '$timestamp' | Nonce: '$nonce'";
$bodyLen = strlen($body);
error_log("[SECURITY] HMAC mismatch | " . $debugMsg);
// TEMPORARY: expose debug in response for diagnosis
error_log("[SECURITY] HMAC mismatch | User: $userId | BodyLen: $bodyLen | TS: '$timestamp'");
// ✅ FIX H-02: إزالة معلومات الـ Debug من الاستجابة
http_response_code(403);
echo json_encode(['error' => 'HMAC_DEBUG', 'debug' => $debugMsg]);
echo json_encode(['error' => 'Request verification failed']);
exit;
}
}
@@ -288,7 +276,13 @@ class JwtService
{
$keyPath = getenv('INTERNAL_SOCKET_KEY_PATH');
$sent = $_SERVER['HTTP_X_INTERNAL_KEY'] ?? '';
$expected = (file_exists($keyPath) ? trim(file_get_contents($keyPath)) : '') ?: 'Siro_Secure_Bridge_Key_2026_@!socket';
$expected = '';
if ($keyPath && file_exists($keyPath)) {
$expected = trim(file_get_contents($keyPath));
}
if (!$expected) {
$expected = getenv('INTERNAL_SOCKET_KEY');
}
if (!$expected || !hash_equals($expected, $sent)) {
error_log('[SECURITY] Invalid internal key from: ' . ($_SERVER['REMOTE_ADDR'] ?? '?'));

View File

@@ -8,7 +8,7 @@ declare(strict_types=1);
// 1. إعدادات الأخطاء والـ Headers الأساسية
// اجعل القيمة true لتفعيل عرض الأخطاء (التطوير)، أو false لإخفائها (التشغيل الفعلي)
$debugMode = true;
$debugMode = getenv('APP_DEBUG') === 'true';
if ($debugMode) {
error_reporting(E_ALL);
@@ -26,10 +26,16 @@ if (!file_exists(dirname($logPath)) || !is_writable(dirname($logPath))) {
}
ini_set('error_log', $logPath);
header_remove('X-Powered-By');
header('Content-Type: application/json; charset=UTF-8');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
header("Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'none'");
header("Referrer-Policy: strict-origin-when-cross-origin");
header("Permissions-Policy: geolocation=(), microphone=(), camera=()");
header("X-XSS-Protection: 1; mode=block");
// CORS (يجب تخصيصه في endpoints مخصصة إن لزم، لكن هذا افتراضي)
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');

View File

@@ -76,7 +76,15 @@ function result(int $count): void
}
function sendEmail(string $from, string $to, string $title, string $body): void
{
$header = "From: $from\nCC: $from";
$from = str_replace(["\r", "\n", "\r\n"], '', $from);
$to = str_replace(["\r", "\n", "\r\n"], '', $to);
$title = str_replace(["\r", "\n", "\r\n"], '', $title);
$header = "From: $from\r\n";
$header .= "Reply-To: $from\r\n";
$header .= "MIME-Version: 1.0\r\n";
$header .= "Content-Type: text/html; charset=UTF-8\r\n";
mail($to, $title, $body, $header);
}
@@ -155,7 +163,7 @@ function securityLog(string $message, array $context = []): void
{
$logDir = __DIR__ . '/../logs';
if (!is_dir($logDir)) {
@mkdir($logDir, 0777, true);
@mkdir($logDir, 0750, true);
}
$entry = date('Y-m-d H:i:s') . ' [SECURITY] ' . $message;
if ($context) $entry .= ' | ' . json_encode($context, JSON_UNESCAPED_UNICODE);
@@ -166,7 +174,7 @@ function appLog(string $message, string $level = 'INFO'): void
{
$logDir = __DIR__ . '/../logs';
if (!is_dir($logDir)) {
@mkdir($logDir, 0777, true);
@mkdir($logDir, 0750, true);
}
$entry = date('Y-m-d H:i:s') . " [$level] " . $message;
@error_log($entry . PHP_EOL, 3, $logDir . '/app.log');
@@ -176,7 +184,7 @@ function uploadLog(string $message, string $level = 'INFO', array $context = [])
{
$logDir = __DIR__ . '/../logs';
if (!is_dir($logDir)) {
@mkdir($logDir, 0777, true);
@mkdir($logDir, 0750, true);
}
if (!isset($context['ip'])) {
@@ -212,3 +220,17 @@ function debugLog(string $message): void
{
appLog($message, 'DEBUG');
}
function getInternalSocketKey(): string
{
$key = getenv('INTERNAL_SOCKET_KEY');
if ($key) {
return trim($key);
}
$path = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '/home/siro-api/.internal_socket_key';
if (file_exists($path)) {
return trim((string)@file_get_contents($path));
}
return '';
}

View File

@@ -1,13 +1,21 @@
<?php
//encrypt_decrypt.php
// ⚠️ هذا الملف للتوافقية فقط. استخدم core/Security/EncryptionHelper.php للتشفير الجديد
require_once realpath(__DIR__ . '/../vendor/autoload.php');
require_once 'load_env.php';
$env_file = '/home/siro-api/env/.env';
loadEnvironment($env_file);
$key = trim(file_get_contents('/home/siro-api/.enckey'));
// ✅ FIX C-02: استخدام getenv بدلاً من file_get_contents الثابت
$keyPath = getenv('ENCRYPTION_KEY_PATH');
$key = '';
if ($keyPath && file_exists($keyPath)) {
$key = trim(file_get_contents($keyPath));
}
if (!$key) {
$key = getenv('ENC_KEY') ?: '';
}
$iv = getenv('initializationVector'); // 16 bytes
@@ -78,6 +86,11 @@ class EncryptionHelper {
$encryptedData = file_get_contents($encryptedFilePath);
$decryptedData = openssl_decrypt($encryptedData, 'AES-256-CBC', $this->key, OPENSSL_RAW_DATA, $this->iv);
if ($decryptedData === false) {
error_log("[ERROR] openssl_decrypt failed for file: $encryptedFilePath");
throw new Exception("❌ فشل فك تشفير الملف: $encryptedFilePath");
}
file_put_contents($destinationPath, $decryptedData);
return true;
}

View File

@@ -8,7 +8,7 @@ use Firebase\JWT\SignatureInvalidException;
use Firebase\JWT\BeforeValidException;
$INTERNAL_KEY = trim((string)@file_get_contents('/home/siro-api/.internal_socket_key'));
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
/**
@@ -20,10 +20,37 @@ $INTERNAL_KEY = trim((string)@file_get_contents('/home/siro-api/.internal_socket
* @param $carType string: نوع الطلب (comfort, speed, Lady...)
*/
function getAllowedSocketUrls(): array {
$env = getenv('ALLOWED_SOCKET_URLS');
if ($env) {
return array_map('trim', explode(',', $env));
}
// القيم الافتراضية لو لم تكن موجودة في .env
return [
'http://188.68.36.205:2021',
'http://188.68.36.205:3031',
'https://location.intaleq.xyz',
];
}
function isAllowedSocketUrl(string $url): bool {
$allowed = getAllowedSocketUrls();
foreach ($allowed as $allowedUrl) {
if (str_starts_with($url, $allowedUrl)) {
return true;
}
}
return false;
}
function sendToLocationServer($action, $data) {
// رابط سيرفر اللوكيشن الداخلي أو العام
$url = "http://188.68.36.205:2021";
$INTERNAL_KEY = trim((string)@file_get_contents('/home/siro-api/.internal_socket_key'));
if (!isAllowedSocketUrl($url)) {
error_log("[SSRF_BLOCKED] Attempted connection to: $url");
return;
}
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
$postData = [
'action' => $action,
@@ -44,7 +71,7 @@ function sendToLocationServer($action, $data) {
function findBestDrivers($con, $lat, $lng, $carType) {
// 1. الاتصال بـ Redis لجلب الأقرب
$locationServerUrl = "https://location.intaleq.xyz/api_get_nearby.php";
$INTERNAL_KEY = trim((string)@file_get_contents('/home/siro-api/.internal_socket_key'));
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
$postData = ['lat' => $lat, 'lng' => $lng, 'radius' => 5, 'limit' => 100];
@@ -94,42 +121,64 @@ function findBestDrivers($con, $lat, $lng, $carType) {
JOIN driverToken dt ON dt.captain_id = d.id
WHERE d.id IN ($placeholders) ";
// ✅ FIX C-01: استخدام allowlist للـ carType لمنع SQL Injection
$carType = trim($carType);
$allowedCarTypes = ['Comfort', 'Mishwar Vip', 'Scooter', 'Pink Bike', 'Electric', 'Lady', 'Van', 'Awfar Car', 'Fixed Price', 'Speed', 'Rayeh Gai'];
if (!in_array($carType, $allowedCarTypes, true)) {
$carType = 'Speed';
}
$sqlParams = [];
switch ($carType) {
case 'Comfort':
$sql .= " AND cr.vehicle_category_id = $CAT_CAR AND CAST(TRIM(cr.year) AS UNSIGNED) > 2017 ";
$sql .= " AND cr.vehicle_category_id = ? AND CAST(TRIM(cr.year) AS UNSIGNED) > ? ";
$sqlParams[] = $CAT_CAR;
$sqlParams[] = 2017;
break;
case 'Mishwar Vip':
$sql .= " AND cr.vehicle_category_id = $CAT_CAR AND CAST(TRIM(cr.year) AS UNSIGNED) > 2020 ";
$sql .= " AND cr.vehicle_category_id = ? AND CAST(TRIM(cr.year) AS UNSIGNED) > ? ";
$sqlParams[] = $CAT_CAR;
$sqlParams[] = 2020;
break;
case 'Scooter':
case 'Pink Bike':
$sql .= " AND cr.vehicle_category_id = $CAT_BIKE ";
$sql .= " AND cr.vehicle_category_id = ? ";
$sqlParams[] = $CAT_BIKE;
break;
case 'Electric':
$sql .= " AND cr.vehicle_category_id = $CAT_CAR AND cr.fuel_type_id = $FUEL_ELECTRIC ";
$sql .= " AND cr.vehicle_category_id = ? AND cr.fuel_type_id = ? ";
$sqlParams[] = $CAT_CAR;
$sqlParams[] = $FUEL_ELECTRIC;
break;
case 'Lady':
$femaleHash = 'bQ6yWJ2EVXKZooHdGclvmFiDlZCM8UYeO+ILFjDUvpQ=';
$sql .= " AND cr.vehicle_category_id = $CAT_CAR AND d.gender = '$femaleHash' ";
$sql .= " AND cr.vehicle_category_id = ? AND d.gender = ? ";
$sqlParams[] = $CAT_CAR;
$sqlParams[] = $femaleHash;
break;
case 'Van':
$sql .= " AND cr.vehicle_category_id = $CAT_VAN ";
$sql .= " AND cr.vehicle_category_id = ? ";
$sqlParams[] = $CAT_VAN;
break;
case 'Awfar Car':
$sql .= " AND cr.vehicle_category_id = $CAT_CAR AND CAST(TRIM(cr.year) AS UNSIGNED) > 1995 ";
$sql .= " AND cr.vehicle_category_id = ? AND CAST(TRIM(cr.year) AS UNSIGNED) > ? ";
$sqlParams[] = $CAT_CAR;
$sqlParams[] = 1995;
break;
case 'Fixed Price':
case 'Speed':
case 'Rayeh Gai':
default:
$sql .= " AND cr.vehicle_category_id = $CAT_CAR AND CAST(TRIM(cr.year) AS UNSIGNED) > 2000 ";
$sql .= " AND cr.vehicle_category_id = ? AND CAST(TRIM(cr.year) AS UNSIGNED) > ? ";
$sqlParams[] = $CAT_CAR;
$sqlParams[] = 2000;
break;
}
try {
$allParams = array_merge($driverIds, $sqlParams);
$stmt = $con->prepare($sql);
$stmt->execute($driverIds);
$stmt->execute($allParams);
$finalDrivers = $stmt->fetchAll(PDO::FETCH_ASSOC);
// دمج البيانات
@@ -157,9 +206,9 @@ function findBestDrivers($con, $lat, $lng, $carType) {
}
// --- دالة مساعدة لمخاطبة سيرفر السائقين (Location Socket) ---
function notifyDriversRideTaken($rideId, $winnerDriverId) {
// رابط سيرفر السائقين الداخلي (نفس البورت المستخدم في driver_socket.php)
$url = "http://188.68.36.205:2021";
$INTERNAL_KEY = trim((string)@file_get_contents('/home/siro-api/.internal_socket_key'));
if (!isAllowedSocketUrl($url)) return;
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
$postData = [
'action' => 'ride_taken_event', // هذا الأكشن الجديد في السوكيت
@@ -179,9 +228,9 @@ function notifyDriversRideTaken($rideId, $winnerDriverId) {
curl_close($ch);
}
function notifyDriversOnLocationServer($drivers_ids_array, $payload, $rideId = null) {
// رابط سيرفر اللوكيشن الخارجي
$url = "http://188.68.36.205:2021";
$INTERNAL_KEY = trim((string)@file_get_contents('/home/siro-api/.internal_socket_key'));
if (!isAllowedSocketUrl($url)) return null;
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
$postData = [
'action' => 'dispatch_order', // اسم الحدث المتفق عليه في socket_server.php هناك
@@ -215,9 +264,9 @@ function notifyDriversOnLocationServer($drivers_ids_array, $payload, $rideId = n
* تخاطب السوكيت الموجود محلياً على نفس السيرفر
*/
function notifyPassengerOnRideServer($passenger_id, $payload) {
// الرابط لسيرفر سوكيت الركاب — IP مباشر لتجاوز مشاكل الجدار الناري والدومين
$url = "http://188.68.36.205:3031";
$INTERNAL_KEY = trim((string)@file_get_contents('/home/siro-api/.internal_socket_key'));
if (!isAllowedSocketUrl($url)) return null;
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
if (empty($INTERNAL_KEY)) {
error_log("[SOCKET_CRITICAL] Internal key missing at /home/siro-api/.internal_socket_key");
@@ -263,8 +312,8 @@ function dispatchRideToDrivers($driversData, $rideId, $payloadTemplate, $startNa
error_log("🚀 [DISPATCH_START] RideID: $rideId | Drivers Count: $countDrivers");
$socketUrl = 'http://188.68.36.205:2021';
$internalKeyPath = '/home/siro-api/.internal_socket_key';
$internalKey = file_exists($internalKeyPath) ? trim((string)@file_get_contents($internalKeyPath)) : '';
if (!isAllowedSocketUrl($socketUrl)) return;
$internalKey = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
foreach ($driversData as $driver) {
$driverId = $driver['driver_id'];

View File

@@ -6,7 +6,7 @@
require_once __DIR__ . '/core/bootstrap.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://intaleqapp.com');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Device-FP');

View File

@@ -6,7 +6,22 @@
require_once __DIR__ . '/core/bootstrap.php';
header('Content-Type: application/json');
header("Access-Control-Allow-Origin: " . (getenv('ALLOWED_ORIGIN') ?: '*'));
// ✅ FIX H-03: allowlist صارم للـ Admin Origins
$allowedOrigins = array_filter([
getenv('ALLOWED_ORIGIN') ?: 'https://siromove.com',
'http://localhost',
'http://127.0.0.1',
]);
$requestOrigin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (!empty($requestOrigin)) {
if (in_array($requestOrigin, $allowedOrigins, true)) {
header("Access-Control-Allow-Origin: " . $requestOrigin);
} else {
header("Access-Control-Allow-Origin: https://siromove.com");
}
}
header("Access-Control-Allow-Credentials: true");
header("Access-Control-Allow-Methods: POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
@@ -38,9 +53,8 @@ try {
$con = Database::get('main');
// ── جلب بيانات المشرف ────────────────────────────────────
// ملاحظة: جدول admin_users سيتم إنشاؤه في Phase 4 (db_improvements.sql)
$stmt = $con->prepare("SELECT id, password, email, role FROM admin_users WHERE username = :id OR email = :id LIMIT 1");
// ── جلب بيانات المشرف من جدول adminUser الموحد ──────────
$stmt = $con->prepare("SELECT id, password, email, role FROM adminUser WHERE id = :id OR email = :id LIMIT 1");
$stmt->execute([':id' => $id]);
$admin = $stmt->fetch();
@@ -73,9 +87,9 @@ try {
}
} catch (PDOException $e) {
securityLog("Admin Login PDO Error", ['msg' => $e->getMessage()]);
error_log("[Admin Login PDO Error] " . $e->getMessage());
jsonError('Login failed: Database error', 500);
} catch (Exception $e) {
securityLog("Admin Login Error", ['msg' => $e->getMessage()]);
error_log("[Admin Login Error] " . $e->getMessage());
jsonError('Login failed: Server error', 500);
}

View File

@@ -6,7 +6,7 @@
require_once __DIR__ . '/core/bootstrap.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://intaleqapp.com');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
@@ -67,7 +67,14 @@ try {
$payload['fingerPrint'] = $fpHash;
}
$secretKey = trim(file_get_contents('/home/siro-api/.secret_key'));
$secretKey = '';
$keyPath = getenv('JWT_SECRET_KEY_PATH');
if ($keyPath && file_exists($keyPath)) {
$secretKey = trim(file_get_contents($keyPath));
}
if (!$secretKey) {
$secretKey = getenv('JWT_SECRET_KEY') ?: '';
}
$jwt = Firebase\JWT\JWT::encode($payload, $secretKey, 'HS256');
jsonSuccess([

View File

@@ -1,12 +1,13 @@
<?php
// ============================================================
// loginFirstTimeDriver.php — توكن التسجيل الأول (السائق)
// تم التحديث: استخدام One-Time Registration Tokens
// ============================================================
require_once __DIR__ . '/core/bootstrap.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://intaleqapp.com');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
@@ -27,7 +28,6 @@ try {
$allowed1 = getenv('allowedDriver1');
$allowed2 = getenv('allowedDriver2');
$allowedAudiences = array_values(array_filter([$allowed1, $allowed2]));
$passwordnewpassenger = getenv('passwordnewpassenger');
if (empty($id) || empty($password) || empty($audience)) {
jsonError('Missing input fields.', 400);
@@ -37,9 +37,25 @@ try {
jsonError('Invalid audience', 400);
}
if (!password_verify($password, $passwordnewpassenger)) {
securityLog("FirstTimeDriver login failed (password)", ['id' => $id]);
jsonError('Invalid credentials.', 401);
// ✅ FIX H-06: استخدام One-Time Registration Token عبر Redis بدلاً من كلمة مرور ثابتة
$useOneTimeToken = getenv('USE_ONE_TIME_REG_TOKEN') === 'true';
if ($useOneTimeToken && $redis) {
// التحقق من وجود توكن صالح في Redis
$storedToken = $redis->get("reg_token:{$id}");
if (!$storedToken || !hash_equals($storedToken, $password)) {
securityLog("FirstTimeDriver failed: Invalid or expired one-time token", ['id' => $id]);
jsonError('Invalid or expired registration token.', 401);
}
// حذف التوكن بعد الاستخدام (One-Time)
$redis->del("reg_token:{$id}");
} else {
// Fallback آمن: استخدام كلمة المرور الثابتة مع Rate Limiting مشدد
$passwordnewpassenger = getenv('passwordnewpassenger');
if (!password_verify($password, $passwordnewpassenger)) {
securityLog("FirstTimeDriver login failed (password)", ['id' => $id]);
jsonError('Invalid credentials.', 401);
}
}
$fpPepper = getenv('FP_PEPPER') ?: '';
@@ -47,6 +63,14 @@ try {
? hash('sha256', $fingerprint . $fpPepper)
: null;
// ✅ FIX C-02: استخدام getenv بدلاً من file_get_contents الثابت
$keyPath = getenv('JWT_SECRET_KEY_PATH');
if ($keyPath && file_exists($keyPath)) {
$secretKey = trim(file_get_contents($keyPath));
} else {
$secretKey = getenv('JWT_SECRET_KEY') ?: '';
}
$payload = [
'user_id' => 'new',
'sub' => $id,
@@ -62,7 +86,6 @@ try {
$payload['fingerPrint'] = $fpHash;
}
$secretKey = trim(file_get_contents('/home/siro-api/.secret_key'));
$jwt = Firebase\JWT\JWT::encode($payload, $secretKey, 'HS256');
jsonSuccess([

View File

@@ -6,7 +6,7 @@
require_once __DIR__ . '/core/bootstrap.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: https://intaleqapp.com');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Device-FP');
@@ -63,7 +63,13 @@ try {
$decPhone = !empty($driver['phone']) ? $encryptionHelper->decryptData($driver['phone']) : null;
$decNat = !empty($driver['national_number']) ? $encryptionHelper->decryptData($driver['national_number']) : null;
// ✅ FIX M-04: تسجيل معلومات تشخيصية عند فشل فك التشفير
if (empty($decPhone) || empty($decNat)) {
securityLog("LoginDriver failed: decryption returned null", [
'driver_id' => $driver['id'] ?? 'unknown',
'has_phone' => !empty($driver['phone']),
'has_nat' => !empty($driver['national_number']),
]);
unauthorizedDriver();
}

View File

@@ -6,7 +6,7 @@
require_once __DIR__ . '/core/bootstrap.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://intaleqapp.com');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Device-FP');
@@ -76,7 +76,14 @@ try {
'jti' => bin2hex(random_bytes(16)),
];
$secretKey = trim((string)@file_get_contents('/home/siro-api/.secret_key_pay'));
$secretKey = '';
$payKeyPath = getenv('WALLET_SECRET_KEY_PATH');
if ($payKeyPath && file_exists($payKeyPath)) {
$secretKey = trim(@file_get_contents($payKeyPath));
}
if (!$secretKey) {
$secretKey = getenv('WALLET_SECRET_KEY') ?: '';
}
$jwt = Firebase\JWT\JWT::encode($payload, $secretKey, 'HS256');
$hmac = hash_hmac('sha256', $id, getenv('SECRET_KEY_HMAC'));

View File

@@ -9,7 +9,7 @@
require_once __DIR__ . '/../get_connect.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://intaleqapp.com');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

View File

@@ -11,7 +11,7 @@ require_once __DIR__ . '/../get_connect.php';
//include 'functions.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://intaleqapp.com');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

View File

@@ -10,7 +10,7 @@
require_once __DIR__ . '/../get_connect.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://intaleqapp.com');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

View File

@@ -9,7 +9,7 @@
require_once __DIR__ . '/../get_connect.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://intaleqapp.com');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

View File

@@ -61,7 +61,8 @@ try {
"Content-Type: application/json"
],
CURLOPT_TIMEOUT => 5,
CURLOPT_SSL_VERIFYPEER => false
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2
]);
$result = curl_exec($ch);

View File

@@ -66,7 +66,8 @@ try {
"Content-Type: application/json"
],
CURLOPT_TIMEOUT => 5,
CURLOPT_SSL_VERIFYPEER => false
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2
]);
$result = curl_exec($ch);

View File

@@ -5,30 +5,41 @@ require_once __DIR__ . '/../../connect.php';
header('Content-Type: application/json');
// 1. Authenticate user
$decodedToken = authenticateJWT();
if (!$decodedToken) {
// 1. الـ Sender ID من JWT مباشرة (connect.php) — ممنوع استقباله من الـ request
if (empty($user_id) || $role !== 'driver') {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
exit;
}
$senderID = filterRequest('driverID');
$senderID = $user_id; // ✅ من JWT
$receiverPhone = filterRequest('receiverPhone');
$amount = filterRequest('amount');
$country = filterRequest('country');
if (empty($senderID) || empty($receiverPhone) || empty($amount) || empty($country)) {
if (empty($receiverPhone) || empty($amount) || empty($country)) {
echo json_encode(['status' => 'error', 'message' => 'Missing required fields']);
exit;
}
// Ensure the sender matches the token
if ($decodedToken->id != $senderID) {
echo json_encode(['status' => 'error', 'message' => 'Unauthorized driver ID']);
exit;
// 2. حد أقصى للتحويل (حسب الدولة والعملة)
$maxAmount = 1000000; // افتراضي
$amountInt = (int)$amount;
if ($amountInt <= 0) {
echo json_encode(['status' => 'error', 'message' => 'Invalid amount']);
exit;
}
$countryLower = strtolower($country);
if ($countryLower === 'syria') $maxAmount = 500;
elseif ($countryLower === 'jordan') $maxAmount = 15;
elseif ($countryLower === 'egypt') $maxAmount = 1000;
if ($amountInt > $maxAmount) {
echo json_encode(['status' => 'error', 'message' => "Transfer amount exceeds maximum limit of $maxAmount"]);
exit;
}
// 2. Fetch Receiver details
// 3. Fetch Receiver details
$stmt = $con->prepare("SELECT d.id as driver_id, dt.token as fcm_token, d.name_arabic
FROM driver d
LEFT JOIN driverToken dt ON d.id = dt.captain_id
@@ -48,7 +59,7 @@ if ($receiverID == $senderID) {
exit;
}
// 3. Determine Payment Server URL based on Country
// 4. Determine Payment Server URL based on Country
$walletServer = "https://walletintaleq.intaleq.xyz"; // Default
if (strtolower($country) === 'jordan') {
$walletServer = getenv('WALLET_SERVER_JORDAN') ?: "https://walletintaleq.intaleq.xyz";
@@ -69,14 +80,14 @@ $postData = [
// Generate Headers for Payment Server (Use internal payment key)
$headers = [];
$paymentKey = getenv('PAYMENT_KEY') ;
$paymentKey = getenv('PAYMENT_KEY');
if (!empty($paymentKey)) {
$headers[] = "payment-key: $paymentKey";
} else {
// Fallback just in case
$headers[] = "payment-key: default_internal_secret_123";
if (empty($paymentKey)) {
error_log("CRITICAL: PAYMENT_KEY environment variable is not set. Transfer blocked.");
echo json_encode(['status' => 'error', 'message' => 'Payment configuration error']);
exit;
}
$headers[] = "payment-key: $paymentKey";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $paymentServerUrl);
@@ -91,7 +102,7 @@ curl_close($ch);
$paymentResponse = json_decode($paymentResponseRaw, true);
// 4. Handle Payment Server Response
// 5. Handle Payment Server Response
if ($httpCode === 200 && isset($paymentResponse['status']) && $paymentResponse['status'] === 'success') {
// Transaction successful, send Push Notification
if (!empty($receiver['fcm_token'])) {
@@ -118,11 +129,11 @@ if ($httpCode === 200 && isset($paymentResponse['status']) && $paymentResponse['
'receiver' => $receiver['name_arabic']
]);
} else {
// Payment failed or server error
// Payment failed or server error — ممنوع تسريب debug في الإنتاج
error_log("[transfer] Payment server error | HTTP: $httpCode | Response: $paymentResponseRaw");
echo json_encode([
'status' => 'error',
'message' => $paymentResponse['message'] ?? 'Payment server error',
'debug' => $paymentResponseRaw
'message' => $paymentResponse['message'] ?? 'Payment server error'
]);
}
?>

View File

@@ -5,7 +5,7 @@
require_once __DIR__ . '/../../connect.php';
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Origin: https://siromove.com");
header("Content-Type: application/json; charset=UTF-8");
// تفعيل إظهار الأخطاء لمعرفة مشكلة الكتابة

View File

@@ -2,11 +2,20 @@
// ضبط الهيدر لإرجاع JSON بترميز UTF-8
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../load_env.php';
loadEnvironment('/home/siro-api/env/.env');
// --- إعدادات الاتصال بقاعدة البيانات ---
$servername = "localhost";
$username = "routeuser"; // <== عدّل
$password = "VETA9mX4tSZzm6AGouIM"; // <== عدّل
$dbname = "routedb"; // <== عدّل
$servername = getenv('DB_REVERSE_GEO_HOST') ?: 'localhost';
$username = getenv('DB_REVERSE_GEO_USER') ?: '';
$password = getenv('DB_REVERSE_GEO_PASS') ?: '';
$dbname = getenv('DB_REVERSE_GEO_NAME') ?: '';
if (empty($username) || empty($password) || empty($dbname)) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Database configuration error']);
exit;
}
// --- استقبال الإحداثيات من الطلب ---
$input_lat = isset($_GET['lat']) ? (float)$_GET['lat'] : null;

View File

@@ -286,20 +286,23 @@ foreach ($categories as $key => $carType) {
$priceToken = "";
if (isset($encryptionHelper)) {
$tokenPayload = [
'passenger_id' => $passenger_id,
'passenger_id' => $passenger_id,
'start_location' => $passengerLat . ',' . $passengerLng,
'end_location' => $destLat . ',' . $destLng,
'expires' => time() + 180, // Valid for 3 minutes
'prices' => $pricesRaw
'end_location' => $destLat . ',' . $destLng,
// ✅ FIX R6: تضمين distance و duration في الـ token لمنع التلاعب
'distance' => $distance,
'duration' => $duration,
'expires' => time() + 180, // Valid for 3 minutes
'prices' => $pricesRaw
];
$priceToken = $encryptionHelper->encryptData(json_encode($tokenPayload));
}
echo json_encode([
'status' => 'success',
'data' => $prices,
'price_token' => $priceToken,
'applied_discount' => $discount,
'status' => 'success',
'data' => $prices,
'price_token' => $priceToken,
'applied_discount' => $discount,
'added_negative_balance' => $negativeBalance
]);
?>

View File

@@ -18,8 +18,7 @@ try {
// =================================================================================
function broadcastRideToMarket($rideId, $lat, $lng, $payloadData) {
$url = getenv('LOCATION_SOCKET_URL');
$keyPath = getenv('INTERNAL_SOCKET_KEY_PATH');
$INTERNAL_KEY = $keyPath && file_exists($keyPath) ? trim(file_get_contents($keyPath)) : '';
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
$marketPayload = [
'id' => (string)$rideId,
@@ -138,6 +137,19 @@ if (!isset($tokenData['prices'][$carType])) {
exit;
}
// ✅ FIX H-05: التحقق من distance و duration في الـ token أيضاً
if (isset($tokenData['distance']) && $tokenData['distance'] != $distance) {
error_log("[add_ride] Security failed — distance mismatch.");
printFailure("Tampered ride data (distance mismatch)");
exit;
}
if (isset($tokenData['duration']) && $tokenData['duration'] != $duration_text) {
error_log("[add_ride] Security failed — duration mismatch.");
printFailure("Tampered ride data (duration mismatch)");
exit;
}
// Securely override pricing from the cryptographically signed token
$price = $tokenData['prices'][$carType]['price'];
$price_for_driver = $tokenData['prices'][$carType]['driver_price'];

View File

@@ -4,7 +4,7 @@
require_once __DIR__ . '/../../get_connect.php';
// السماح بالوصول من أي دومين (لأن الرابط سيفتح في متصفح العميل)
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Origin: https://siromove.com");
header("Content-Type: application/json; charset=UTF-8");
$rideID = filter_input(INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT);

View File

@@ -13,7 +13,7 @@ while (ob_get_level()) {
// ابدأ مخزناً جديداً ونظيفاً لهذا الملف فقط
ob_start();
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Origin: https://siromove.com");
header("Access-Control-Allow-Methods: GET");
header("Content-Type: application/json; charset=UTF-8");
@@ -41,8 +41,9 @@ try {
$driverID = $rideData['driver_id'];
$status = $rideData['status'];
$secretSalt = "Siro_Secure_Track_2025";
$generatedToken = md5(trim(strval($rideID)) . trim(strval($driverID)) . $secretSalt);
// ✅ FIX H-03: استبدال md5 بـ hash_hmac
$secretSalt = getenv('TRACKING_SECRET_SALT') ;
$generatedToken = hash_hmac('sha256', $rideID . $driverID, $secretSalt);
if ($token !== $generatedToken) sendError("Invalid Token");

View File

@@ -53,13 +53,19 @@ DROP TABLE IF EXISTS `adminUser`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `adminUser` (
`id` int NOT NULL AUTO_INCREMENT,
`device_number` varchar(300) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`name` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'UUID hex via bin2hex(random_bytes(16))',
`fingerprint` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'بصمة الجهاز مشفرة (AES-GCM)',
`fingerprint_hash` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'sha256 للبصمة للبحث السريع',
`name` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'الاسم مشفر (AES-GCM)',
`phone` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'الهاتف مشفر (AES-GCM)',
`email` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'البريد مشفر (AES-GCM)',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'bcrypt',
`role` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'admin' COMMENT 'admin | super_admin | service',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_fp_hash` (`fingerprint_hash`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--

View File

@@ -18,7 +18,6 @@ if (isset($_POST['start_date']) && isset($_POST['end_date'])) {
}
// 2. الاستعلام: تجميع عدد الملاحظات لكل موظف في كل يوم
// نستخدم DATE(createdAt) لتوحيد التوقيت لليوم فقط
$sql = "
SELECT
DATE(createdAt) as date,
@@ -27,7 +26,7 @@ $sql = "
FROM
notesForDriverService
WHERE
createdAt BETWEEN '$start_date' AND '$end_date 23:59:59'
createdAt BETWEEN :start_date AND :end_date
GROUP BY
DATE(createdAt), editor
ORDER BY
@@ -35,8 +34,9 @@ $sql = "
";
try {
$end_date_full = $end_date . ' 23:59:59';
$stmt = $con->prepare($sql);
$stmt->execute();
$stmt->execute([':start_date' => $start_date, ':end_date' => $end_date_full]);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($data) {

View File

@@ -25,21 +25,20 @@ if (isset($_POST['start_date']) && isset($_POST['end_date'])) {
$end_date = date('Y-m-t', strtotime($start_date));
}
// الاستعلام: جلب عدد السائقين لكل نوع توظيف خلال الفترة المحددة
$sql = "SELECT
employmentType,
COUNT(*) AS `count`
FROM
`driver`
WHERE
DATE(created_at) >= '$start_date'
AND DATE(created_at) <= '$end_date'
DATE(created_at) >= :start_date
AND DATE(created_at) <= :end_date
GROUP BY
employmentType";
try {
$stmt = $con->prepare($sql);
$stmt->execute();
$stmt->execute([':start_date' => $start_date, ':end_date' => $end_date]);
$stats_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($stats_data) {

View File

@@ -12,7 +12,6 @@ $current_month = str_pad($current_month, 2, "0", STR_PAD_LEFT);
$first_day_of_month = date('Y-m-d', strtotime($current_year . '-' . $current_month . '-01'));
$last_day_of_month = date('Y-m-t', strtotime($first_day_of_month));
// تم تعديل الاستعلام ليستخدم المتغيرات $first_day_of_month و $last_day_of_month
$sql = "SELECT
DATE(d.created_at) AS `date`,
d.`maritalStatus` AS NAME,
@@ -21,23 +20,20 @@ FROM
`driver` d
WHERE
d.`maritalStatus` IN ('mayar','masa', 'shahd', 'rama2','rama1')
AND DATE(d.created_at) >= '$first_day_of_month'
AND DATE(d.created_at) <= '$last_day_of_month'
AND DATE(d.created_at) >= :first_day
AND DATE(d.created_at) <= :last_day
GROUP BY
`date`, d.`maritalStatus`
ORDER BY
`date` ASC;
";
`date` ASC";
$stmt = $con->prepare($sql);
$stmt->execute();
$stmt->execute([':first_day' => $first_day_of_month, ':last_day' => $last_day_of_month]);
$passenger_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($passenger_data) {
// طباعة البيانات كـ JSON
jsonSuccess($data = $passenger_data);
jsonSuccess($passenger_data);
} else {
// طباعة رسالة فشل
jsonError($message = "No data found");
jsonError("No data found");
}
?>

View File

@@ -5,14 +5,13 @@ require_once __DIR__ . '/../connect.php';
// إذا لم يتم إرسال تاريخ، نستخدم تاريخ اليوم الحالي
$filter_date = isset($_POST['date']) ? $_POST['date'] : date('Y-m-d');
// الاستعلام: جلب كافة الملاحظات لليوم المحدد مرتبة من الأحدث للأقدم
$sql = "SELECT * FROM `notesForDriverService`
WHERE DATE(`createdAt`) = '$filter_date'
WHERE DATE(`createdAt`) = :filter_date
ORDER BY `createdAt` DESC";
try {
$stmt = $con->prepare($sql);
$stmt->execute();
$stmt->execute([':filter_date' => $filter_date]);
$notes_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($notes_data) {

View File

@@ -18,38 +18,40 @@ if (isset($_POST['start_date']) && isset($_POST['end_date'])) {
$end_date = date('Y-m-t', strtotime($start_date));
}
// 2. جملة SQL المعدلة
$end_date_full = $end_date . ' 23:59:59';
$sql = "
WITH RECURSIVE date_series AS (
SELECT '$start_date' AS date
SELECT :start_date AS date
UNION ALL
SELECT DATE_ADD(date, INTERVAL 1 DAY)
FROM date_series
WHERE date < '$end_date'
WHERE date < :end_date
)
SELECT
date_series.date AS day,
-- (passengers) عدد الركاب اليومي
COALESCE((SELECT COUNT(id) FROM passengers WHERE DATE(passengers.created_at) = date_series.date), 0) AS totalPassengers,
-- (passengers) الإجمالي للفترة المحددة
(
SELECT COUNT(*)
FROM passengers
WHERE passengers.created_at BETWEEN '$start_date' AND '$end_date 23:59:59'
WHERE passengers.created_at BETWEEN :start_date_total AND :end_date_total
) AS totalMonthly
FROM
date_series
GROUP BY date_series.date
ORDER BY date_series.date ASC;
";
// ملاحظة: جعلت الترتيب ASC (تصاعدي) لأن الرسم البياني يحتاج الأيام من البداية للنهاية
ORDER BY date_series.date ASC";
try {
$stmt = $con->prepare($sql);
$stmt->execute();
$stmt->execute([
':start_date' => $start_date,
':end_date' => $end_date,
':start_date_total' => $start_date,
':end_date_total' => $end_date_full
]);
$passenger_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($passenger_data) {

View File

@@ -18,24 +18,23 @@ if (isset($_POST['start_date']) && isset($_POST['end_date'])) {
$end_date = date('Y-m-t', strtotime($start_date));
}
// 2. جملة الـ SQL المعدلة لتعمل مع النطاق الزمني
$end_date_full = $end_date . ' 23:59:59';
$sql = "
WITH RECURSIVE date_series AS (
SELECT '$start_date' AS date
SELECT :start_date AS date
UNION ALL
SELECT DATE_ADD(date, INTERVAL 1 DAY)
FROM date_series
WHERE date < '$end_date'
WHERE date < :end_date
)
SELECT
date_series.date AS day,
-- حساب الرحلات المنتهية في هذا اليوم
COALESCE(SUM(ride.status = 'Finished'), 0) AS totalRides,
-- حساب الإجمالي للفترة المحددة كاملة (وليس للشهر فقط)
(SELECT COUNT(*) FROM ride
WHERE ride.created_at >= '$start_date'
AND ride.created_at <= '$end_date 23:59:59'
WHERE ride.created_at >= :start_date_total
AND ride.created_at <= :end_date_total
AND ride.status = 'Finished') AS totalMonthly
FROM
@@ -44,16 +43,22 @@ LEFT JOIN
ride ON DATE(ride.created_at) = date_series.date
AND ride.status = 'Finished'
WHERE
date_series.date >= '$start_date'
AND date_series.date <= '$end_date'
date_series.date >= :start_date_where
AND date_series.date <= :end_date_where
GROUP BY
date_series.date
ORDER BY
date_series.date ASC;
";
date_series.date ASC";
$stmt = $con->prepare($sql);
$stmt->execute();
$stmt->execute([
':start_date' => $start_date,
':end_date' => $end_date,
':start_date_total' => $start_date,
':end_date_total' => $end_date_full,
':start_date_where' => $start_date,
':end_date_where' => $end_date
]);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($data) {

View File

@@ -18,36 +18,33 @@ if (isset($_POST['start_date']) && isset($_POST['end_date'])) {
$end_date = date('Y-m-t', strtotime($start_date));
}
// 2. جملة SQL المعدلة
$end_date_full = $end_date . ' 23:59:59';
$sql = "
WITH RECURSIVE date_series AS (
SELECT '$start_date' AS DATE
SELECT :start_date AS DATE
UNION ALL
SELECT DATE_ADD(DATE, INTERVAL 1 DAY)
FROM date_series
WHERE DATE < '$end_date'
WHERE DATE < :end_date
)
SELECT
date_series.date AS day,
-- [1] إجمالي السائقين (الكلي في النظام)
(SELECT COUNT(*) FROM driver) AS totalDrivers,
-- [2] يومي: عدد السائقين المسجلين
(
SELECT COUNT(*)
FROM driver
WHERE DATE(driver.created_at) = date_series.date
) AS dailyTotalDrivers,
-- [3] يومي: عدد السائقين المتصل بهم (notesForDriverService)
(
SELECT COUNT(*)
FROM notesForDriverService
WHERE DATE(notesForDriverService.createdAt) = date_series.date
) AS dailyTotalCallingDrivers,
-- [4] يومي: Matching Notes (Join with driver on phone)
(
SELECT COUNT(*)
FROM notesForDriverService n
@@ -55,26 +52,23 @@ SELECT
WHERE DATE(n.createdAt) = date_series.date
) AS dailyMatchingNotes,
-- [5] إجمالي الفترة: سائقين
(
SELECT COUNT(*)
FROM driver
WHERE driver.created_at BETWEEN '$start_date' AND '$end_date 23:59:59'
WHERE driver.created_at BETWEEN :start_date1 AND :end_date1
) AS totalMonthlyDrivers,
-- [6] إجمالي الفترة: Calling Drivers
(
SELECT COUNT(*)
FROM notesForDriverService
WHERE notesForDriverService.createdAt BETWEEN '$start_date' AND '$end_date 23:59:59'
WHERE notesForDriverService.createdAt BETWEEN :start_date2 AND :end_date2
) AS totalMonthlyCallingDrivers,
-- [7] إجمالي الفترة: Matching Notes
(
SELECT COUNT(*)
FROM notesForDriverService n
JOIN driver d ON n.phone = d.phone
WHERE n.createdAt BETWEEN '$start_date' AND '$end_date 23:59:59'
WHERE n.createdAt BETWEEN :start_date3 AND :end_date3
) AS totalMonthlyMatchingNotes
FROM
@@ -82,12 +76,20 @@ FROM
GROUP BY
date_series.date
ORDER BY
date_series.date ASC;
";
date_series.date ASC";
try {
$stmt = $con->prepare($sql);
$stmt->execute();
$stmt->execute([
':start_date' => $start_date,
':end_date' => $end_date,
':start_date1' => $start_date,
':end_date1' => $end_date_full,
':start_date2' => $start_date,
':end_date2' => $end_date_full,
':start_date3' => $start_date,
':end_date3' => $end_date_full
]);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($data) {

View File

@@ -108,11 +108,11 @@ try {
// توليد مفتاح HMAC فريد للمستخدم (للتوافق مع CRUD الجديد)
$hmacKey = hash_hmac('sha256', (string)$user['id'], getenv('SECRET_KEY_HMAC'));
// ✅ FIX H-05: لا نعيد مفتاح HMAC أبداً (يُحسب على العميل بنفس المعادلة)
printSuccess([
"message" => "Login successful",
"data" => $user,
"jwt" => $jwt,
"hmac" => $hmacKey,
"expires_in" => $expires_in
]);
@@ -124,8 +124,10 @@ try {
jsonError("الجهاز أو الحساب غير مسجل. يرجى إدخال البريد الإلكتروني وكلمة المرور إذا كان هذا أول تسجيل دخول لك.");
}
} catch (Exception $e) {
error_log("[ServiceApp Login Error] " . $e->getMessage());
jsonError("Server error: " . $e->getMessage());
// ✅ FIX M-02: إخفاء تفاصيل الخطأ في الإنتاج
$debugMode = getenv('APP_DEBUG') === 'true';
securityLog("[ServiceApp Login Error]", ['msg' => $e->getMessage()]);
jsonError($debugMode ? "Server error: " . $e->getMessage() : "Server error. Please try again later.", 500);
}
exit();

View File

@@ -6,7 +6,7 @@
<title>نظام إدارة المركبات</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer">
<style>
body { font-family: 'Cairo', sans-serif; background-color: #f8fafc; }
.skeleton {

View File

@@ -1,68 +1,73 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/connect.php'; // Ensure this is correct and contains the connection logic
// ============================================================
// upload_audio.php — رفع ملفات صوتية بشكل آمن
// ============================================================
// Get the audio file from the request
$audio_file = $_FILES['audio'];
$passengerId = filterRequest("passengerId"); // Ensure this is defined correctly
require_once __DIR__ . '/connect.php';
// Check if the audio file was uploaded successfully
if ($audio_file['error'] !== UPLOAD_ERR_OK) {
error_log("File upload error: " . $audio_file['error']);
echo json_encode(array('status' => 'The audio file was not uploaded successfully.'));
exit;
}
header('Content-Type: application/json; charset=UTF-8');
// Get the file name and extension of the audio file
$audio_name = $audio_file['name'];
$audio_extension = pathinfo($audio_name, PATHINFO_EXTENSION);
try {
// ✅ FIX C-03: إضافة Rate Limiting
$limiter = new RateLimiter($redis);
$limiter->enforce(RateLimiter::identifier(), 'upload');
// Check if the audio file is a valid audio format
if (!in_array($audio_extension, array('m4a', 'mp3', 'wav'))) {
echo json_encode(array('status' => 'The audio file is not a valid format.'));
exit;
}
// Get the audio file from the request
if (!isset($_FILES['audio']) || $_FILES['audio']['error'] !== UPLOAD_ERR_OK) {
uploadLog("File upload error code: " . ($_FILES['audio']['error'] ?? 'NO_FILE'), 'ERROR');
echo json_encode(array('status' => 'The audio file was not uploaded successfully.'));
exit;
}
// MIME Type validation using finfo
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime_type = finfo_file($finfo, $audio_file['tmp_name']);
finfo_close($finfo);
$audio_file = $_FILES['audio'];
$allowed_mime_types = ['audio/mp4', 'audio/mpeg', 'audio/wav', 'audio/x-m4a'];
if (!in_array($mime_type, $allowed_mime_types)) {
echo json_encode(array('status' => 'The audio file is not a valid format (MIME mismatch).'));
exit;
}
// Validate MIME type using finfo
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime_type = $finfo->file($audio_file['tmp_name']);
// Generate a new filename using the passenger ID to avoid conflicts
$new_filename = $audio_name . '.' . $audio_extension;
$allowed_mime_types = ['audio/mp4', 'audio/mpeg', 'audio/wav', 'audio/x-m4a', 'audio/ogg', 'audio/webm'];
if (!in_array($mime_type, $allowed_mime_types, true)) {
uploadLog("Invalid MIME type: $mime_type", 'WARNING', ['ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown']);
echo json_encode(array('status' => 'The audio file is not a valid format (MIME mismatch).'));
exit;
}
// Move the audio file to the uploads directory with the new filename
$target_dir = "upload_audio/";
if (!is_dir($target_dir)) {
mkdir($target_dir, 0755, true); // Create directory if it doesn't exist
}
$target_file = $target_dir . $new_filename;
if (!move_uploaded_file($audio_file['tmp_name'], $target_file)) {
error_log("Failed to move file to target directory: " . print_r($audio_file, true));
echo json_encode(array('status' => 'Failed to move the audio file.'));
exit;
}
// Get extension from MIME type (safe)
$ext = match ($mime_type) {
'audio/mp4', 'audio/x-m4a' => 'm4a',
'audio/mpeg' => 'mp3',
'audio/wav' => 'wav',
'audio/ogg' => 'ogg',
'audio/webm' => 'webm',
default => 'bin',
};
// Construct the link to the uploaded audio file dynamically
$host = $_SERVER['HTTP_HOST'] ?? 'api.siromove.com';
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
$base_url = "$protocol://$host/siro/upload_audio/";
$linkAudio = $base_url . $new_filename;
// ✅ Generate secure random filename to prevent path traversal and overwrite
$new_filename = bin2hex(random_bytes(16)) . '.' . $ext;
// Move the audio file to the uploads directory
$target_dir = __DIR__ . "/upload_audio/";
if (!is_dir($target_dir)) {
mkdir($target_dir, 0750, true);
}
$target_file = $target_dir . $new_filename;
// Respond with success and the audio file link
echo json_encode(array('status' => 'Audio file uploaded successfully.', 'link' => $linkAudio));
if (!move_uploaded_file($audio_file['tmp_name'], $target_file)) {
uploadLog("Failed to move file", 'ERROR', ['error' => print_r($audio_file, true)]);
echo json_encode(array('status' => 'Failed to move the audio file.'));
exit;
}
// Close the database connection if it was established
if (isset($conn)) {
mysqli_close($conn);
// Construct the link dynamically
$host = $_SERVER['HTTP_HOST'] ?? 'api.siromove.com';
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
$linkAudio = "$protocol://$host/siro/upload_audio/" . $new_filename;
uploadLog("Audio uploaded successfully: $linkAudio", 'INFO');
echo json_encode(array('status' => 'Audio file uploaded successfully.', 'link' => $linkAudio));
} catch (Exception $e) {
error_log("[upload_audio] Error: " . $e->getMessage());
echo json_encode(array('status' => 'Server error.'));
}
?>

View File

@@ -0,0 +1,154 @@
# 🛡️ تقرير التدقيق الأمني الشامل - Siro Project + Wallet Server
**التاريخ:** 16 يونيو 2026
**النسخة:** 2.0 (Final - مع التعديلات)
**المحلل:** فريق التدقيق الأمني
---
# 📊 ملخص تنفيذي
| العنصر | القيمة |
|--------|--------|
| إجمالي ملفات PHP | 1,500+ |
| إجمالي ملفات Dart/Flutter | 932 |
| إجمالي الثغرات المكتشفة | 19 |
| 🔴 ثغرات حرجة تم إصلاحها | 5 |
| 🟠 عالية تم إصلاحها | 3 |
| 🟡 متوسطة تم إصلاحها | 2 |
| 📋 باقٍ تحت المراقبة | 9 |
---
# ✅ التعديلات المنفذة
## 1⃣ حذف الملفات غير المستخدمة
| الملف | المسار | السبب |
|------|--------|-------|
| ❌ `loginJwtWalletDriver.php` | `walletintaleq.intaleq.xyz/v2/main/` | غير مستخدم - نسخة قديمة بدون بصمة جهاز |
| ❌ `debug_login.php` | `backend/Admin/auth/` | ملف debug خطير يكشف بنية DB ومسارات السيرفر |
## 2⃣ إصلاح `unserialize` في `functions.php`
**التغيير:**
```php
// قبل (PHP serialize - خطر):
define('ALLOWED_SOCKET_URLS', serialize([...]));
$allowed = unserialize(ALLOWED_SOCKET_URLS);
// بعد (getenv + CSV آمن):
function getAllowedSocketUrls(): array {
$env = getenv('ALLOWED_SOCKET_URLS');
if ($env) return array_map('trim', explode(',', $env));
return [/* defaults */];
}
$allowed = getAllowedSocketUrls();
```
**الفائدة:** إزالة خطر `unserialize Object Injection` ونقل الإعدادات لـ `.env`
## 3⃣ إصلاح `CORS: *` في Wallet Server
**الملفات المعدلة:**
- `walletintaleq.intaleq.xyz/v2/main/jwtconnect.php` - سطر 12
- `walletintaleq.intaleq.xyz/v2/main/loginWalletAdmin.php` - سطر 23
**التغيير:**
```php
// قبل:
header("Access-Control-Allow-Origin: *");
// بعد:
$allowedOrigins = ['https://walletintaleq.intaleq.xyz', 'https://wallet.siromove.com', ...];
header("Access-Control-Allow-Origin: https://walletintaleq.intaleq.xyz");
```
## 4⃣ إصلاح `MD5` في `transfer.php`
**التغيير:**
```php
// قبل (MD5 مكسور + uniqid يمكن توقعه):
$token1 = md5(uniqid("tk1", true));
// بعد (cryptographically secure):
$token1 = bin2hex(random_bytes(32));
```
## 5⃣ نقل `ALLOWED_SOCKET_URLS` لـ `.env`
يضاف للسيرفر في ملف `.env`:
```
ALLOWED_SOCKET_URLS=http://188.68.36.205:2021,http://188.68.36.205:3031,https://location.intaleq.xyz
```
---
# 🔒 الوضع الأمني الحالي (بعد التعديلات)
## ما تم حله (ثغرات مغلقة)
| # | الثغرة | الحالة |
|---|--------|--------|
| CRIT-01 | Backdoor في `loginWalletAdmin.php` | ⏳ موجود - بدون تعديل (قرار فني) |
| CRIT-02 | بصمة جهاز معلقة في `loginJwtWalletDriver.php` | ✅ **ملف محذوف** (غير مستخدم) |
| CRIT-03 | `debug_login.php` يكشف معلومات حساسة | ✅ **ملف محذوف** |
| CRIT-04 | `CORS: *` في wallet server | ✅ **مصلح** - مقيد بالدومين |
| CRIT-05 | `unserialize()` في `functions.php` | ✅ **مصلح** - يستخدم `getenv()` |
| CRIT-06 | `MD5` لتوليد التوكنات المالية | ✅ **مصلح** - يستخدم `random_bytes(32)` |
| CRIT-07 | عدم التحقق من `openssl_decrypt` | ⏳ موجود - أولوية منخفضة |
| CRIT-08 | `unlink()` مع مسار متحكم فيه | ⏳ موجود - محمي بـ `realpath()` |
| HIGH-01 | Rate Limiting يمكن تجاوزه بسقوط Redis | ⏳ موجود - مخاطرة مقبولة |
| HIGH-04 | `passwordnewpassenger` مشترك | ⏳ موجود (في `.env` - طبقة ثانية مع fingerprint) |
| HIGH-05 | كشف معلومات في رسائل الخطأ | ⏳ موجود - لكن غير مكشوف للعامة |
| MED-03 | استخدام `rand()` بدل `random_int()` | ✅ **مصلح** - يستخدم `bin2hex(random_bytes(4))` |
## نقاط مهمة لم نغيرها (قرار فني)
- **Android Manifest Permissions**: كلها مستخدمة فعلياً في التطبيق (contacts, audio, camera, overlay). لا خطورة على السيرفر.
- **`exported="true"` لـ BackgroundService**: تُركت كما هي (تحتاجها Flutter للعمل).
- **`passwordnewpassenger`**: موجود في `.env` ومحمي بـ fingerprint + JWT + rate limiting.
---
# 📊 مصفوفة المخاطر النهائية
| المستوى | العدد | الحالة |
|:-------:|:-----:|--------|
| 🔴 حرجة | 2 | قيد المراقبة (backdoor admin + openssl_decrypt) |
| 🟠 عالية | 3 | قيد المراقبة (Redis fallback, password مشترك, معلومات خطأ) |
| 🟡 متوسطة | 2 | قيد المراقبة (secure_image, CSP headers) |
| 🟢 منخفضة | 1 | قيد المراقبة (TODO في upload) |
---
# 🛠️ الإجراءات المنفذة
## ✅ تم التنفيذ
| # | الإجراء | الملف/المسار |
|---|--------|--------------|
| 1 | **حذف** `loginJwtWalletDriver.php` | `walletintaleq.intaleq.xyz/v2/main/` |
| 2 | **حذف** `debug_login.php` | `backend/Admin/auth/` |
| 3 | **إصلاح** `unserialize``getenv()` | `backend/functions.php` |
| 4 | **إصلاح** `CORS: *` (ملفين) | `walletintaleq.intaleq.xyz/v2/main/jwtconnect.php` |
| 5 | **إصلاح** `CORS: *` | `walletintaleq.intaleq.xyz/v2/main/loginWalletAdmin.php` |
| 6 | **إصلاح** `MD5``random_bytes(32)` | `walletintaleq.intaleq.xyz/v2/main/ride/driverWallet/transfer.php` |
## ⏳ قيد المراقبة (لا تحتاج تغيير حالياً)
- **Android Manifest permissions**: مستخدمة كلها في التطبيق
- **`exported="true"`**: تُركت للعمل
- **`passwordnewpassenger`**: في `.env` مع fingerprint
- **`openssl_decrypt` check**: low priority (الكود يتعامل مع false)
- **Redis fallback**: مخاطرة مقبولة (فقط في حال تعطل Redis)
---
# 🔍 ملخص Semgrep (بعد الإصلاح)
| # | الملف | القاعدة | الحالة قبل | الحالة بعد |
|---|-------|--------|:----------:|:----------:|
| 1 | `backend/functions.php:30` | `unserialize-use` | 🔴 | ✅ مصلح |
| 2 | `walletintaleq/jwtconnect.php:12` | `php-permissive-cors` | 🔴 | ✅ مصلح |
| 3 | `walletintaleq/loginWalletAdmin.php:23` | `php-permissive-cors` | 🔴 | ✅ مصلح |
| 4 | `backend/encrypt_decrypt.php:103` | `openssl-decrypt-validate` | 🟡 | ⏳ موجود |
| 5 | `walletintaleq/encrypt_decrypt.php:79,90` | `openssl-decrypt-validate` | 🟡 | ⏳ موجود |
**ملاحظة:** ملف `loginJwtWalletDriver.php` لم يعد موجوداً للفحص.
---
**توقيع المحلل:** _________________
**التاريخ:** 16/06/2026
**تصنيف السرية:** 📛 سري - للاطلاع المصرح فقط

File diff suppressed because it is too large Load Diff

View File

@@ -35,8 +35,12 @@ class CRUD {
String payload = parts[1];
// إضافة padding للـ base64
switch (payload.length % 4) {
case 2: payload += '=='; break;
case 3: payload += '='; break;
case 2:
payload += '==';
break;
case 3:
payload += '=';
break;
}
final decoded = jsonDecode(utf8.decode(base64Url.decode(payload)));
final exp = decoded['exp'];
@@ -116,7 +120,8 @@ class CRUD {
http.Response? response;
int attempts = 0;
final requestId = DateTime.now().millisecondsSinceEpoch.toString().substring(7);
final requestId =
DateTime.now().millisecondsSinceEpoch.toString().substring(7);
Log.print('🚀 [REQ-$requestId] $link');
if (payload != null) Log.print('📦 [PAYLOAD-$requestId] $payload');
@@ -448,10 +453,11 @@ class CRUD {
required String prompt,
}) async {
var url = Uri.parse(link);
// API key مشفر ومحمي في api_key.dart عبر X.r() ثلاثي المستوى
var apiKey = AK.llamaKey;
var headers = {
'Content-Type': 'application/json',
'Authorization':
'Bearer LL-X5lJ0Px9CzKK0HTuVZ3u2u4v3tGWkImLTG7okGRk4t25zrsLqJ0qNoUzZ2x4ciPy',
'Authorization': 'Bearer $apiKey',
};
var data = json.encode({
'model': 'Llama-3-70b-Inst-FW',
@@ -535,7 +541,6 @@ class CRUD {
return response.statusCode;
}
Future<dynamic> postPayMob(
{required String link, Map<String, dynamic>? payload}) async {
var url = Uri.parse(link);
@@ -699,7 +704,8 @@ class CRUD {
if (response.statusCode == 200 || response.statusCode == 201) {
return jsonDecode(response.body);
}
Log.print('MapSaas Post Error: ${response.statusCode} - ${response.body}');
Log.print(
'MapSaas Post Error: ${response.statusCode} - ${response.body}');
return null;
} catch (e) {
Log.print('MapSaas Post Exception: $e');

View File

@@ -1,3 +1,5 @@
import 'dart:math';
import 'dart:typed_data';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:flutter/foundation.dart';
import 'package:secure_string_operations/secure_string_operations.dart';
@@ -11,9 +13,13 @@ class EncryptionHelper {
static EncryptionHelper? _instance;
late final encrypt.Key key;
late final encrypt.IV iv;
late final encrypt.IV iv; // Legacy CBC IV (kept for backward-compat)
/// Prefix to distinguish new GCM ciphertext from old CBC ciphertext
static const String _gcmPrefix = 'GCM:';
EncryptionHelper._(this.key, this.iv);
static EncryptionHelper get instance {
if (_instance == null) {
throw Exception(
@@ -26,14 +32,13 @@ class EncryptionHelper {
static Future<void> initialize() async {
if (_instance != null) {
debugPrint("EncryptionHelper is already initialized.");
return; // Prevent re-initialization
return;
}
debugPrint("Initializing EncryptionHelper...");
var keyOfApp = r(Env.keyOfApp).toString().split(Env.addd)[0];
var initializationVector =
r(Env.initializationVector).toString().split(Env.addd)[0];
// Set the global instance
_instance = EncryptionHelper._(
encrypt.Key.fromUtf8(keyOfApp!),
encrypt.IV.fromUtf8(initializationVector!),
@@ -41,31 +46,74 @@ class EncryptionHelper {
debugPrint("EncryptionHelper initialized successfully.");
}
/// Encrypts a string
/// ✅ FIX H-04: Encrypts a string using AES-256-GCM with a random IV
/// Format: "GCM:<base64_iv>:<base64_ciphertext>"
String encryptData(String plainText) {
try {
final encrypter =
encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
final encrypted = encrypter.encrypt(plainText, iv: iv);
return encrypted.base64;
final randomIv = _generateRandomIv(12); // 12-byte nonce recommended for GCM
final gcmEncrypter =
encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.gcm));
final encrypted = gcmEncrypter.encrypt(plainText, iv: randomIv);
// Prepend GCM prefix + IV so the server/decoder knows the format
return '$_gcmPrefix${randomIv.base64}:${encrypted.base64}';
} catch (e) {
debugPrint('Encryption Error: $e');
return '';
debugPrint('GCM Encryption failed, falling back to CBC: $e');
// Fallback to CBC for environments that don't support GCM
try {
final cbcEncrypter =
encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
final encrypted = cbcEncrypter.encrypt(plainText, iv: iv);
return encrypted.base64;
} catch (e2) {
debugPrint('CBC Encryption Error: $e2');
return '';
}
}
}
/// Decrypts a string
/// ✅ FIX H-04: Decrypts a string — supports both GCM (new) and CBC (legacy)
String decryptData(String encryptedText) {
try {
final encrypter =
encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
final encrypted = encrypt.Encrypted.fromBase64(encryptedText);
return encrypter.decrypt(encrypted, iv: iv);
if (encryptedText.startsWith(_gcmPrefix)) {
// New GCM format: "GCM:<iv_b64>:<cipher_b64>"
final parts = encryptedText.substring(_gcmPrefix.length).split(':');
if (parts.length != 2) {
debugPrint('Invalid GCM format, falling back to CBC');
return _decryptLegacyCbc(encryptedText);
}
final gcmIv = encrypt.IV.fromBase64(parts[0]);
final gcmEncrypter =
encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.gcm));
return gcmEncrypter.decrypt(
encrypt.Encrypted.fromBase64(parts[1]),
iv: gcmIv);
}
// Legacy CBC format (no prefix)
return _decryptLegacyCbc(encryptedText);
} catch (e) {
debugPrint('Decryption Error: $e');
return '';
try {
return _decryptLegacyCbc(encryptedText);
} catch (_) {
return '';
}
}
}
/// Legacy CBC decryption (backward compatibility with existing data)
String _decryptLegacyCbc(String encryptedText) {
final cbcEncrypter =
encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
final encrypted = encrypt.Encrypted.fromBase64(encryptedText);
return cbcEncrypter.decrypt(encrypted, iv: iv);
}
/// Generates a cryptographically secure random IV
encrypt.IV _generateRandomIv(int length) {
final random = Random.secure();
final bytes = List<int>.generate(length, (_) => random.nextInt(256));
return encrypt.IV(Uint8List.fromList(bytes));
}
}
r(String string) {

View File

@@ -1,3 +1,5 @@
import 'dart:convert';
import 'dart:math';
import 'package:siro_rider/env/env.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:flutter/foundation.dart';
@@ -10,6 +12,7 @@ class EncryptionHelper {
late final encrypt.Key key;
late final encrypt.IV iv;
static const String _gcmPrefix = 'GCM:';
EncryptionHelper._(this.key, this.iv);
static EncryptionHelper get instance {
@@ -39,31 +42,76 @@ class EncryptionHelper {
debugPrint("EncryptionHelper initialized successfully.");
}
/// Encrypts a string
/// ✅ FIX H-04: Encrypts a string using AES-256-GCM (new) with random IV
String encryptData(String plainText) {
try {
final encrypter =
encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
final encrypted = encrypter.encrypt(plainText, iv: iv);
return encrypted.base64;
// Try GCM first (secure mode with random IV)
final randomIv = _generateRandomIv(12);
// GCM mode in encrypt package handles nonce/IV length automatically (12 bytes recommended)
final gcmEncrypter =
encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.gcm));
final encrypted = gcmEncrypter.encrypt(plainText, iv: randomIv);
// Prefix with GCM: to distinguish from legacy CBC
return '$_gcmPrefix${randomIv.base64}:${encrypted.base64}';
} catch (e) {
debugPrint('Encryption Error: $e');
return '';
debugPrint('GCM Encryption failed, falling back to CBC: $e');
// Fallback to CBC with a warning
try {
final cbcEncrypter =
encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
final encrypted = cbcEncrypter.encrypt(plainText, iv: iv);
return encrypted.base64;
} catch (e2) {
debugPrint('CBC Encryption Error: $e2');
return '';
}
}
}
/// Decrypts a string
/// ✅ FIX H-04: Decrypts a string (supports both GCM and legacy CBC)
String decryptData(String encryptedText) {
try {
final encrypter =
encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
final encrypted = encrypt.Encrypted.fromBase64(encryptedText);
return encrypter.decrypt(encrypted, iv: iv);
// Check if it's GCM format
if (encryptedText.startsWith(_gcmPrefix)) {
final parts = encryptedText.substring(_gcmPrefix.length).split(':');
if (parts.length != 2) {
debugPrint('Invalid GCM format, falling back to CBC');
return _decryptLegacyCbc(encryptedText);
}
final iv = encrypt.IV.fromBase64(parts[0]);
final encrypted = parts[1];
final gcmEncrypter =
encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.gcm));
return gcmEncrypter.decrypt(encrypt.Encrypted.fromBase64(encrypted),
iv: iv);
}
// Legacy CBC format
return _decryptLegacyCbc(encryptedText);
} catch (e) {
debugPrint('Decryption Error: $e');
return '';
// Try CBC as last resort
try {
return _decryptLegacyCbc(encryptedText);
} catch (_) {
return '';
}
}
}
/// Legacy CBC decryption (backward compatibility)
String _decryptLegacyCbc(String encryptedText) {
final cbcEncrypter =
encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
final encrypted = encrypt.Encrypted.fromBase64(encryptedText);
return cbcEncrypter.decrypt(encrypted, iv: iv);
}
/// Generate cryptographically secure random IV
encrypt.IV _generateRandomIv(int length) {
final random = Random.secure();
final bytes = List<int>.generate(length, (_) => random.nextInt(256));
return encrypt.IV(Uint8List.fromList(bytes));
}
}
r(String string) {

View File

@@ -9,7 +9,19 @@ loadEnvironment($env_file);
$secretKey = getenv('SECRET_KEY'); // Only need the secret key now
// --- CORS Headers ---
header("Access-Control-Allow-Origin: *"); // Replace * with your Flutter app's origin!
$allowedOrigins = [
'https://walletintaleq.intaleq.xyz',
'https://wallet.siromove.com',
'https://wallet-syria.siromove.com',
'https://wallet-egypt.siromove.com',
'https://wallet-jordan.siromove.com',
];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins)) {
header("Access-Control-Allow-Origin: $origin");
} else {
header("Access-Control-Allow-Origin: https://walletintaleq.intaleq.xyz");
}
header("Access-Control-Allow-Methods: GET, POST, OPTIONS"); // Adjust as needed
header("Access-Control-Allow-Headers: Content-Type, Authorization");
header('Content-Type: application/json'); // Set content type to JSON

View File

@@ -1,117 +0,0 @@
<?php
require_once realpath(__DIR__ . '/../vendor/autoload.php');
require_once 'load_env.php';
$env_file = '/home/intaleq-wallet/env/.env';
loadEnvironment($env_file);
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// Retrieve environment variables
//$secretKey = getenv('SECRET_KEY');
$secretKey = trim(file_get_contents('/home/intaleq-wallet/.secret_key'));
$allowed1 = getenv('allowedWallet1');
$allowed2 = getenv('allowedWallet2');
$issuer = 'Tripz-Wallet';
$allowedAudiences = array_filter([$allowed1, $allowed2]);
$passwordnewpassenger = getenv('passwordnewpassenger');
include "functions.php";
// Validate environment variables
if (empty($secretKey) || empty($passwordnewpassenger) || empty($allowedAudiences)) {
http_response_code(500);
die(json_encode(['error' => 'Server configuration error: Missing environment variables.']));
}
// CORS Headers
header('Content-Type: application/json');
header("Access-Control-Allow-Origin: https://walletintaleq.intaleq.xyz");
header("Access-Control-Allow-Methods: POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
try {
$id = filterRequest('id') ?? '';
$password = filterRequest('password') ?? '';
$audience = filterRequest('aud') ?? '';
$fingerPrint = filterRequest('fingerPrint');
// Input validation
if (empty($id) || empty($password) || empty($audience) || empty($fingerPrint)) {
die(json_encode(['error' => 'Missing required parameters.']));
}
if (!in_array($audience, $allowedAudiences)) {
http_response_code(400);
die(json_encode(['error' => 'Invalid audience']));
}
/*
// Database connection
$dbuser = getenv('USER');
$dbpass = getenv('PASS');
$dbname = getenv('dbname');
$dsn = "mysql:host=localhost;dbname=$dbname;charset=utf8mb4";
$options = [
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
];
$con = new PDO($dsn, $dbuser, $dbpass, $options);
// Fetch token data from database
$stmt = $con->prepare("SELECT `id`, `token`, `captain_id`, `fingerPrint` FROM `driverToken` WHERE `captain_id` = :captain_id");
$stmt->bindParam(':captain_id', $id, PDO::PARAM_STR);
$stmt->execute();
$tokenData = $stmt->fetch(PDO::FETCH_ASSOC);
// 1) يجب وجود سجل
if (!$tokenData) {
http_response_code(403);
die(json_encode(['error' => 'No token record found for this user.']));
}
if (empty($tokenData['fingerPrint']) || !hash_equals($tokenData['fingerPrint'], $fingerPrint)) {
http_response_code(403);
die(json_encode(['error' => 'Device fingerprint verification failed']));
}
*/
// JWT Payload
$payload = [
'user_id' => $id,
'fingerPrint' => $fingerPrint,
'exp' => time() + 60,
'iat' => time(),
'iss' => $issuer,
'aud' => $audience
];
// Ensure secret key is valid before encoding
if (empty($secretKey)) {
throw new Exception("SECRET_KEY is empty.");
}
// Encode JWT
$jwt = JWT::encode($payload, $secretKey, 'HS256');
$hmac = hash_hmac('sha256', $id, getenv('SECRET_KEY_HMAC'));
echo json_encode([
'status' => 'success',
'jwt' => $jwt,
'hmac' => $hmac,
'expires_in' => 60
]);
http_response_code(200);
} catch (Exception $e) {
http_response_code(500);
echo "🔥 Server error: " . $e->getMessage() . "\n";
echo json_encode(['error' => 'An unexpected error occurred.']);
}

View File

@@ -20,7 +20,7 @@ $allowedAudiences = array_filter([$allowed1, $allowed2]);
// --- إعداد رؤوس CORS ---
header('Content-Type: application/json');
header("Access-Control-Allow-Origin: *"); // Allow all for admin app or specify domain
header("Access-Control-Allow-Origin: https://walletintaleq.intaleq.xyz"); // Wallet admin only
header("Access-Control-Allow-Methods: POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization, X-Device-FP");

View File

@@ -80,11 +80,11 @@ try {
}
// 3. Generate unique Tokens and paymentIDs
$paymentID1 = "transfer_" . time() . rand(1000, 9999);
$paymentID2 = "transfer_recv_" . time() . rand(1000, 9999);
$token1 = md5(uniqid("tk1", true));
$token2 = md5(uniqid("tk2", true));
$seferToken = md5(uniqid("sfr", true));
$paymentID1 = "transfer_" . time() . bin2hex(random_bytes(4));
$paymentID2 = "transfer_recv_" . time() . bin2hex(random_bytes(4));
$token1 = bin2hex(random_bytes(32));
$token2 = bin2hex(random_bytes(32));
$seferToken = bin2hex(random_bytes(32));
// 4. Deduct from Sender (payments table)
$deductAmount = -$amount;