chore: استيراد أولي من سيرو (ecfe7568) — بلا أي تعديل
نسخة كاملة من مستودع سيرو عند ecfe7568 لتكون أساس تطبيق «انطلق». نُسخ المتعقَّب في git فقط (12,509 ملفاً / 302 م.ب) بـ git archive، لا `cp -r` — فاستُثنيت تلقائياً مخلفات البناء (build · node_modules · .dart_tool · .gradle · Pods ≈ 10.7 غ.ب) وكل ما يستثنيه .gitignore. هذا الكوميت **بلا أي تعديل عمداً** حتى يكون كل ما يليه فرقاً مقروءاً مقابل سيرو الأصلي. سيرو نفسه لم يُمسّ. ⚠️ لا يبني بعد: `.env` و`lib/env/env.g.dart` غير متعقَّبين في سيرو (وهذا صحيح — أسرار لكل مستأجر). كل تطبيق فلاتر هنا يحتاج .env خاصاً بانطلق ثم توليد env.g.dart عبر build_runner. لا تُنسخ أسرار سيرو. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "transit-dashboard",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 5183,
|
||||
"cwd": "transit_dashboard"
|
||||
},
|
||||
{
|
||||
"name": "siro-admin",
|
||||
"runtimeExecutable": "python3",
|
||||
"runtimeArgs": ["-m", "http.server", "8899", "--directory", "dashboard/siro-admin"],
|
||||
"port": 8899
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
bd73a4b3-6e9c-4611-af5a-f87b31b166be
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
{"session_id": 1782053786182, "last_ping": 1782053786182}
|
||||
@@ -0,0 +1,35 @@
|
||||
# INTRODUCTION
|
||||
#
|
||||
# This is the Flutter and Dart telemetry reporting
|
||||
# configuration file.
|
||||
#
|
||||
# Lines starting with a #" are documentation that
|
||||
# the tools maintain automatically.
|
||||
#
|
||||
# All other lines are configuration lines. They have
|
||||
# the form "name=value". If multiple lines contain
|
||||
# the same configuration name with different values,
|
||||
# the parser will default to a conservative value.
|
||||
|
||||
# DISABLING TELEMETRY REPORTING
|
||||
#
|
||||
# To disable telemetry reporting, set "reporting" to
|
||||
# the value "0" and to enable, set to "1":
|
||||
reporting=1
|
||||
|
||||
# NOTIFICATIONS
|
||||
#
|
||||
# Each tool records when it last informed the user about
|
||||
# analytics reporting and the privacy policy.
|
||||
#
|
||||
# The following tools have so far read this file:
|
||||
#
|
||||
# dart-tools (Dart CLI developer tool)
|
||||
# devtools (DevTools debugging and performance tools)
|
||||
# flutter-tools (Flutter CLI developer tool)
|
||||
#
|
||||
# For each one, the file may contain a configuration line
|
||||
# where the name is the code in the list above, e.g. "dart-tool",
|
||||
# and the value is a date in the form YYYY-MM-DD, a comma, and
|
||||
# a number representing the version of the message that was
|
||||
# displayed.
|
||||
@@ -0,0 +1,103 @@
|
||||
# ============================================================
|
||||
# Siro Project - .gitignore
|
||||
# ============================================================
|
||||
|
||||
# --- Environment & Secrets ---
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
**/*.env
|
||||
**/private_key.pem
|
||||
**/public_key.pem
|
||||
*.pem
|
||||
service-account.json
|
||||
**/service-account.json
|
||||
|
||||
# --- IDE & OS ---
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.vscode/
|
||||
.idea/
|
||||
*.iml
|
||||
.ruby-lsp/
|
||||
.kilo/
|
||||
.dartServer/
|
||||
.gradle/
|
||||
.gradle_home/
|
||||
**/.gradle/
|
||||
**/.dart_tool/
|
||||
.well-known/
|
||||
|
||||
# --- Build Artifacts ---
|
||||
node_modules/
|
||||
vendor/
|
||||
**/vendor/
|
||||
build/
|
||||
dist/
|
||||
*.js.map
|
||||
*.css.map
|
||||
|
||||
# --- Flutter/Dart ---
|
||||
.dart_tool/
|
||||
.packages
|
||||
.pub-cache/
|
||||
pubspec.lock
|
||||
*.g.dart
|
||||
**/env.g.dart
|
||||
*.freezed.dart
|
||||
*.config.dart
|
||||
|
||||
# --- Android ---
|
||||
*.apk
|
||||
*.aab
|
||||
*.dex
|
||||
*.class
|
||||
*.keystore
|
||||
local.properties
|
||||
android/.gradle/
|
||||
android/captures/
|
||||
|
||||
# --- iOS ---
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
Pods/
|
||||
DerivedData/
|
||||
*.xcworkspace
|
||||
xcuserdata/
|
||||
|
||||
# --- Composer / PHP ---
|
||||
# composer.lock مُتتبَّع عمداً: بدونه ينهار payment_server/v2 بعد أي نشر نظيف
|
||||
# (vendor يبقى مستثنى أعلاه — يُبنى بـ composer install من الـ lock)
|
||||
|
||||
# --- Logs ---
|
||||
*.log
|
||||
logs/
|
||||
**/logs/
|
||||
|
||||
# --- Uploads ---
|
||||
uploads/
|
||||
**/uploads/
|
||||
portrate_captain_image/
|
||||
card_image/
|
||||
imageForUsingApp/
|
||||
new_driver_car/
|
||||
upload_audio/
|
||||
|
||||
# --- Python ---
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# --- Firebase ---
|
||||
google-services.json
|
||||
.google-services.json
|
||||
GoogleService-Info.plist
|
||||
|
||||
# --- Audit/Scan Output ---
|
||||
semgrep_*.json
|
||||
nuclei_results.txt
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,64 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.siro.android_bot"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.siro.android_bot"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
buildConfigField("String", "BOT_SECRET_KEY", "\"SIRO_BOT_SUPER_SECRET_123\"")
|
||||
// مضيف الباك إند في مكان واحد: تبديله عند نسخ البوت لعلامة أخرى
|
||||
// (مثل انطلق على api.intaleqapp.com) يصير سطراً واحداً لا بحثاً في الكود.
|
||||
buildConfigField("String", "BACKEND_HOST", "\"https://jordan-siro.intaleqapp.com\"")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.siro.android_bot
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.siro.android_bot", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<queries>
|
||||
<package android:name="ae.com.yalla.go.dubai.client" />
|
||||
<package android:name="com.zakinn.app" />
|
||||
<package android:name="com.bis.taxi" />
|
||||
<package android:name="com.careem.acma" />
|
||||
<package android:name="com.ubercab" />
|
||||
<package android:name="com.taxif.passenger" />
|
||||
<package android:name="me.com.easytaxi" />
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Android_bot"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.Android_bot">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".service.ScraperAccessibilityService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.accessibilityservice"
|
||||
android:resource="@xml/accessibility_service_config" />
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.siro.android_bot
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.siro.android_bot.ui.theme.Android_botTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) ?: "UNKNOWN"
|
||||
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
Android_botTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
BotSetupScreen(
|
||||
deviceId = androidId,
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BotSetupScreen(deviceId: String, modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "Siro Scraper Bot 🤖",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Text(
|
||||
text = "Your Device ID (Android ID):",
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = deviceId,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Black,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Button(onClick = {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Device ID", deviceId)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast.makeText(context, "Copied to clipboard!", Toast.LENGTH_SHORT).show()
|
||||
}) {
|
||||
Text("Copy ID")
|
||||
}
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
Text(
|
||||
text = "Make sure to add this ID to your Siro Server.\nAlso, remember to enable this app in Settings > Accessibility.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun GreetingPreview() {
|
||||
Android_botTheme {
|
||||
BotSetupScreen("a1b2c3d4e5f6g7h8")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.siro.android_bot.network
|
||||
|
||||
import com.siro.android_bot.BuildConfig
|
||||
import android.content.Context
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.io.OutputStreamWriter
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class WorkerClient(private val context: Context) {
|
||||
|
||||
private val TAG = "WorkerClient"
|
||||
// TODO: Fetch this from server during first handshake instead of hardcoding
|
||||
// For now, must match the BOT_SECRET_KEY env var on the server
|
||||
private val SECRET_KEY = BuildConfig.BOT_SECRET_KEY
|
||||
|
||||
// Read the real hardware Android ID
|
||||
val deviceId: String by lazy {
|
||||
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: "UNKNOWN_DEVICE"
|
||||
}
|
||||
|
||||
// المضيف من BuildConfig.BACKEND_HOST (app/build.gradle.kts) — لا يُشفَّر هنا،
|
||||
// حتى يكون تبديله عند نسخ البوت لعلامة أخرى سطراً واحداً في gradle.
|
||||
// للتجريب المحلي: اضبط BACKEND_HOST على "http://10.0.2.2:8000" مع تقديم
|
||||
// نفس المسار /backend/bot/ من جذر السيرفر المحلي.
|
||||
private val BASE_URL = "${BuildConfig.BACKEND_HOST}/backend/bot/standalone_worker.php"
|
||||
|
||||
private fun generateSignature(deviceId: String, ts: Long): String {
|
||||
val message = "$deviceId$ts"
|
||||
val algorithm = "HmacSHA256"
|
||||
val mac = Mac.getInstance(algorithm)
|
||||
val secretKeySpec = SecretKeySpec(SECRET_KEY.toByteArray(), algorithm)
|
||||
mac.init(secretKeySpec)
|
||||
val hashBytes = mac.doFinal(message.toByteArray())
|
||||
return hashBytes.joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
fun fetchTask(): JSONObject? {
|
||||
try {
|
||||
val ts = System.currentTimeMillis() / 1000
|
||||
val sig = generateSignature(deviceId, ts)
|
||||
|
||||
val urlString = "$BASE_URL?device_id=$deviceId&ts=$ts&sig=$sig"
|
||||
val url = URL(urlString)
|
||||
|
||||
Log.d(TAG, "Fetching task from: $urlString")
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
connection.connectTimeout = 10000
|
||||
connection.readTimeout = 10000
|
||||
|
||||
val responseCode = connection.responseCode
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
val reader = BufferedReader(InputStreamReader(connection.inputStream))
|
||||
val response = reader.readText()
|
||||
reader.close()
|
||||
return JSONObject(response)
|
||||
} else {
|
||||
Log.e(TAG, "Fetch Error: HTTP $responseCode")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Exception during fetchTask: ${e.message}")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun submitPrice(taskId: String, appName: String, startLat: Double, startLng: Double, endLat: Double, endLng: Double, distanceKm: Double, price: Double): Boolean {
|
||||
try {
|
||||
val ts = System.currentTimeMillis() / 1000
|
||||
val sig = generateSignature(deviceId, ts)
|
||||
|
||||
val payload = JSONObject().apply {
|
||||
put("device_id", deviceId)
|
||||
put("ts", ts)
|
||||
put("sig", sig)
|
||||
put("task_id", taskId)
|
||||
put("type", "price_check")
|
||||
put("status", "success")
|
||||
|
||||
val resultData = JSONObject().apply {
|
||||
put("app", appName)
|
||||
put("distance_km", distanceKm)
|
||||
put("price", price)
|
||||
put("start_lat", startLat)
|
||||
put("start_lng", startLng)
|
||||
put("end_lat", endLat)
|
||||
put("end_lng", endLng)
|
||||
}
|
||||
put("result_data", resultData)
|
||||
}
|
||||
|
||||
val url = URL(BASE_URL)
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "POST"
|
||||
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8")
|
||||
connection.doOutput = true
|
||||
connection.connectTimeout = 10000
|
||||
connection.readTimeout = 10000
|
||||
|
||||
val writer = OutputStreamWriter(connection.outputStream)
|
||||
writer.write(payload.toString())
|
||||
writer.flush()
|
||||
writer.close()
|
||||
|
||||
val responseCode = connection.responseCode
|
||||
return if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
val reader = BufferedReader(InputStreamReader(connection.inputStream))
|
||||
Log.d(TAG, "Submit Success: ${reader.readText()}")
|
||||
reader.close()
|
||||
true
|
||||
} else {
|
||||
Log.e(TAG, "Submit Error: HTTP $responseCode")
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Exception during submitPrice: ${e.message}")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.siro.android_bot.service
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
|
||||
object AppLauncher {
|
||||
private const val TAG = "AppLauncher"
|
||||
|
||||
// Maps the server app name to the actual Android package name
|
||||
// Important: We will verify these exact package names tomorrow once you install them.
|
||||
private val APP_PACKAGES = mapOf(
|
||||
"yallago" to "ae.com.yalla.go.dubai.client",
|
||||
"ae.com.yalla.go.dubai.client" to "ae.com.yalla.go.dubai.client",
|
||||
"zaken" to "com.zakinn.app",
|
||||
"com.zakinn.app" to "com.zakinn.app",
|
||||
"tufaddal" to "com.bis.taxi",
|
||||
"com.bis.taxi" to "com.bis.taxi",
|
||||
"careem" to "com.careem.acma",
|
||||
"com.careem.acma" to "com.careem.acma",
|
||||
"uber" to "com.ubercab",
|
||||
"com.ubercab" to "com.ubercab",
|
||||
"taxif" to "com.taxif.passenger",
|
||||
"com.taxif.passenger" to "com.taxif.passenger",
|
||||
"jeeny" to "me.com.easytaxi",
|
||||
"me.com.easytaxi" to "me.com.easytaxi"
|
||||
)
|
||||
|
||||
fun launchApp(context: Context, appName: String): Boolean {
|
||||
val packageName = APP_PACKAGES[appName.lowercase()]
|
||||
if (packageName == null) {
|
||||
Log.e(TAG, "Unknown app name from server: $appName")
|
||||
return false
|
||||
}
|
||||
|
||||
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
|
||||
if (launchIntent != null) {
|
||||
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
context.startActivity(launchIntent)
|
||||
Log.i(TAG, "Successfully launched $appName ($packageName)")
|
||||
return true
|
||||
} else {
|
||||
Log.e(TAG, "App $appName ($packageName) is not installed on this device!")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.siro.android_bot.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.siro.android_bot.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun Android_botTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.siro.android_bot.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
<string name="app_name">android_bot</string>
|
||||
<string name="accessibility_service_description">Siro Scraper Accessibility Service helps automate competitor price checks.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.Android_bot" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
|
||||
android:accessibilityFeedbackType="feedbackGeneric"
|
||||
android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagIncludeNotImportantViews"
|
||||
android:canRetrieveWindowContent="true"
|
||||
android:canPerformGestures="true"
|
||||
android:description="@string/accessibility_service_description"
|
||||
android:notificationTimeout="100" />
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older than API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">location.intaleq.xyz</domain>
|
||||
<domain includeSubdomains="true">intaleq.xyz</domain>
|
||||
<domain includeSubdomains="true">siromove.com</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.siro.android_bot
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
@@ -0,0 +1,32 @@
|
||||
[versions]
|
||||
agp = "8.13.2"
|
||||
kotlin = "2.0.21"
|
||||
coreKtx = "1.18.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
lifecycleRuntimeKtx = "2.10.0"
|
||||
activityCompose = "1.8.0"
|
||||
composeBom = "2024.09.00"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#Sun Jun 21 02:56:41 EET 2026
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,23 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "android_bot"
|
||||
include(":app")
|
||||
@@ -0,0 +1,199 @@
|
||||
# =============================================================================
|
||||
# 🔐 Siro Project - Secure Environment Configuration
|
||||
# =============================================================================
|
||||
# ⚠️ CRITICAL: NEVER commit this file to Git!
|
||||
# Add .env to .gitignore immediately
|
||||
# =============================================================================
|
||||
|
||||
# =============================================================================
|
||||
# Database Configuration - MAIN DATABASE
|
||||
# =============================================================================
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_NAME=siro_main
|
||||
DB_USER=siro_user
|
||||
DB_PASS=<CHANGE_ME_STRONG_PASSWORD>
|
||||
|
||||
# =============================================================================
|
||||
# Database Configuration - TRANSIT DATABASE (مواصلاتي — جامعات/مدارس/فنادق/شركات)
|
||||
# =============================================================================
|
||||
# قاعدة بيانات معزولة تماماً عن main/ride/tracking — ممنوع أي JOIN بينها وبينهم،
|
||||
# الربط بينها وبين النظام الرئيسي عبر المعرّفات (passenger_id/driver_id) فقط.
|
||||
DB_TRANSIT_HOST=localhost
|
||||
DB_TRANSIT_PORT=3306
|
||||
DB_TRANSIT_NAME=siroTransitDb
|
||||
DB_TRANSIT_USER=siroTransitUser
|
||||
DB_TRANSIT_PASS=<CHANGE_ME_STRONG_PASSWORD>
|
||||
# مفتاح تشفير الرقم الجامعي (32 byte) — منفصل عن ENC_KEY لمزيد من العزل
|
||||
TRANSIT_STUDENT_ID_KEY=<CHANGE_ME_32_CHAR_KEY>
|
||||
# Origins مسموحة للوحة الويب (مشرف المؤسسة)
|
||||
TRANSIT_ADMIN_ORIGINS=https://transit.siromove.com,https://admin.siromove.com
|
||||
# رابط تفعيل السائق (deep link في تطبيق السائق)
|
||||
APP_DEEP_LINK_BASE=https://siromove.com/driver/transit-activate
|
||||
|
||||
# =============================================================================
|
||||
# Encryption Configuration - CRITICAL FOR SECURITY
|
||||
# =============================================================================
|
||||
# 🔐 Generate 32-character hex key: openssl rand -hex 16
|
||||
ENC_KEY=<CHANGE_ME_32_BYTE_HEX_KEY>
|
||||
ENCRYPTION_KEY_PATH=/home/siro-api/env/.encryption_key
|
||||
|
||||
# =============================================================================
|
||||
# JWT Configuration
|
||||
# =============================================================================
|
||||
JWT_SECRET=<CHANGE_ME_LONG_RANDOM_STRING>
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_EXPIRY=3600
|
||||
JWT_REFRESH_EXPIRY=86400
|
||||
|
||||
# =============================================================================
|
||||
# Redis Configuration
|
||||
# =============================================================================
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_AUTH=<CHANGE_ME_REDIS_PASSWORD>
|
||||
REDIS_DB=0
|
||||
|
||||
# =============================================================================
|
||||
# Rate Limiter Configuration
|
||||
# =============================================================================
|
||||
RATE_LIMIT_LOGIN_ATTEMPTS=5
|
||||
RATE_LIMIT_LOGIN_WINDOW=300
|
||||
RATE_LIMIT_API_REQUESTS=100
|
||||
RATE_LIMIT_API_WINDOW=60
|
||||
|
||||
# =============================================================================
|
||||
# Wallet Configuration - S2S API
|
||||
# =============================================================================
|
||||
WALLET_API_URL=https://walletintaleq.intaleq.xyz/v2/main/
|
||||
# 🔐 Generate HMAC secret: openssl rand -base64 32
|
||||
WALLET_HMAC_SECRET=<CHANGE_ME_LONG_HMAC_SECRET>
|
||||
BACKEND_ID=siromove-backend-01
|
||||
ALLOWED_BACKEND_IDS=siromove-backend-01,siromove-backend-02
|
||||
|
||||
# =============================================================================
|
||||
# Socket/Location Server Configuration
|
||||
# =============================================================================
|
||||
ALLOWED_SOCKET_URLS=https://location.siromove.com,https://socket.siromove.com
|
||||
SOCKET_API_TIMEOUT=10
|
||||
SOCKET_INTERNAL_KEY=<CHANGE_ME_INTERNAL_KEY>
|
||||
LOCATION_SERVER_URL=http://location.intaleq.xyz:2021
|
||||
RIDE_SOCKET_URL=http://location.intaleq.xyz:3031
|
||||
|
||||
# =============================================================================
|
||||
# CORS Configuration
|
||||
# =============================================================================
|
||||
CORS_ALLOWED_ORIGINS=https://siromove.com,https://www.siromove.com
|
||||
CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS
|
||||
CORS_ALLOWED_HEADERS=Content-Type,Authorization
|
||||
|
||||
# =============================================================================
|
||||
# Logging Configuration
|
||||
# =============================================================================
|
||||
LOG_LEVEL=info
|
||||
LOG_PATH=/var/log/siro-api/
|
||||
SECURITY_LOG_PATH=/var/log/siro-api/security/
|
||||
|
||||
# =============================================================================
|
||||
# Bot Configuration
|
||||
# =============================================================================
|
||||
BOT_SECRET_KEY=<CHANGE_ME_BOT_SECRET>
|
||||
FEMALE_GENDER_HASH=<CHANGE_ME_FEMALE_HASH>
|
||||
|
||||
# =============================================================================
|
||||
# Firebase Configuration
|
||||
# =============================================================================
|
||||
FIREBASE_PROJECT_ID=siro-project
|
||||
FIREBASE_API_KEY=<CHANGE_ME_FIREBASE_KEY>
|
||||
|
||||
# =============================================================================
|
||||
# Payment Gateway Configuration
|
||||
# =============================================================================
|
||||
PAYMENT_GATEWAY_URL=https://api.paymentprovider.com
|
||||
PAYMENT_GATEWAY_KEY=<CHANGE_ME_PAYMENT_KEY>
|
||||
PAYMENT_GATEWAY_SECRET=<CHANGE_ME_PAYMENT_SECRET>
|
||||
PAYMENT_WEBHOOK_SECRET=<CHANGE_ME_WEBHOOK_SECRET>
|
||||
# Internal key used for server-to-server calls (Siro Backend → Wallet Server)
|
||||
PAYMENT_KEY=<CHANGE_ME_SHARED_PAYMENT_KEY>
|
||||
|
||||
# =============================================================================
|
||||
# Wallet Servers — Multi-Country (انطلق / Wallet Intaliq)
|
||||
# =============================================================================
|
||||
# Jordan wallet server (walletintaleq.intaleq.xyz)
|
||||
WALLET_SERVER_JORDAN=https://walletintaleq.intaleq.xyz
|
||||
|
||||
# Egypt wallet server
|
||||
WALLET_SERVER_EGYPT=https://wallet-egypt.siromove.com
|
||||
|
||||
# Syria wallet server
|
||||
WALLET_SERVER_SYRIA=https://wallet-syria.siromove.com
|
||||
|
||||
# Shared S2S secret key (must match wallet server's X-S2S-Api-Key config)
|
||||
S2S_SHARED_KEY=<CHANGE_ME_S2S_SHARED_SECRET>
|
||||
|
||||
# =============================================================================
|
||||
# Siro Commissions per Country
|
||||
# =============================================================================
|
||||
# Set the commission percentage Siro takes from drivers in each country
|
||||
# (0.15 = 15%, 0.12 = 12%, 0.10 = 10%)
|
||||
SIRO_COMMISSION_JO=0.15
|
||||
SIRO_COMMISSION_SY=0.12
|
||||
SIRO_COMMISSION_EG=0.10
|
||||
SIRO_COMMISSION_IQ=0.10
|
||||
|
||||
# =============================================================================
|
||||
# SMS Configuration (for OTP)
|
||||
# =============================================================================
|
||||
SMS_PROVIDER=twilio
|
||||
SMS_API_KEY=<CHANGE_ME_SMS_KEY>
|
||||
SMS_API_SECRET=<CHANGE_ME_SMS_SECRET>
|
||||
|
||||
# =============================================================================
|
||||
# Email Configuration
|
||||
# =============================================================================
|
||||
MAIL_HOST=smtp.gmail.com
|
||||
MAIL_PORT=587
|
||||
MAIL_USER=<CHANGE_ME_EMAIL>
|
||||
MAIL_PASS=<CHANGE_ME_EMAIL_PASSWORD>
|
||||
|
||||
# =============================================================================
|
||||
# Application Configuration
|
||||
# =============================================================================
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_NAME=Siro
|
||||
APP_DOMAIN=api-syria.siromove.com
|
||||
APP_COUNTRY=Jordan
|
||||
APP_CURRENCY=JOD
|
||||
|
||||
# =============================================================================
|
||||
# Nabeh Integration (server-to-server API key)
|
||||
# Must match NABEH_API_KEY in Nabeh's .env
|
||||
# =============================================================================
|
||||
NABEH_API_KEY=<CHANGE_ME_SHARED_SECRET>
|
||||
SECRET_KEY_HMAC=<CHANGE_ME_HMAC_SECRET_FOR_SIGNED_URLS>
|
||||
|
||||
# =============================================================================
|
||||
# Security Configuration - Fingerprint & Testers
|
||||
# =============================================================================
|
||||
FP_PEPPER=<CHANGE_ME_FINGERPRINT_PEPPER>
|
||||
ALLOWED_TESTER_EMAILS=driver_tester@siromove.com,passenger_tester@siromove.com
|
||||
|
||||
# =============================================================================
|
||||
# Gemini AI Configuration
|
||||
# =============================================================================
|
||||
GEMINI_API_KEY=<CHANGE_ME_GEMINI_KEY>
|
||||
|
||||
# =============================================================================
|
||||
# Feature Flags
|
||||
# =============================================================================
|
||||
FEATURE_MFA_ENABLED=true
|
||||
FEATURE_S2S_WALLET_ENABLED=true
|
||||
FEATURE_CERTIFICATE_PINNING=true
|
||||
|
||||
# =============================================================================
|
||||
# SECRETS - DO NOT EDIT OR COMMIT!
|
||||
# =============================================================================
|
||||
# This file contains secrets. Keep it secure!
|
||||
# Permissions: chmod 600 .env
|
||||
# Owner: www-data (or your web server user)
|
||||
@@ -0,0 +1,6 @@
|
||||
.DS_Store
|
||||
logs/
|
||||
*.log
|
||||
error_log
|
||||
.gemini/
|
||||
portrate_captain_image/
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Unauthorized: Admin access required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sql = "SELECT
|
||||
`driver`.`id`,
|
||||
`driver`.`phone`,
|
||||
`driver`.`email`,
|
||||
`driver`.`gender`,
|
||||
`driver`.`status`,
|
||||
`driver`.`birthdate`,
|
||||
`driver`.`site`,
|
||||
`driver`.`first_name`,
|
||||
`driver`.`last_name`,
|
||||
`driver`.`employmentType`,
|
||||
`driver`.`maritalStatus`,
|
||||
`driver`.`created_at`,
|
||||
`driver`.`updated_at`,
|
||||
(
|
||||
SELECT COUNT(`driver`.`id`) FROM `driver`
|
||||
) AS countPassenger,
|
||||
(
|
||||
SELECT CAST(AVG(`rating`) AS DECIMAL(10, 2))
|
||||
FROM `ratingPassenger`
|
||||
WHERE `ratingPassenger`.`driverID` = `driver`.`id`
|
||||
) AS ratingPassenger,
|
||||
(
|
||||
SELECT COUNT(*) FROM `ratingPassenger` WHERE `driverID` = `driver`.`id`
|
||||
) AS countDriverRate,
|
||||
(
|
||||
SELECT COUNT(*) FROM `canecl` WHERE `driverID` = `driver`.`id`
|
||||
) AS countPassengerCancel,
|
||||
(
|
||||
SELECT CAST(AVG(`rating`) AS DECIMAL(10, 2))
|
||||
FROM `ratingDriver`
|
||||
WHERE `driver_id` = `driver`.`id`
|
||||
) AS passengerAverageRating,
|
||||
(
|
||||
SELECT COUNT(*) FROM `ratingDriver` WHERE `driver_id` = `driver`.`id`
|
||||
) AS countPassengerRate,
|
||||
(
|
||||
SELECT COUNT(*) FROM `ride` WHERE `driver_id` = `driver`.`id`
|
||||
) AS countPassengerRide,
|
||||
(
|
||||
SELECT `token`
|
||||
FROM `driverToken`
|
||||
WHERE `captain_id` = `driver`.`id`
|
||||
LIMIT 1
|
||||
) AS passengerToken
|
||||
FROM `driver`
|
||||
ORDER BY passengerAverageRating DESC
|
||||
LIMIT :lim OFFSET :off";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$page = max(1, (int) filterRequest('page'));
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
$stmt->bindValue(':lim', $limit, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':off', $offset, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// فك تشفير الحقول الحساسة
|
||||
foreach ($result as &$row) {
|
||||
$row['phone'] = $encryptionHelper->decryptData($row['phone'] ?? '') ?: ($row['phone'] ?? '');
|
||||
$row['email'] = $encryptionHelper->decryptData($row['email'] ?? '') ?: ($row['email'] ?? '');
|
||||
$row['gender'] = $encryptionHelper->decryptData($row['gender'] ?? '') ?: ($row['gender'] ?? 'unknown yet');
|
||||
$row['birthdate'] = $encryptionHelper->decryptData($row['birthdate'] ?? '') ?: ($row['birthdate'] ?? 'unknown yet');
|
||||
$row['site'] = $encryptionHelper->decryptData($row['site'] ?? '') ?: ($row['site'] ?? 'unknown yet');
|
||||
$row['first_name'] = $encryptionHelper->decryptData($row['first_name'] ?? '') ?: ($row['first_name'] ?? '');
|
||||
$row['last_name'] = $encryptionHelper->decryptData($row['last_name'] ?? '') ?: ($row['last_name'] ?? '');
|
||||
$row['employmentType'] = $encryptionHelper->decryptData($row['employmentType'] ?? '') ?: ($row['employmentType'] ?? 'unknown yet');
|
||||
$row['maritalStatus'] = $encryptionHelper->decryptData($row['maritalStatus'] ?? '') ?: ($row['maritalStatus'] ?? 'unknown yet');
|
||||
}
|
||||
|
||||
$countStmt = $con->query("SELECT COUNT(*) FROM `driver`");
|
||||
$total = $countStmt->fetchColumn();
|
||||
|
||||
if (count($result) > 0) {
|
||||
jsonSuccess([
|
||||
'data' => $result,
|
||||
'total' => (int) $total,
|
||||
'page' => $page,
|
||||
'pages' => (int) ceil($total / $limit),
|
||||
]);
|
||||
} else {
|
||||
jsonError("No records found");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$driver_id = filterRequest("driver_id");
|
||||
$driverEmail = $encryptionHelper->encryptData(filterRequest("driverEmail"));
|
||||
$driverPhone = $encryptionHelper->encryptData(filterRequest("driverPhone"));
|
||||
|
||||
|
||||
/**
|
||||
* الفهرس الأعمى: يسمح بالبحث بعد نقل التخزين إلى AES-GCM العشوائي.
|
||||
* تُبقى المقارنة القديمة في نفس الاستعلام كاحتياط حتى تنتهي تعبئة الفهارس.
|
||||
*/
|
||||
global $blindIndex;
|
||||
$emailBidx = $blindIndex ? $blindIndex->index('driver.email', filterRequest("driverEmail")) : null;
|
||||
$phoneBidx = $blindIndex ? $blindIndex->index('driver.phone', filterRequest("driverPhone")) : null;
|
||||
|
||||
$sql = "SELECT
|
||||
`driver`.`id`,
|
||||
`driver`.`phone`,
|
||||
`driver`.`email`,
|
||||
`driver`.`gender`,
|
||||
`driver`.`status`,
|
||||
`driver`.`birthdate`,
|
||||
`driver`.`site`,
|
||||
`driver`.`first_name`,
|
||||
`driver`.`last_name`,
|
||||
`driver`.`education`,
|
||||
`driver`.`employmentType`,
|
||||
`driver`.`maritalStatus`,
|
||||
`driver`.`created_at`,
|
||||
`driver`.`updated_at`,
|
||||
(
|
||||
SELECT COUNT(*) FROM `driver`
|
||||
) AS countPassenger,
|
||||
(
|
||||
SELECT CAST(AVG(`rating`) AS DECIMAL(10, 2))
|
||||
FROM `ratingPassenger`
|
||||
WHERE `ratingPassenger`.`driverID` = `driver`.`id`
|
||||
) AS ratingPassenger,
|
||||
(
|
||||
SELECT COUNT(*) FROM `ratingPassenger` WHERE `driverID` = `driver`.`id`
|
||||
) AS countDriverRate,
|
||||
(
|
||||
SELECT COUNT(*) FROM `canecl` WHERE `driverID` = `driver`.`id`
|
||||
) AS countPassengerCancel,
|
||||
(
|
||||
SELECT CAST(AVG(`rating`) AS DECIMAL(10, 2))
|
||||
FROM `ratingDriver`
|
||||
WHERE `driver_id` = `driver`.`id`
|
||||
) AS passengerAverageRating,
|
||||
(
|
||||
SELECT COUNT(*) FROM `ratingDriver` WHERE `driver_id` = `driver`.`id`
|
||||
) AS countPassengerRate,
|
||||
(
|
||||
SELECT COUNT(*) FROM `ride` WHERE `driver_id` = `driver`.`id`
|
||||
) AS countPassengerRide,
|
||||
(
|
||||
SELECT `token`
|
||||
FROM `driverToken`
|
||||
WHERE `captain_id` = `driver`.`id`
|
||||
LIMIT 1
|
||||
) AS passengerToken
|
||||
FROM `driver`
|
||||
WHERE `driver`.`email` = :email OR `driver`.`phone` = :phone OR `driver`.`id` = :id
|
||||
OR (:email_bidx IS NOT NULL AND `driver`.`email_bidx` = :email_bidx)
|
||||
OR (:phone_bidx IS NOT NULL AND `driver`.`phone_bidx` = :phone_bidx)
|
||||
ORDER BY passengerAverageRating DESC
|
||||
LIMIT 10
|
||||
";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->bindParam(":email", $driverEmail);
|
||||
$stmt->bindParam(":phone", $driverPhone);
|
||||
$stmt->bindParam(":id", $driver_id);
|
||||
$stmt->bindParam(":email_bidx", $emailBidx);
|
||||
$stmt->bindParam(":phone_bidx", $phoneBidx);
|
||||
$stmt->execute();
|
||||
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// فك تشفير الحقول الحساسة
|
||||
foreach ($result as &$row) {
|
||||
$row['phone'] = $encryptionHelper->decryptData($row['phone']);
|
||||
$row['email'] = $encryptionHelper->decryptData($row['email']);
|
||||
$row['gender'] = $encryptionHelper->decryptData($row['gender']);
|
||||
$row['birthdate'] = $encryptionHelper->decryptData($row['birthdate']);
|
||||
$row['site'] = $encryptionHelper->decryptData($row['site']);
|
||||
$row['first_name'] = $encryptionHelper->decryptData($row['first_name']);
|
||||
$row['last_name'] = $encryptionHelper->decryptData($row['last_name']);
|
||||
$row['education'] = $encryptionHelper->decryptData($row['education']);
|
||||
$row['employmentType'] = $encryptionHelper->decryptData($row['employmentType']);
|
||||
$row['maritalStatus'] = $encryptionHelper->decryptData($row['maritalStatus']);
|
||||
}
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
jsonSuccess($result);
|
||||
} else {
|
||||
jsonError("No records found");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Unauthorized: Admin access required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$driver_id = filterRequest("driver_id");
|
||||
|
||||
if (empty($driver_id)) {
|
||||
jsonError("driver_id is required", 400);
|
||||
}
|
||||
|
||||
$sql = "SELECT
|
||||
`driver`.`id`,
|
||||
`driver`.`phone`,
|
||||
`driver`.`email`,
|
||||
`driver`.`gender`,
|
||||
`driver`.`status`,
|
||||
`driver`.`birthdate`,
|
||||
`driver`.`site`,
|
||||
`driver`.`first_name`,
|
||||
`driver`.`last_name`,
|
||||
`driver`.`employmentType`,
|
||||
`driver`.`maritalStatus`,
|
||||
`driver`.`created_at`,
|
||||
`driver`.`updated_at`,
|
||||
(
|
||||
SELECT COUNT(*) FROM `driver`
|
||||
) AS countPassenger,
|
||||
(
|
||||
SELECT CAST(AVG(`rating`) AS DECIMAL(10, 2))
|
||||
FROM `ratingPassenger`
|
||||
WHERE `ratingPassenger`.`driverID` = `driver`.`id`
|
||||
) AS ratingPassenger,
|
||||
(
|
||||
SELECT COUNT(*) FROM `ratingPassenger`
|
||||
WHERE `ratingPassenger`.`driverID` = `driver`.`id`
|
||||
) AS countDriverRate,
|
||||
(
|
||||
SELECT COUNT(*) FROM `canecl`
|
||||
WHERE `canecl`.`driverID` = `driver`.`id`
|
||||
) AS countPassengerCancel,
|
||||
(
|
||||
SELECT CAST(AVG(`rating`) AS DECIMAL(10, 2))
|
||||
FROM `ratingDriver`
|
||||
WHERE `ratingDriver`.`driver_id` = `driver`.`id`
|
||||
) AS passengerAverageRating,
|
||||
(
|
||||
SELECT COUNT(*) FROM `ratingDriver`
|
||||
WHERE `ratingDriver`.`driver_id` = `driver`.`id`
|
||||
) AS countPassengerRate,
|
||||
(
|
||||
SELECT COUNT(*) FROM `ride`
|
||||
WHERE `ride`.`driver_id` = `driver`.`id`
|
||||
) AS countPassengerRide,
|
||||
(
|
||||
SELECT `token`
|
||||
FROM `driverToken`
|
||||
WHERE `driverToken`.`captain_id` = `driver`.`id`
|
||||
LIMIT 1
|
||||
) AS passengerToken
|
||||
FROM `driver`
|
||||
WHERE `driver`.`id` = :driver_id
|
||||
ORDER BY passengerAverageRating DESC
|
||||
LIMIT 10";
|
||||
|
||||
try {
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->bindParam(':driver_id', $driver_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
// بلا هذا الالتقاط كان الاستثناء يُنهي السكربت فيصل للعميل جسم فارغ
|
||||
// بحالة HTTP 200، فيظهر كـ "رد غير JSON".
|
||||
error_log("[getCaptainDetailsById] " . $e->getMessage());
|
||||
jsonError("Could not read the captain record: " . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
// فك تشفير الحقول الحساسة بعد الجلب
|
||||
foreach ($result as &$row) {
|
||||
foreach (['phone','email','gender','birthdate','site','first_name','last_name','employmentType','maritalStatus'] as $f) {
|
||||
if (!array_key_exists($f, $row)) $row[$f] = null;
|
||||
}
|
||||
$row['phone'] = $encryptionHelper->decryptData($row['phone']);
|
||||
$row['email'] = $encryptionHelper->decryptData($row['email']);
|
||||
$row['gender'] = $encryptionHelper->decryptData($row['gender']);
|
||||
$row['birthdate'] = $encryptionHelper->decryptData($row['birthdate']);
|
||||
$row['site'] = $encryptionHelper->decryptData($row['site']);
|
||||
$row['first_name'] = $encryptionHelper->decryptData($row['first_name']);
|
||||
$row['last_name'] = $encryptionHelper->decryptData($row['last_name']);
|
||||
$row['employmentType'] = $encryptionHelper->decryptData($row['employmentType']);
|
||||
$row['maritalStatus'] = $encryptionHelper->decryptData($row['maritalStatus']);
|
||||
}
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
jsonSuccess($result);
|
||||
} else {
|
||||
jsonError("No records found");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Unauthorized: Admin access required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$page = max(1, (int) filterRequest('page'));
|
||||
$limit = 50;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$sql = "
|
||||
SELECT
|
||||
d.phone,
|
||||
d.id,
|
||||
d.name_arabic,
|
||||
dt.token
|
||||
FROM
|
||||
`driver` d
|
||||
LEFT JOIN driverToken dt ON
|
||||
dt.captain_id = d.id
|
||||
LIMIT :lim OFFSET :off
|
||||
";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->bindValue(':lim', $limit, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':off', $offset, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$countStmt = $con->query("SELECT COUNT(*) FROM `driver`");
|
||||
$total = $countStmt->fetchColumn();
|
||||
|
||||
foreach ($result as &$row) {
|
||||
$row['phone'] = $encryptionHelper->decryptData($row['phone']);
|
||||
if (!empty($row['token'])) {
|
||||
$row['token'] = $encryptionHelper->decryptData($row['token']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
jsonSuccess([
|
||||
'data' => $result,
|
||||
'total' => (int) $total,
|
||||
'page' => $page,
|
||||
'pages' => (int) ceil($total / $limit),
|
||||
]);
|
||||
} else {
|
||||
jsonError("No records found");
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$sql = "SELECT
|
||||
(
|
||||
SELECT TIME_FORMAT(SEC_TO_TIME(AVG(TIMESTAMPDIFF(SECOND, rideTimeStart, rideTimeFinish))), '%Hh %im')
|
||||
FROM ride
|
||||
WHERE rideTimeStart IS NOT NULL AND rideTimeFinish IS NOT NULL
|
||||
) AS driver_avg_duration,
|
||||
|
||||
(
|
||||
SELECT COUNT(*) FROM (
|
||||
SELECT COUNT(driver_id) FROM ride GROUP BY driver_id
|
||||
) AS sub
|
||||
) AS num_Driver,
|
||||
|
||||
(
|
||||
SELECT COUNT(*) FROM ride
|
||||
) AS total_rides,
|
||||
|
||||
(
|
||||
SELECT COUNT(*) FROM ride WHERE status = 'waiting'
|
||||
) AS ongoing_rides,
|
||||
|
||||
(
|
||||
SELECT COUNT(*) FROM ride WHERE status = 'Finished'
|
||||
) AS completed_rides,
|
||||
|
||||
(
|
||||
SELECT COUNT(*) FROM ride WHERE status = 'cancelled'
|
||||
) AS cancelled_rides,
|
||||
|
||||
(
|
||||
SELECT TIME_FORMAT(SEC_TO_TIME(MAX(TIMESTAMPDIFF(SECOND, rideTimeStart, rideTimeFinish))), '%Hh %im')
|
||||
FROM ride
|
||||
WHERE rideTimeStart IS NOT NULL AND rideTimeFinish IS NOT NULL
|
||||
) AS longest_duration,
|
||||
|
||||
(
|
||||
SELECT ROUND(SUM(distance), 2) FROM ride
|
||||
) AS total_distance,
|
||||
|
||||
(
|
||||
SELECT ROUND(AVG(distance), 2) FROM ride
|
||||
) AS average_distance,
|
||||
|
||||
(
|
||||
SELECT ROUND(MAX(distance), 2) FROM ride
|
||||
) AS longest_distance,
|
||||
|
||||
(
|
||||
SELECT ROUND(SUM(price_for_driver), 2) FROM ride
|
||||
) AS total_driver_earnings,
|
||||
|
||||
(
|
||||
SELECT ROUND(SUM(price_for_passenger), 2) FROM ride
|
||||
) AS total_company_earnings,
|
||||
|
||||
(
|
||||
SELECT ROUND(
|
||||
(SELECT SUM(price_for_passenger) FROM ride) /
|
||||
NULLIF((SELECT SUM(price_for_driver) FROM ride), 0),
|
||||
2
|
||||
)
|
||||
) AS companyPercent
|
||||
|
||||
FROM dual
|
||||
LIMIT 1";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute();
|
||||
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
jsonSuccess($result);
|
||||
} else {
|
||||
jsonError("No records found");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$currentYear = date('Y');
|
||||
$currentMonth = date('m');
|
||||
|
||||
// SQL to get daily ride counts
|
||||
$sql = "
|
||||
SELECT
|
||||
YEAR(date) AS year,
|
||||
MONTH(date) AS month,
|
||||
DAY(date) AS day,
|
||||
COUNT(*) AS rides_count
|
||||
FROM
|
||||
ride
|
||||
GROUP BY
|
||||
YEAR(date),
|
||||
MONTH(date),
|
||||
DAY(date)
|
||||
ORDER BY
|
||||
YEAR(date),
|
||||
MONTH(date),
|
||||
DAY(date)
|
||||
";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute();
|
||||
$dailyRides = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// SQL to get current month's total ride count
|
||||
$sqlMonth = "
|
||||
SELECT COUNT(*) AS current_month_rides_count
|
||||
FROM ride
|
||||
WHERE MONTH(date) = :currentMonth AND YEAR(date) = :currentYear
|
||||
";
|
||||
$stmtMonth = $con->prepare($sqlMonth);
|
||||
$stmtMonth->bindParam(':currentMonth', $currentMonth);
|
||||
$stmtMonth->bindParam(':currentYear', $currentYear);
|
||||
$stmtMonth->execute();
|
||||
$monthRides = $stmtMonth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// Append current month total to each row (if needed)
|
||||
foreach ($dailyRides as &$row) {
|
||||
$row['current_month_rides_count'] = $monthRides['current_month_rides_count'];
|
||||
}
|
||||
|
||||
// Return result
|
||||
if ($dailyRides) {
|
||||
jsonSuccess($dailyRides);
|
||||
} else {
|
||||
jsonError("No records found");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin/Staff/activate.php
|
||||
* تفعيل الحسابات المعلقة للمشرفين (Admins) وموظفي خدمة العملاء (Service) من قبل المشرف العام
|
||||
*/
|
||||
require_once __DIR__ . '/../../core/bootstrap.php';
|
||||
require_once __DIR__ . '/../../functions.php';
|
||||
|
||||
$userId = filterRequest('user_id');
|
||||
$type = filterRequest('type'); // 'admin' or 'service'
|
||||
|
||||
if (empty($userId) || empty($type)) {
|
||||
jsonError("رقم المستخدم ونوع الحساب مطلوبان.");
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$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;
|
||||
}
|
||||
|
||||
if ($type === 'admin') {
|
||||
$stmt = $con->prepare("UPDATE adminUser SET status = 'active' WHERE id = :id AND status = 'pending'");
|
||||
$stmt->execute([':id' => $userId]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
printSuccess(["message" => "تم تفعيل حساب المشرف بنجاح."]);
|
||||
} else {
|
||||
jsonError("لم يتم العثور على حساب مشرف معلق بهذا المعرف.");
|
||||
}
|
||||
} elseif ($type === 'service') {
|
||||
$stmt = $con->prepare("UPDATE users SET status = 'approved' WHERE id = :id AND status = 'pending' AND user_type = 'service'");
|
||||
$stmt->execute([':id' => $userId]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
printSuccess(["message" => "تم تفعيل حساب موظف الخدمة بنجاح."]);
|
||||
} else {
|
||||
jsonError("لم يتم العثور على حساب موظف خدمة معلق بهذا المعرف.");
|
||||
}
|
||||
} else {
|
||||
jsonError("نوع حساب غير صالح.");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log("[Staff Activate Error] " . $e->getMessage());
|
||||
jsonError("An internal error occurred. Please try again later.");
|
||||
}
|
||||
exit();
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin/Staff/add.php
|
||||
* إضافة موظف جديد (أدمن أو خدمة عملاء) مع تشفير البيانات وحفظ بصمة الجهاز
|
||||
*/
|
||||
require_once __DIR__ . '/../../core/bootstrap.php';
|
||||
|
||||
$con = Database::get('main');
|
||||
|
||||
// التحقق من الصلاحيات: فقط المشرف العام يمكنه إضافة مشرفين جدد
|
||||
$jwtService = new JwtService($redis);
|
||||
$auth = $jwtService->authenticate();
|
||||
$authRole = $auth->role ?? '';
|
||||
|
||||
$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');
|
||||
$site = filterRequest("site") ?? 'main';
|
||||
$country = filterRequest("country") ?? 'Jordan';
|
||||
|
||||
if (empty($name) || empty($password) || empty($role)) {
|
||||
jsonError("Missing required fields (name, password, role).");
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
// تشفير البيانات الحساسة
|
||||
$encName = $encryptionHelper->encryptData($name);
|
||||
$encPhone = $encryptionHelper->encryptData($phone);
|
||||
$encEmail = $encryptionHelper->encryptData($email);
|
||||
|
||||
// تشفير البصمة وهش البصمة (إذا تم إرسالها)
|
||||
$encFp = $fingerprint ? $encryptionHelper->encryptData($fingerprint) : '';
|
||||
$fpHash = $fingerprint ? hash('sha256', $fingerprint) : '';
|
||||
$uniqueId = bin2hex(random_bytes(16));
|
||||
|
||||
if ($role === 'admin') {
|
||||
$sql = "INSERT INTO adminUser (id, fingerprint, fingerprint_hash, name, phone, email, password, role, created_at)
|
||||
VALUES (:id, :fp, :fp_hash, :name, :phone, :email, :pass, :role, NOW())";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([
|
||||
':id' => $uniqueId,
|
||||
':fp' => $encFp,
|
||||
':fp_hash' => $fpHash,
|
||||
':name' => $encName,
|
||||
':phone' => $encPhone,
|
||||
':email' => $encEmail,
|
||||
':pass' => $hashedPassword,
|
||||
':role' => $role
|
||||
]);
|
||||
} else {
|
||||
// الإضافة لجدول المستخدمين (خدمة العملاء)
|
||||
$sql = "INSERT INTO users (id, fingerprint, fingerprint_hash, phone, email, gender, password, birthdate, user_type, first_name, last_name, site, country, status, created_at)
|
||||
VALUES (:id, :fp, :fp_hash, :phone, :email, :gender, :pass, :bdate, 'service', :fname, :lname, :site, :country, 'approved', NOW())";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([
|
||||
':id' => $uniqueId,
|
||||
':fp' => $encFp,
|
||||
':fp_hash' => $fpHash,
|
||||
':phone' => $encPhone,
|
||||
':email' => $encEmail,
|
||||
':gender' => $gender,
|
||||
':pass' => $hashedPassword,
|
||||
':bdate' => $birthdate,
|
||||
':fname' => $encName,
|
||||
':lname' => '',
|
||||
':site' => $site,
|
||||
':country' => $country
|
||||
]);
|
||||
}
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
jsonSuccess("Staff member added successfully.");
|
||||
} else {
|
||||
jsonError("Failed to add staff member.");
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("[Staff Add Error] " . $e->getMessage());
|
||||
jsonError("An internal error occurred. Please try again later.");
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin/Staff/pending.php
|
||||
* جلب الحسابات المعلقة للإداريين والخدمة
|
||||
*/
|
||||
// connect.php يفرض JWT — بدونه كانت هذه النقطة تكشف أسماء وأرقام
|
||||
// المشرفين المعلقين لأي زائر بلا أي مصادقة.
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Unauthorized: Admin access required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$allPending = [];
|
||||
$sources = [];
|
||||
|
||||
// كل مصدر يُجلب على حدة: غياب جدول users في بعض عمليات النشر كان يُفشل
|
||||
// الطلب بالكامل ويخفي طلبات المشرفين المعلقة أيضاً.
|
||||
/**
|
||||
* بعض عمليات النشر أنشأت adminUser بلا عمود status (انظر schema_primary.sql)،
|
||||
* وعندها لا يمكن تمييز الحسابات المعلقة أصلاً. نفحص العمود أولاً لنُرجع سبباً
|
||||
* واضحاً بدل فشل عام.
|
||||
*/
|
||||
function columnExists(PDO $con, string $table, string $column): bool
|
||||
{
|
||||
try {
|
||||
$stmt = $con->prepare("SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?");
|
||||
$stmt->execute([$table, $column]);
|
||||
return (int) $stmt->fetchColumn() > 0;
|
||||
} catch (Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!columnExists($con, 'adminUser', 'status')) {
|
||||
throw new RuntimeException("adminUser.status column is missing — admin approvals cannot be tracked until it is added.");
|
||||
}
|
||||
|
||||
$stmt1 = $con->query("SELECT id, name, phone, role, created_at, 'admin' as type FROM adminUser WHERE status = 'pending'");
|
||||
$admins = $stmt1->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($admins as &$admin) {
|
||||
$admin['name'] = $encryptionHelper->decryptData($admin['name']) ?: $admin['name'];
|
||||
$admin['phone'] = $encryptionHelper->decryptData($admin['phone']) ?: $admin['phone'];
|
||||
}
|
||||
unset($admin);
|
||||
|
||||
$allPending = array_merge($allPending, $admins);
|
||||
$sources['admins'] = 'ok';
|
||||
} catch (Throwable $e) {
|
||||
error_log("[Staff Pending] adminUser query failed: " . $e->getMessage());
|
||||
$sources['admins'] = 'unavailable: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt2 = $con->query("SELECT id, first_name, last_name, phone, user_type as role, created_at, 'service' as type FROM users WHERE status = 'pending' AND user_type = 'service'");
|
||||
$services = $stmt2->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($services as &$service) {
|
||||
$service['name'] = trim(
|
||||
($encryptionHelper->decryptData($service['first_name']) ?: $service['first_name']) . ' ' .
|
||||
($encryptionHelper->decryptData($service['last_name']) ?: $service['last_name'])
|
||||
);
|
||||
$service['phone'] = $encryptionHelper->decryptData($service['phone']) ?: $service['phone'];
|
||||
}
|
||||
unset($service);
|
||||
|
||||
$allPending = array_merge($allPending, $services);
|
||||
$sources['service_staff'] = 'ok';
|
||||
} catch (Throwable $e) {
|
||||
error_log("[Staff Pending] users query failed: " . $e->getMessage());
|
||||
$sources['service_staff'] = 'unavailable';
|
||||
}
|
||||
|
||||
printSuccess([
|
||||
"data" => $allPending,
|
||||
"sources" => $sources,
|
||||
]);
|
||||
exit();
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/**
|
||||
* 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) {
|
||||
http_response_code(403);
|
||||
exit(json_encode(['error' => 'Admin already initialized. This script runs only once.']));
|
||||
}
|
||||
|
||||
// ── كلمة المرور من البيئة أو تُنشأ عشوائياً ──────────────
|
||||
$password = getenv('SETUP_SUPER_ADMIN_PASSWORD');
|
||||
if (!$password) {
|
||||
$password = bin2hex(random_bytes(12));
|
||||
}
|
||||
$hashedPass = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
// ── بصمات افتراضية (تُستبدل عند أول تسجيل دخول فعلي) ───
|
||||
$admins = [
|
||||
[
|
||||
'name' => 'Super Admin',
|
||||
'fp' => 'SETUP_DEFAULT_FP_001',
|
||||
'role' => 'super_admin'
|
||||
]
|
||||
];
|
||||
|
||||
try {
|
||||
foreach ($admins as $admin) {
|
||||
$encName = $encryptionHelper->encryptData($admin['name']);
|
||||
$encFp = $encryptionHelper->encryptData($admin['fp']);
|
||||
$fpHash = hash('sha256', $admin['fp']);
|
||||
$uniqueId = bin2hex(random_bytes(16));
|
||||
|
||||
$sql = "INSERT INTO adminUser (id, fingerprint, fingerprint_hash, name, password, role, created_at)
|
||||
VALUES (:id, :fp, :fp_hash, :name, :pass, :role, NOW())";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([
|
||||
':id' => $uniqueId,
|
||||
':fp' => $encFp,
|
||||
':fp_hash' => $fpHash,
|
||||
':name' => $encName,
|
||||
':pass' => $hashedPass,
|
||||
':role' => $admin['role']
|
||||
]);
|
||||
}
|
||||
echo "<h1>Initialization Successful</h1>";
|
||||
} catch (Exception $e) {
|
||||
echo "An internal error occurred";
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../core/bootstrap.php';
|
||||
|
||||
$deviceNumber = filterRequest("deviceNumber");
|
||||
$name = filterRequest("name");
|
||||
$password = filterRequest("password");
|
||||
$role = filterRequest("role") ?? 'admin';
|
||||
|
||||
if (empty($name) || empty($password)) {
|
||||
jsonError("Name and password are required.");
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$con = Database::get('main');
|
||||
|
||||
// Hash the password for security
|
||||
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
$sql = "INSERT INTO `adminUser`(`id`, `device_number`, `name`, `password`, `role`) VALUES (
|
||||
UUID(),
|
||||
:deviceNumber,
|
||||
:name,
|
||||
:password,
|
||||
:role
|
||||
)";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([
|
||||
':deviceNumber' => $deviceNumber,
|
||||
':name' => $name,
|
||||
':password' => $hashedPassword,
|
||||
':role' => $role
|
||||
]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
jsonSuccess("Admin user data saved successfully");
|
||||
} else {
|
||||
jsonError("Failed to save admin user data");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log("[Admin Add Error] " . $e->getMessage());
|
||||
jsonError("An internal error occurred. Please try again later.");
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
// عرض كافة الأخطاء
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$invoiceNumber = filterRequest("invoiceNumber");
|
||||
$amount = filterRequest("amount");
|
||||
$date = filterRequest("date");
|
||||
$name = filterRequest("name");
|
||||
$driverID = filterRequest("driverID") ?? '0';
|
||||
|
||||
$linkImage = null;
|
||||
$uploadDate = date("Y-m-d H:i:s");
|
||||
|
||||
// ✅ طباعة بيانات الإدخال للتأكد
|
||||
error_log("[add_invoice.php] 📥 Data received | invoiceNumber: $invoiceNumber, amount: $amount, date: $date");
|
||||
|
||||
// التحقق من وجود ملف الصورة
|
||||
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
|
||||
$image_file = $_FILES['image'];
|
||||
$image_name = $image_file['name'];
|
||||
$image_extension = strtolower(pathinfo($image_name, PATHINFO_EXTENSION));
|
||||
$allowed_extensions = ['jpg', 'jpeg', 'png'];
|
||||
|
||||
if (!in_array($image_extension, $allowed_extensions)) {
|
||||
error_log("[add_invoice.php] ❌ Invalid image extension: .$image_extension");
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid file type.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mime_type = finfo_file($finfo, $image_file['tmp_name']);
|
||||
finfo_close($finfo);
|
||||
|
||||
$allowed_mime_types = ['image/jpeg', 'image/png', 'image/jpg'];
|
||||
if (!in_array($mime_type, $allowed_mime_types)) {
|
||||
error_log("[add_invoice.php] ❌ Invalid MIME type: $mime_type");
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid file type (MIME mismatch).']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$new_filename = $invoiceNumber . '.' . $image_extension;
|
||||
$target_dir = "invoice_images/";
|
||||
$target_file = $target_dir . $new_filename;
|
||||
|
||||
if (!is_dir($target_dir)) {
|
||||
if (!mkdir($target_dir, 0755, true)) {
|
||||
error_log("[add_invoice.php] ❌ Failed to create directory: $target_dir");
|
||||
}
|
||||
}
|
||||
|
||||
if (!move_uploaded_file($image_file['tmp_name'], $target_file)) {
|
||||
error_log("[add_invoice.php] ❌ Failed to move uploaded file.");
|
||||
echo json_encode(['status' => 'error', 'message' => 'Failed to upload image.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'api.siromove.com';
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
|
||||
$linkImage = "$protocol://$host/siro/Admin/adminUser/invoice_images/" . $new_filename;
|
||||
error_log("[add_invoice.php] ✅ Image uploaded successfully: $linkImage");
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $con->prepare("INSERT INTO invoice_records (driverID, invoice_number, name, amount, date, image_link, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$driverID, $invoiceNumber, $name, $amount, $date, $linkImage, $uploadDate]);
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'Invoice data saved.',
|
||||
'image' => $linkImage
|
||||
]);
|
||||
|
||||
error_log("[add_invoice.php] ✅ Invoice saved successfully.");
|
||||
} catch (PDOException $e) {
|
||||
$errorMsg = $e->getMessage();
|
||||
error_log("[add_invoice.php] 🛑 PDO ERROR: $errorMsg");
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => "Database error occurred: " . $errorMsg()
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$device_number = filterRequest("device_number");
|
||||
|
||||
$sql = "SELECT
|
||||
*
|
||||
FROM
|
||||
`adminUser`
|
||||
WHERE
|
||||
`device_number` = '$device_number'";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute();
|
||||
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (count($result) === 1) {
|
||||
// Print the first record as a success message
|
||||
jsonSuccess($result[0]);
|
||||
} else {
|
||||
// Print a failure message
|
||||
jsonError($message = "Failed to retrieve Password or user name incorrect");
|
||||
}
|
||||
?>
|
||||
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 5.6 MiB |
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
// ✅ استرجاع كل الفواتير من قاعدة البيانات
|
||||
try {
|
||||
$stmt = $con->prepare("SELECT * FROM invoice_records ORDER BY date DESC");
|
||||
$stmt->execute();
|
||||
$invoices = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// ✅ حساب عدد الفواتير ومجموع المبالغ
|
||||
$count = count($invoices);
|
||||
$totalAmount = array_sum(array_column($invoices, 'amount'));
|
||||
|
||||
echo json_encode([
|
||||
"status" => "success",
|
||||
"data" => $invoices,
|
||||
"summary" => [
|
||||
"count" => $count,
|
||||
"total" => $totalAmount
|
||||
]
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode([
|
||||
"status" => "error",
|
||||
"message" => "An internal error occurred"
|
||||
]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$driverID = $_POST['driverID'] ?? 'MISSING';
|
||||
$invoiceNumber = $_POST['invoiceNumber'] ?? 'MISSING';
|
||||
$name = $_POST['name'] ?? 'MISSING';
|
||||
$amount = $_POST['amount'] ?? 'MISSING';
|
||||
$date = $_POST['date'] ?? 'MISSING';
|
||||
$uploadDate = date("Y-m-d H:i:s");
|
||||
|
||||
try {
|
||||
$stmt = $con->prepare("INSERT INTO invoice_records (driverID, invoice_number, name, amount, date, image_link, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$driverID, $invoiceNumber, $name, $amount, $date, null, $uploadDate]);
|
||||
echo json_encode(['status' => 'success', 'message' => 'OK']);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin/auth/approve_admin.php
|
||||
* الموافقة على أو رفض طلبات انضمام المشرفين
|
||||
* مسموح فقط للسوبر أدمن
|
||||
*/
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
if ($role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Forbidden. Super Admin access required.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$targetId = filterRequest('admin_id');
|
||||
$action = filterRequest('action'); // approved, rejected, suspended
|
||||
|
||||
if (empty($targetId) || empty($action)) {
|
||||
jsonError("Admin ID and action are required.");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!in_array($action, ['approved', 'rejected', 'suspended'])) {
|
||||
jsonError("Invalid action.");
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$con = Database::get('main');
|
||||
|
||||
$sql = "UPDATE adminUser SET status = :status, approved_by = :by, approved_at = NOW() WHERE id = :id";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([
|
||||
':status' => $action,
|
||||
':by' => $user_id, // السوبر أدمن الحالي
|
||||
':id' => $targetId
|
||||
]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
printSuccess(null, "Admin status updated to $action.");
|
||||
} else {
|
||||
jsonError("Admin not found or status already updated.");
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("[Approve Admin Error] " . $e->getMessage());
|
||||
jsonError("An internal error occurred. Please try again later.");
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin/auth/list_pending.php
|
||||
* عرض قائمة المشرفين الذين ينتظرون الموافقة
|
||||
* مسموح فقط للسوبر أدمن
|
||||
*/
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
// التحقق من الصلاحيات
|
||||
if ($role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Forbidden. Super Admin access required.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$con = Database::get('main');
|
||||
|
||||
$stmt = $con->prepare("SELECT id, name, phone, created_at FROM adminUser WHERE status = 'pending' ORDER BY created_at DESC");
|
||||
$stmt->execute();
|
||||
$pending = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// فك تشفير الأسماء
|
||||
foreach ($pending as &$admin) {
|
||||
$admin['name'] = $encryptionHelper->decryptData($admin['name']) ?: $admin['name'];
|
||||
}
|
||||
|
||||
printSuccess($pending);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("[List Pending Admins Error] " . $e->getMessage());
|
||||
jsonError("An internal error occurred. Please try again later.");
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin/auth/login.php
|
||||
* تسجيل دخول المشرفين باستخدام البصمة وكلمة المرور ونظام OTP الموحد (Nabeh API)
|
||||
*/
|
||||
require_once __DIR__ . '/../../core/bootstrap.php';
|
||||
require_once __DIR__ . '/../../functions.php';
|
||||
|
||||
// $encryptionHelper is already initialized by bootstrap.php (lines 159, 182)
|
||||
global $encryptionHelper;
|
||||
|
||||
$fingerprint = filterRequest('fingerprint');
|
||||
$password = filterRequest('password');
|
||||
$phone = filterRequest('phone');
|
||||
$audience = filterRequest('aud') ?? 'admin';
|
||||
$isRenewal = filterRequest('is_renewal') === '1';
|
||||
|
||||
if (empty($fingerprint) || empty($password)) {
|
||||
jsonError("Fingerprint and password are required.");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Rate Limiting
|
||||
$rateLimiter = new RateLimiter($redis);
|
||||
$rateLimiter->enforce(RateLimiter::identifier(), 'login');
|
||||
|
||||
// تتبع المحاولات الفاشلة لكل حساب
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// البحث عن المشرف باستخدام بصمة الجهاز (Fingerprint Hash)
|
||||
$fpHash = hash('sha256', $fingerprint);
|
||||
$isTrustedDevice = false;
|
||||
|
||||
// تسجيل محاولة تسجيل الدخول للتدقيق
|
||||
$loginAuditData = [
|
||||
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
'fingerprint_hash' => $fpHash,
|
||||
'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');
|
||||
$stmt = $con->prepare("SELECT * FROM adminUser WHERE fingerprint_hash = :fp LIMIT 1");
|
||||
$stmt->execute([':fp' => $fpHash]);
|
||||
$admin = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($admin) {
|
||||
$isTrustedDevice = true;
|
||||
} else if (!empty($phone)) {
|
||||
// 1. بحث بالـ ID أو الفهارس العمياء (الهاتف / البريد) لضمان السرعة والتوافق مع التشفير المتغير
|
||||
global $blindIndex;
|
||||
$phoneBidx = $blindIndex ? $blindIndex->index('adminUser.phone', $phone) : null;
|
||||
$emailBidx = $blindIndex ? $blindIndex->index('adminUser.email', $phone) : null;
|
||||
|
||||
$sql = "SELECT * FROM adminUser WHERE id = :id";
|
||||
$params = [':id' => $phone];
|
||||
|
||||
if ($phoneBidx) {
|
||||
$sql .= " OR phone_bidx = :phone_bidx";
|
||||
$params[':phone_bidx'] = $phoneBidx;
|
||||
}
|
||||
if ($emailBidx) {
|
||||
$sql .= " OR email_bidx = :email_bidx";
|
||||
$params[':email_bidx'] = $emailBidx;
|
||||
}
|
||||
$sql .= " LIMIT 1";
|
||||
|
||||
$stmtId = $con->prepare($sql);
|
||||
$stmtId->execute($params);
|
||||
$admin = $stmtId->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// 2. إذا لم يتم العثور بالـ ID، نفحص الحقول المشفّرة (email / phone / name) عبر فك التشفير
|
||||
if (!$admin) {
|
||||
$stmtAll = $con->query("SELECT * FROM adminUser");
|
||||
while ($row = $stmtAll->fetch(PDO::FETCH_ASSOC)) {
|
||||
$decPhone = ($encryptionHelper && !empty($row['phone'])) ? $encryptionHelper->decryptData($row['phone']) : $row['phone'];
|
||||
$decEmail = ($encryptionHelper && !empty($row['email'])) ? $encryptionHelper->decryptData($row['email']) : $row['email'];
|
||||
$decName = ($encryptionHelper && !empty($row['name'])) ? $encryptionHelper->decryptData($row['name']) : $row['name'];
|
||||
|
||||
if ($phone === $decPhone || $phone === $decEmail || $phone === $decName || $phone === $row['phone'] || $phone === $row['email']) {
|
||||
$admin = $row;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// فحص ما إذا كانت بصمة الجهاز محفوظة ومطابقة للجهاز الحالي
|
||||
if ($admin && !empty($admin['fingerprint_hash']) && hash_equals($admin['fingerprint_hash'], $fpHash)) {
|
||||
$isTrustedDevice = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($admin) {
|
||||
// 1. التحقق من حالة الحساب
|
||||
if (isset($admin['status'])) {
|
||||
if ($admin['status'] === 'pending') {
|
||||
jsonError("حسابك قيد المراجعة حالياً. يرجى الانتظار للموافقة.");
|
||||
exit;
|
||||
} elseif ($admin['status'] === 'suspended') {
|
||||
jsonError("هذا الحساب معلق. يرجى التواصل مع المدير.");
|
||||
exit;
|
||||
} elseif ($admin['status'] === 'rejected') {
|
||||
jsonError("تم رفض طلب الانضمام لهذا الحساب.");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. التحقق من كلمة المرور
|
||||
if (password_verify($password, $admin['password'])) {
|
||||
|
||||
// إذا كان الجهاز موثوقاً (البصمة محفوظة ومطابقة) أو طلب تجديد توكن تلقائي
|
||||
if ($isTrustedDevice || $isRenewal) {
|
||||
$encFpRaw = $encryptionHelper ? $encryptionHelper->encryptData($fingerprint) : $fingerprint;
|
||||
$updateStmt = $con->prepare("UPDATE adminUser SET fingerprint = :fp_raw, fingerprint_hash = :fp WHERE id = :id");
|
||||
$updateStmt->execute([
|
||||
':fp_raw' => $encFpRaw,
|
||||
':fp' => $fpHash,
|
||||
':id' => $admin['id']
|
||||
]);
|
||||
$admin['fingerprint_hash'] = $fpHash;
|
||||
|
||||
$jwtService = new JwtService($redis);
|
||||
$role = $admin['role'] ?? 'admin';
|
||||
|
||||
if ($redis) {
|
||||
$oldJti = $redis->get("active_jti:" . $admin['id']);
|
||||
if ($oldJti) {
|
||||
$jwtService->revokeToken($oldJti, 3600);
|
||||
}
|
||||
}
|
||||
|
||||
$jwt = $jwtService->generateAccessToken($admin['id'], $role, $audience, $fingerprint);
|
||||
|
||||
if ($encryptionHelper && !empty($admin['name'])) {
|
||||
$admin['name'] = $encryptionHelper->decryptData($admin['name']) ?: $admin['name'];
|
||||
}
|
||||
unset($admin['password']);
|
||||
|
||||
printSuccess([
|
||||
"message" => "Login successful",
|
||||
"admin" => $admin,
|
||||
"jwt" => $jwt,
|
||||
"expires_in" => 3600
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3. توليد رمز تحقق OTP (3 أرقام) وإرساله عبر نظام OTP الموحد (Nabeh API للواتساب)
|
||||
$otp = (string)random_int(100, 999);
|
||||
$encryptedPhone = $admin['phone'] ?? '';
|
||||
|
||||
$rawPhone = ($encryptionHelper && !empty($encryptedPhone)) ? $encryptionHelper->decryptData($encryptedPhone) : $encryptedPhone;
|
||||
if (!$rawPhone || empty($rawPhone)) {
|
||||
$rawPhone = $encryptedPhone;
|
||||
}
|
||||
|
||||
// تحميل موزع خدمات OTP عبر Nabeh API
|
||||
require_once __DIR__ . '/../../auth/otp/providers.php';
|
||||
|
||||
$success = false;
|
||||
if (function_exists('sendNabehOtp')) {
|
||||
$success = sendNabehOtp($rawPhone, $otp, 'whatsapp', 'admin');
|
||||
}
|
||||
|
||||
// تخزين OTP (SHA-256 hash) في جدول token_verification_admin
|
||||
$otpHash = hash('sha256', $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, $otpHash]);
|
||||
|
||||
$maskedPhone = (strlen($rawPhone) > 7) ? substr($rawPhone, 0, 4) . '****' . substr($rawPhone, -3) : $rawPhone;
|
||||
|
||||
printSuccess([
|
||||
"status" => "otp_required",
|
||||
"message" => $success ? "تم إرسال رمز التحقق إلى WhatsApp الخاص بك." : "فشل إرسال واتساب. تحقق من error_log لمعرفة OTP.",
|
||||
"phone" => $maskedPhone
|
||||
]);
|
||||
exit;
|
||||
} else {
|
||||
jsonError("كلمة المرور غير صحيحة.");
|
||||
}
|
||||
} else {
|
||||
jsonError("الحساب غير موجود. يرجى التأكد من اسم المستخدم أو البريد الإلكتروني وكلمة المرور.");
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log("[Admin Login Throwable Error] " . $e->getMessage() . "\nTrace: " . $e->getTraceAsString());
|
||||
jsonError("حدث خطأ في السيرفر: " . $e->getMessage(), 500);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin/auth/loginWallet.php
|
||||
* توليد توكن خاص بسيرفر المحفظة (Wallet SSO)
|
||||
* يتم توقيعه بالمفتاح المشترك (SECRET_KEY_PAY)
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../core/bootstrap.php';
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
|
||||
// التحقق من الجلسة الحالية للأدمن
|
||||
$jwtService = new JwtService($redis ?? null);
|
||||
$admin = $jwtService->authenticate();
|
||||
|
||||
error_log("[Wallet_SSO] Authenticated Admin ID: " . ($admin->user_id ?? 'N/A') . " | Role: " . ($admin->role ?? 'N/A'));
|
||||
|
||||
if ($admin->role !== 'admin' && $admin->role !== 'super_admin') {
|
||||
jsonError("Unauthorized. Admin access required.");
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// جلب المفتاح المشترك لسيرفر المحفظة من متغير البيئة أو الملف
|
||||
$payKeyPath = getenv('SECRET_KEY_PAY_PATH');
|
||||
$payKey = ($payKeyPath && file_exists($payKeyPath)) ? trim(file_get_contents($payKeyPath)) : getenv('SECRET_KEY_PAY');
|
||||
|
||||
if (empty($payKey)) {
|
||||
$fallbackPath = getenv('SECRET_KEY_PATH');
|
||||
$payKey = ($fallbackPath && file_exists($fallbackPath)) ? trim(file_get_contents($fallbackPath)) : null;
|
||||
}
|
||||
|
||||
if (empty($payKey)) {
|
||||
jsonError("Internal configuration error: Shared secret key missing.");
|
||||
exit;
|
||||
}
|
||||
|
||||
$issuer = 'Tripz-Wallet';
|
||||
$audience = 'Tripz-Wallet';
|
||||
$hmacSecret = getenv('SECRET_KEY_HMAC') ?: '';
|
||||
|
||||
$ttl = 600; // 10 دقائق
|
||||
$iat = time();
|
||||
$exp = $iat + $ttl;
|
||||
$jti = bin2hex(random_bytes(16));
|
||||
|
||||
// محتوى التوكن (Payload)
|
||||
$payload = [
|
||||
'iss' => $issuer,
|
||||
'aud' => $audience,
|
||||
'user_id' => $admin->user_id,
|
||||
'role' => 'admin', // نرسل 'admin' للمحفظة لضمان التوافق مع برمجياتها القديمة
|
||||
'iat' => $iat,
|
||||
'exp' => $exp,
|
||||
'jti' => $jti
|
||||
];
|
||||
|
||||
// إلغاء التوكن القديم إذا وجد في Redis
|
||||
if ($redis) {
|
||||
$oldJtiKey = "wallet_jti:" . $admin->user_id;
|
||||
$oldJti = $redis->get($oldJtiKey);
|
||||
if ($oldJti) {
|
||||
// إضافة التوكن القديم للقائمة السوداء
|
||||
$redis->setex("jwt:blacklist:$oldJti", $ttl + 60, '1');
|
||||
}
|
||||
// تخزين الـ JTI الجديد
|
||||
$redis->setex($oldJtiKey, $ttl, $jti);
|
||||
}
|
||||
|
||||
// إضافة بصمة الجهاز للتوكن لزيادة الأمان
|
||||
$fpHeader = $_SERVER['HTTP_X_DEVICE_FP'] ?? null;
|
||||
$fpPepper = getenv('FP_PEPPER');
|
||||
if ($fpHeader && $fpPepper) {
|
||||
$payload['fingerPrint'] = hash('sha256', $fpHeader . $fpPepper);
|
||||
}
|
||||
|
||||
// توليد التوكن
|
||||
$jwt = JWT::encode($payload, $payKey, 'HS256');
|
||||
|
||||
// حساب الـ HMAC Hash المطلوب لسيرفر المحفظة
|
||||
$hmacHash = hash_hmac('sha256', (string)$admin->user_id, $hmacSecret);
|
||||
|
||||
printSuccess([
|
||||
"status" => "success",
|
||||
"jwt" => $jwt,
|
||||
"hmac" => $hmacHash,
|
||||
"expires_in" => $ttl
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("[Admin Wallet SSO Error] " . $e->getMessage());
|
||||
jsonError("An internal error occurred. Please try again later.");
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin/auth/register.php
|
||||
* التسجيل الذاتي للمشرفين (Admins) مع التحقق من الصلاحيات من ملف .env
|
||||
*/
|
||||
require_once __DIR__ . '/../../core/bootstrap.php';
|
||||
require_once __DIR__ . '/../../functions.php';
|
||||
|
||||
$name = filterRequest('name');
|
||||
$phone = filterRequest('phone');
|
||||
$password = filterRequest('password');
|
||||
$fingerprint = filterRequest('fingerprint');
|
||||
|
||||
if (empty($name) || empty($phone) || empty($password) || empty($fingerprint)) {
|
||||
jsonError("جميع الحقول مطلوبة بما فيها بصمة الجهاز.");
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. التحقق من البيئة (Environment Whitelist)
|
||||
$allowedPhonesStr = getenv('AUTHORIZED_ADMIN_PHONES');
|
||||
if (!$allowedPhonesStr) {
|
||||
// في حال لم يتم إعداد المتغير، نرفض الجميع للأمان
|
||||
jsonError("غير مصرح لك بالتسجيل كمشرف (القائمة البيضاء غير معدة).");
|
||||
exit;
|
||||
}
|
||||
|
||||
$allowedPhones = array_map('trim', explode(',', $allowedPhonesStr));
|
||||
if (!in_array($phone, $allowedPhones)) {
|
||||
jsonError("أنت غير مصرح لك بالتسجيل كمشرف. يرجى مراجعة الإدارة.");
|
||||
exit;
|
||||
}
|
||||
|
||||
$con = Database::get('main');
|
||||
|
||||
// 2. التحقق من عدم وجود الحساب مسبقاً (عن طريق الهاتف أو البصمة)
|
||||
$fpHash = hash('sha256', $fingerprint);
|
||||
$encPhoneInput = $encryptionHelper->encryptData($phone);
|
||||
|
||||
$check = $con->prepare("SELECT id FROM adminUser WHERE phone = ? OR fingerprint_hash = ? LIMIT 1");
|
||||
$check->execute([$encPhoneInput, $fpHash]);
|
||||
|
||||
if ($check->rowCount() > 0) {
|
||||
jsonError("رقم الهاتف أو الجهاز مسجل مسبقاً.");
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3. تجهيز البيانات
|
||||
$uniqueId = bin2hex(random_bytes(16)); // UUID آمن (32 حرف hex عشوائي)
|
||||
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
$encName = $encryptionHelper->encryptData($name);
|
||||
$encPhone = $encPhoneInput;
|
||||
$encFp = $encryptionHelper->encryptData($fingerprint);
|
||||
|
||||
// 4. الإدخال في قاعدة البيانات بحالة pending
|
||||
$sql = "INSERT INTO adminUser (id, fingerprint, fingerprint_hash, name, phone, password, role, status, created_at)
|
||||
VALUES (:id, :fp, :fp_hash, :name, :phone, :pass, 'admin', 'pending', NOW())";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([
|
||||
':id' => $uniqueId,
|
||||
':fp' => $encFp,
|
||||
':fp_hash' => $fpHash,
|
||||
':name' => $encName,
|
||||
':phone' => $encPhone,
|
||||
':pass' => $hashedPassword
|
||||
]);
|
||||
|
||||
printSuccess([
|
||||
"status" => "pending",
|
||||
"message" => "تم تسجيل حسابك بنجاح وهو الآن قيد المراجعة. يرجى انتظار تفعيل المشرف العام."
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("[Admin Register Error] " . $e->getMessage());
|
||||
jsonError("An internal error occurred. Please try again later.");
|
||||
}
|
||||
|
||||
exit();
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
// send_otp_admin.php — إرسال رمز التحقق لمسؤول عبر WhatsApp
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
error_log("--- [send_otp_admin] Script started ---");
|
||||
|
||||
// جلب الرقم من الطلب
|
||||
$receiver = filterRequest("receiver");
|
||||
//error_log("[send_otp_admin] Received phone number: " . var_export($receiver, true));
|
||||
|
||||
if (!$receiver) {
|
||||
// error_log("[send_otp_admin] Missing phone number");
|
||||
jsonError("رقم الهاتف مفقود.");
|
||||
exit;
|
||||
}
|
||||
|
||||
// قراءة الأرقام المصرح بها من ENV
|
||||
$allowedPhones = explode(',', getenv('ADMIN_PHONE_NUMBERS'));
|
||||
//error_log("[send_otp_admin] Allowed phones: " . implode(', ', $allowedPhones));
|
||||
|
||||
if (!in_array($receiver, $allowedPhones)) {
|
||||
error_log("[send_otp_admin] Unauthorized phone number attempted: $receiver");
|
||||
jsonError("رقم الهاتف غير مصرح له.");
|
||||
exit;
|
||||
}
|
||||
|
||||
// توليد رمز تحقق عشوائي
|
||||
$otp = rand(10000, 99999);
|
||||
$messageBody = "رمز التحقق الخاص بك للدخول إلى لوحة الإدارة هو: $otp";
|
||||
//error_log("[send_otp_admin] Generated OTP: $otp for $receiver");
|
||||
|
||||
// إرسال الرسالة عبر WhatsApp
|
||||
$success = sendWhatsAppFromServer($receiver, $messageBody);
|
||||
error_log("[send_otp_admin] WhatsApp sending result: " . ($success ? "success" : "failure"));
|
||||
|
||||
if ($success) {
|
||||
try {
|
||||
$stmt = $con->prepare("INSERT INTO token_verification_admin (phone_number, token, expiration_time)
|
||||
VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 5 MINUTE))
|
||||
ON DUPLICATE KEY UPDATE token = VALUES(token), expiration_time = VALUES(expiration_time)");
|
||||
$stmt->execute([$receiver, $otp]);
|
||||
// error_log("[send_otp_admin] OTP saved to database successfully for $receiver");
|
||||
|
||||
jsonSuccess(null, "OTP sent successfully.");
|
||||
} catch (PDOException $e) {
|
||||
// error_log("[send_otp_admin] Database error: " . $e->getMessage());
|
||||
jsonError("حدث خطأ في حفظ الرمز.");
|
||||
}
|
||||
} else {
|
||||
// error_log("[send_otp_admin] Failed to send WhatsApp message to $receiver");
|
||||
jsonError("فشل في إرسال الرمز عبر WhatsApp.");
|
||||
}
|
||||
|
||||
//error_log("--- [send_otp_admin] Script ended ---");
|
||||
?>
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin/auth/verify_login.php
|
||||
* الخطوة الثانية من تسجيل الدخول: التحقق من الـ OTP وإصدار التوكن النهائي
|
||||
*/
|
||||
require_once __DIR__ . '/../../core/bootstrap.php';
|
||||
require_once __DIR__ . '/../../functions.php';
|
||||
|
||||
$otp = filterRequest('otp');
|
||||
$fingerprint = filterRequest('fingerprint'); // مطلوب لربط التوكن بالجهاز
|
||||
$audience = filterRequest('aud') ?? 'admin';
|
||||
|
||||
if (empty($otp) || empty($fingerprint)) {
|
||||
jsonError("OTP and fingerprint are required.");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Rate Limiting: 3 محاولات OTP في 5 دقائق لكل IP
|
||||
$rateLimiter = new RateLimiter($redis);
|
||||
$rateLimiter->enforce(RateLimiter::identifier(), 'otp');
|
||||
|
||||
try {
|
||||
$con = Database::get('main');
|
||||
|
||||
// 1. جلب بيانات المسؤول عبر البصمة أو من الـ OTP المعلق للجهاز الجديد
|
||||
$fpHash = hash('sha256', $fingerprint);
|
||||
$stmt = $con->prepare("SELECT * FROM adminUser WHERE fingerprint_hash = :fp LIMIT 1");
|
||||
$stmt->execute([':fp' => $fpHash]);
|
||||
$admin = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$otpHash = hash('sha256', (string)$otp);
|
||||
|
||||
if (!$admin) {
|
||||
// إذا كانت البصمة جديدة وغير مسجلة بعد، نبحث عن الحساب المرتبط بـ OTP المعلق
|
||||
$stmtOtp = $con->prepare("SELECT phone_number FROM token_verification_admin WHERE token = ? AND expiration_time >= NOW() LIMIT 1");
|
||||
$stmtOtp->execute([$otpHash]);
|
||||
$otpRow = $stmtOtp->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($otpRow && !empty($otpRow['phone_number'])) {
|
||||
// $targetPhone هو الرقم المشفر من token_verification_admin (نفس القيمة المخزنة في adminUser.phone)
|
||||
$targetPhone = $otpRow['phone_number'];
|
||||
global $encryptionHelper;
|
||||
|
||||
// البحث المباشر: phone المشفر مطابق لنفس النص المشفر في adminUser.phone
|
||||
$stmtAdmin = $con->prepare("SELECT * FROM adminUser WHERE phone = :p1 OR id = :p2 LIMIT 1");
|
||||
$stmtAdmin->execute([':p1' => $targetPhone, ':p2' => $targetPhone]);
|
||||
$admin = $stmtAdmin->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// مسار احتياطي: فك التشفير لمقارنة القيم (ضروري لـ AES-GCM حيث التشفير غير حتمي)
|
||||
if (!$admin) {
|
||||
$decTarget = ($encryptionHelper && !empty($targetPhone)) ? $encryptionHelper->decryptData($targetPhone) : null;
|
||||
$stmtAll = $con->query("SELECT * FROM adminUser");
|
||||
while ($row = $stmtAll->fetch(PDO::FETCH_ASSOC)) {
|
||||
// مقارنة مباشرة للنصوص المشفرة (نفس ciphertext)
|
||||
if ($targetPhone === $row['phone']) {
|
||||
$admin = $row;
|
||||
break;
|
||||
}
|
||||
// مقارنة عبر فك التشفير (AES-GCM: ciphertexts مختلفة لنفس النص)
|
||||
if ($decTarget) {
|
||||
$decPhone = ($encryptionHelper && !empty($row['phone'])) ? $encryptionHelper->decryptData($row['phone']) : $row['phone'];
|
||||
if ($decTarget === $decPhone) {
|
||||
$admin = $row;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$admin) {
|
||||
jsonError("المسؤول غير موجود أو رمز التحقق غير صالح.");
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. رقم الهاتف المشفر (للاستخدام في جدول OTP)
|
||||
$encryptedPhone = $admin['phone'] ?? '';
|
||||
|
||||
// فك تشفيره لو احتجنا إرساله أو عرضه، لكن هنا نحن نحتاج المشفر للبحث
|
||||
// $phone = $encryptionHelper->decryptData($encryptedPhone);
|
||||
|
||||
// 3. التحقق من الـ OTP (الهاش محسوب مسبقاً في المتغير $otpHash)
|
||||
$stmt = $con->prepare("SELECT * FROM token_verification_admin
|
||||
WHERE phone_number = ? AND token = ?
|
||||
AND expiration_time >= NOW()");
|
||||
$stmt->execute([$encryptedPhone, $otpHash]);
|
||||
|
||||
if ($stmt->rowCount() === 0) {
|
||||
jsonError("رمز التحقق غير صالح أو منتهي الصلاحية.");
|
||||
exit;
|
||||
}
|
||||
|
||||
// حذف الرمز بعد استخدامه لمرة واحدة (باستخدام الرقم المشفر)
|
||||
$con->prepare("DELETE FROM token_verification_admin WHERE phone_number = ?")->execute([$encryptedPhone]);
|
||||
|
||||
// 4. تحديث وتأكيد بصمة المتصفح/الجهاز الحالية للمسؤول في قاعدة البيانات بعد التحقق الناجح من OTP
|
||||
$encFpRaw = ($encryptionHelper && !empty($fingerprint)) ? $encryptionHelper->encryptData($fingerprint) : $fingerprint;
|
||||
$updateFpStmt = $con->prepare("UPDATE adminUser SET fingerprint = :fp_raw, fingerprint_hash = :fp WHERE id = :id");
|
||||
$updateFpStmt->execute([
|
||||
':fp_raw' => $encFpRaw,
|
||||
':fp' => $fpHash,
|
||||
':id' => $admin['id']
|
||||
]);
|
||||
$admin['fingerprint_hash'] = $fpHash;
|
||||
|
||||
// 5. إصدار التوكن النهائي
|
||||
$jwtService = new JwtService($redis);
|
||||
$role = $admin['role'] ?? 'admin';
|
||||
|
||||
// إلغاء التوكن القديم إذا وجد في Redis (Token Revocation)
|
||||
if ($redis) {
|
||||
$oldJti = $redis->get("active_jti:" . $admin['id']);
|
||||
if ($oldJti) {
|
||||
$jwtService->revokeToken($oldJti, 3600);
|
||||
}
|
||||
}
|
||||
|
||||
$jwt = $jwtService->generateAccessToken($admin['id'], $role, $audience, $fingerprint);
|
||||
|
||||
// فك تشفير البيانات للعرض
|
||||
if ($encryptionHelper && !empty($admin['name'])) {
|
||||
$admin['name'] = $encryptionHelper->decryptData($admin['name']) ?: $admin['name'];
|
||||
}
|
||||
unset($admin['password']);
|
||||
|
||||
printSuccess([
|
||||
"message" => "Login successful",
|
||||
"admin" => $admin,
|
||||
"jwt" => $jwt,
|
||||
"expires_in" => 3600
|
||||
]);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log("[Admin Verify OTP Error] " . $e->getMessage() . "\nTrace: " . $e->getTraceAsString());
|
||||
jsonError("Server Error: " . $e->getMessage() . " on line " . $e->getLine());
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$phone = filterRequest("phone_number");
|
||||
$otp = filterRequest("otp");
|
||||
$deviceNumber = filterRequest("device_number");
|
||||
|
||||
if (empty($phone) || empty($otp)) {
|
||||
jsonError("رقم الهاتف أو رمز التحقق مفقود.");
|
||||
exit;
|
||||
}
|
||||
|
||||
// التحقق من رمز التحقق (OTP)
|
||||
$stmt = $con->prepare("SELECT * FROM token_verification_admin
|
||||
WHERE phone_number = ? AND token = ?
|
||||
AND expiration_time >= NOW()");
|
||||
$stmt->execute([$phone, $otp]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
// ✅ تحقق ناجح - ننتقل إلى إدخال أو تحديث سجل adminUser
|
||||
|
||||
// تحقق إن كان المستخدم موجود مسبقًا
|
||||
$checkAdmin = $con->prepare("SELECT * FROM adminUser WHERE name = ?");
|
||||
$checkAdmin->execute([$phone]);
|
||||
|
||||
$now = date("Y-m-d H:i:s");
|
||||
|
||||
if ($checkAdmin->rowCount() > 0) {
|
||||
// المستخدم موجود ✅ تحديث device_number و updated_at
|
||||
$update = $con->prepare("UPDATE adminUser
|
||||
SET device_number = ?, updated_at = ?
|
||||
WHERE name = ?");
|
||||
$update->execute([$deviceNumber, $now, $phone]);
|
||||
jsonSuccess(["message" => "verified and updated existing admin"]);
|
||||
} else {
|
||||
// المستخدم غير موجود ✅ إدخال جديد
|
||||
$insert = $con->prepare("INSERT INTO adminUser (device_number, name, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?)");
|
||||
$insert->execute([$deviceNumber, $phone, $now, $now]);
|
||||
jsonSuccess(["message" => "verified and new admin created"]);
|
||||
}
|
||||
|
||||
} else {
|
||||
// ❌ رمز التحقق غير صالح
|
||||
jsonError("رمز التحقق غير صالح أو منتهي.");
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../connect.php';
|
||||
|
||||
// التحقق من الصلاحيات: مسموح فقط للأدمن والسوبر أدمن
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Unauthorized access. Admin role required.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sql = "
|
||||
SELECT
|
||||
-- العدادات العامة
|
||||
(SELECT COUNT(*) FROM passengers) AS countPassengers,
|
||||
(SELECT COUNT(*) FROM driver) AS countDriver,
|
||||
(SELECT COUNT(*) FROM ride) AS countRide,
|
||||
|
||||
-- إحصائيات الشهر الحالي
|
||||
(SELECT COUNT(*) FROM passengers WHERE created_at BETWEEN DATE_FORMAT(CURDATE(), '%Y-%m-01') AND LAST_DAY(CURDATE())) AS countPassengersThisMonth,
|
||||
(SELECT COUNT(*) FROM driver WHERE created_at BETWEEN DATE_FORMAT(CURDATE(), '%Y-%m-01') AND LAST_DAY(CURDATE())) AS countDriverThisMonth,
|
||||
(SELECT COUNT(*) FROM ride WHERE created_at BETWEEN DATE_FORMAT(CURDATE(), '%Y-%m-01') AND LAST_DAY(CURDATE())) AS countRideThisMonth,
|
||||
(SELECT COUNT(*) FROM CarRegistration WHERE created_at BETWEEN DATE_FORMAT(CURDATE(), '%Y-%m-01') AND LAST_DAY(CURDATE())) AS countCarRegistrationThisMonth,
|
||||
|
||||
-- شكاوى
|
||||
(SELECT COUNT(*) FROM complaint WHERE date_filed BETWEEN DATE_FORMAT(CURDATE(), '%Y-%m-01') AND LAST_DAY(CURDATE())) AS countComplaintThisMonth,
|
||||
(SELECT COUNT(*) FROM complaint WHERE date_filed BETWEEN DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY) AND DATE_ADD(DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY), INTERVAL 6 DAY)) AS countComplaintThisWeek,
|
||||
(SELECT COUNT(*) FROM complaint WHERE DATE(date_filed) = CURDATE()) AS countComplaintToday,
|
||||
|
||||
-- المحافظ والتحويلات
|
||||
|
||||
-- إحصائيات وقت ومسافة الرحلات
|
||||
-- تُستثنى الفروق السالبة (رحلات سجّلت وقت نهاية أقدم من البداية) لأنها
|
||||
-- كانت تُنتج متوسط مدة سالباً.
|
||||
(SELECT TIME_FORMAT(SEC_TO_TIME(AVG(TIMESTAMPDIFF(SECOND, rideTimeStart, rideTimeFinish))), '%Hh %im') FROM ride WHERE rideTimeStart IS NOT NULL AND rideTimeFinish IS NOT NULL AND TIMESTAMPDIFF(SECOND, rideTimeStart, rideTimeFinish) > 0) AS driver_avg_duration,
|
||||
(SELECT MAX(SEC_TO_TIME(TIMESTAMPDIFF(SECOND, rideTimeStart, rideTimeFinish))) FROM ride WHERE rideTimeStart IS NOT NULL AND rideTimeFinish IS NOT NULL) AS longest_duration,
|
||||
(SELECT ROUND(SUM(distance),2) FROM ride) AS total_distance,
|
||||
(SELECT ROUND(AVG(distance),2) FROM ride) AS average_distance,
|
||||
(SELECT ROUND(MAX(distance),2) FROM ride) AS longest_distance,
|
||||
|
||||
-- أرباح السائق والشركة
|
||||
-- ملاحظة: خط الرحلات الحالي يكتب 'completed' بينما القديم يكتب 'Finished'،
|
||||
-- والاكتفاء بالقديم كان يُرجع NULL للأرباح وصفراً للرحلات المكتملة/الملغاة.
|
||||
(SELECT SUM(price_for_driver) FROM ride WHERE LOWER(status) IN ('finished','completed')) AS total_driver_earnings,
|
||||
(SELECT ROUND(AVG(price_for_passenger),2) FROM ride) AS avg_passenger_price,
|
||||
|
||||
-- توزيع الرحلات حسب الوقت
|
||||
(SELECT COUNT(*) FROM ride WHERE HOUR(created_at) BETWEEN 6 AND 11) AS morning_ride_count,
|
||||
(SELECT COUNT(*) FROM ride WHERE HOUR(created_at) BETWEEN 12 AND 17) AS evening_ride_count,
|
||||
(SELECT COUNT(*) FROM ride WHERE HOUR(created_at) BETWEEN 18 AND 23 OR HOUR(created_at) BETWEEN 0 AND 5) AS night_ride_count,
|
||||
|
||||
-- أنواع الرحلات
|
||||
(SELECT COUNT(*) FROM ride WHERE carType = 'Comfort') AS comfort,
|
||||
(SELECT COUNT(*) FROM ride WHERE carType = 'Speed') AS speed,
|
||||
(SELECT COUNT(*) FROM ride WHERE carType = 'Lady') AS lady,
|
||||
|
||||
-- حالة الرحلات (تغطي عائلتي الحالات: القديمة CamelCase والجديدة lowercase)
|
||||
(SELECT COUNT(*) FROM ride WHERE LOWER(status) IN ('wait','waiting','new','nothing','pending','searching')) AS ongoing_rides,
|
||||
(SELECT COUNT(*) FROM ride WHERE LOWER(status) IN ('finished','completed')) AS completed_rides,
|
||||
(SELECT COUNT(*) FROM ride WHERE LOWER(status) LIKE 'cancel%' OR LOWER(status) IN ('timeout','refused')) AS cancelled_rides,
|
||||
|
||||
-- عدد السائقين الفريدين
|
||||
(SELECT COUNT(*) FROM (SELECT driver_id FROM ride GROUP BY driver_id) AS sub) AS num_Driver,
|
||||
|
||||
-- التحويلات البنكية
|
||||
0 AS transfer_from_count
|
||||
";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute();
|
||||
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($result) {
|
||||
jsonSuccess($result);
|
||||
} else {
|
||||
jsonError("No dashboard data found");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
// حارس الصلاحيات: هذه النقطة تحذف سجلاً نهائياً من قاعدة البيانات.
|
||||
// connect.php يتحقق من صحة التوكن فقط، فبدون هذا الفحص كان أي توكن صالح
|
||||
// (سائق أو راكب) قادراً على حذف السائقين.
|
||||
if ($role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode([
|
||||
'status' => 'failure',
|
||||
'message' => 'Forbidden. Super Admin access required.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$driver_id = filterRequest("driver_id");
|
||||
$phone = filterRequest("phone");
|
||||
$reason = filterRequest("reason"); // يمكن أن يأتي من البارامتر أو نخليه افتراضي
|
||||
|
||||
if (empty($driver_id) || empty($phone)) {
|
||||
jsonError("Driver ID and phone are required.");
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// تشفير رقم الهاتف
|
||||
$encPhone = $encryptionHelper->encryptData($phone);
|
||||
|
||||
// حذف السائق من جدول driver
|
||||
$sqlDel = "DELETE FROM driver WHERE id = :id";
|
||||
$stmtDel = $con->prepare($sqlDel);
|
||||
$stmtDel->bindParam(':id', $driver_id, PDO::PARAM_INT);
|
||||
$stmtDel->execute();
|
||||
|
||||
if ($stmtDel->rowCount() > 0) {
|
||||
// إضافة بيانات السائق المحذوف إلى البلاك ليست
|
||||
$sqlInsert = "INSERT INTO blacklist_driver (driver_id, phone, reason)
|
||||
VALUES (:driver_id, :phone, :reason)";
|
||||
$stmtInsert = $con->prepare($sqlInsert);
|
||||
$stmtInsert->execute([
|
||||
'driver_id' => $driver_id,
|
||||
'phone' => $encPhone,
|
||||
'reason' => !empty($reason) ? $reason : "Deleted & blacklisted by admin"
|
||||
]);
|
||||
|
||||
jsonSuccess(null, "Driver deleted and blacklisted successfully.");
|
||||
} else {
|
||||
jsonError("No driver found with the provided ID.");
|
||||
}
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_log("[deleteCaptain.php] " . $e->getMessage());
|
||||
jsonError("An internal error occurred. Please try again later.");
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$driver_id = filterRequest("driver_id");
|
||||
|
||||
// Prepare the DELETE query
|
||||
$sql = "DELETE FROM `car_locations` WHERE driver_id = :driver_id";
|
||||
$stmt = $con->prepare($sql);
|
||||
|
||||
// Bind the driver_id parameter
|
||||
$stmt->bindParam(':driver_id', $driver_id, PDO::PARAM_STR);
|
||||
|
||||
try {
|
||||
// Execute the query
|
||||
$stmt->execute();
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
// Success response
|
||||
jsonSuccess(null, "Record(s) deleted successfully.");
|
||||
} else {
|
||||
// Failure response: no records found to delete
|
||||
jsonError("No records found for the provided driver ID.");
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
// Handle any SQL errors
|
||||
jsonError("An internal error occurred. Please try again later.");
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$phone = filterRequest("phone");
|
||||
|
||||
if (empty($phone)) {
|
||||
jsonError("Phone number is required.");
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
/**
|
||||
* البحث عبر الفهرس الأعمى أولاً (phone_bidx): مطابقة تامة عبر فهرس مُهيأ
|
||||
* ولا تعتمد على كون التشفير حتمياً، فتظل تعمل بعد النقل إلى AES-GCM.
|
||||
*
|
||||
* يُبقى المسار القديم (مقارنة النص المشفّر) كاحتياط حتى ينتهي تشغيل
|
||||
* scripts/backfill_blind_index.php، وإلا لتوقّف البحث بين الترحيل والتعبئة.
|
||||
*/
|
||||
global $blindIndex;
|
||||
$driver = null;
|
||||
|
||||
if ($blindIndex) {
|
||||
$bidx = $blindIndex->index('driver.phone', $phone);
|
||||
if ($bidx) {
|
||||
$stmt = $con->prepare("SELECT * FROM driver WHERE phone_bidx = :bidx LIMIT 1");
|
||||
$stmt->execute([':bidx' => $bidx]);
|
||||
$driver = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$driver) {
|
||||
$encPhone = $encryptionHelper->encryptData($phone);
|
||||
$stmt = $con->prepare("SELECT * FROM driver WHERE phone = :phone LIMIT 1");
|
||||
$stmt->execute([':phone' => $encPhone]);
|
||||
}
|
||||
|
||||
if (!$driver) {
|
||||
$driver = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
if ($driver) {
|
||||
// ✅ الحقول المشفرة اللي لازم تنفك:
|
||||
$encryptedFields = [
|
||||
'phone',
|
||||
'email',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'national_number',
|
||||
'address','gender','site',
|
||||
'birthdate',
|
||||
'name_arabic',
|
||||
];
|
||||
|
||||
foreach ($encryptedFields as $field) {
|
||||
if (!empty($driver[$field])) {
|
||||
$driver[$field] = $encryptionHelper->decryptData($driver[$field]);
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ احذف كلمة المرور من النتيجة
|
||||
unset($driver['password']);
|
||||
|
||||
jsonSuccess($driver);
|
||||
|
||||
} else {
|
||||
jsonError("No driver found with this phone.");
|
||||
}
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_log("[find_driver_by_phone.php] " . $e->getMessage());
|
||||
jsonError("An internal error occurred. Please try again later.");
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$sql = "SELECT
|
||||
COUNT(`car_locations`.driver_id) AS driver_count,
|
||||
driver.id,
|
||||
driver.phone,
|
||||
driver.name_arabic,
|
||||
MAX(dt.token) AS token
|
||||
FROM
|
||||
`car_locations`
|
||||
LEFT JOIN driver ON driver.id = car_locations.driver_id
|
||||
LEFT JOIN driverToken dt ON dt.captain_id = driver.id
|
||||
WHERE
|
||||
`car_locations`.created_at > TIMESTAMP(DATE_SUB(NOW(), INTERVAL 7 DAY))
|
||||
GROUP BY
|
||||
driver.id
|
||||
ORDER BY
|
||||
driver_count DESC
|
||||
LIMIT 19;
|
||||
";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute();
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// فك التشفير للحقول الحساسة
|
||||
foreach ($rows as &$row) {
|
||||
if (!empty($row['phone'])) {
|
||||
$row['phone'] = $encryptionHelper->decryptData($row['phone']);
|
||||
}
|
||||
if (!empty($row['name_arabic'])) {
|
||||
$row['name_arabic'] = $encryptionHelper->decryptData($row['name_arabic']);
|
||||
}
|
||||
if (!empty($row['token'])) {
|
||||
$row['token'] = $encryptionHelper->decryptData($row['token']);
|
||||
}
|
||||
}
|
||||
|
||||
jsonSuccess($rows);
|
||||
} else {
|
||||
jsonError($message = "No recent driver location activity found");
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$phone = filterRequest("phone");
|
||||
|
||||
// تنظيف الرقم من أي مسافات أو رموز زائدة
|
||||
$phone = preg_replace('/[^0-9]/', '', $phone);
|
||||
|
||||
// احتمالات الرقم (بالصفر الدولي أو بدونه)
|
||||
$phoneVariants = [];
|
||||
$phoneVariants[] = $phone; // كما هو (مثلاً 0992952235)
|
||||
|
||||
if (str_starts_with($phone, '0')) {
|
||||
$phoneVariants[] = '963' . substr($phone, 1); // تحويل 09 إلى 9639
|
||||
} elseif (str_starts_with($phone, '963')) {
|
||||
$phoneVariants[] = '0' . substr($phone, 3); // تحويل 9639 إلى 09
|
||||
}
|
||||
|
||||
// Encrypt each variant to see if any match the encrypted column
|
||||
$encVariants = [];
|
||||
foreach ($phoneVariants as $v) {
|
||||
$encVariants[] = $encryptionHelper->encryptData($v);
|
||||
}
|
||||
|
||||
error_log("[GIFT_CHECK] Received Phone: " . $phone);
|
||||
error_log("[GIFT_CHECK] Variants: " . implode(', ', $phoneVariants));
|
||||
|
||||
// بناء استعلام يبحث عن كل الاحتمالات (المشفرة وغير المشفرة)
|
||||
$placeholders = [];
|
||||
$params = [];
|
||||
|
||||
foreach ($encVariants as $i => $ev) {
|
||||
$placeholders[] = "phone = :enc$i";
|
||||
$params[":enc$i"] = $ev;
|
||||
}
|
||||
foreach ($phoneVariants as $i => $pv) {
|
||||
$placeholders[] = "phone = :raw$i";
|
||||
$params[":raw$i"] = $pv;
|
||||
}
|
||||
|
||||
$sql = "SELECT * FROM `driver` WHERE " . implode(" OR ", $placeholders);
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
foreach ($params as $key => $val) {
|
||||
$stmt->bindValue($key, $val);
|
||||
}
|
||||
|
||||
$stmt->execute();
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Decrypt sensitive fields
|
||||
foreach ($rows as &$row) {
|
||||
if (!empty($row['phone'])) {
|
||||
$row['phone'] = $encryptionHelper->decryptData($row['phone']);
|
||||
}
|
||||
if (!empty($row['name_arabic'])) {
|
||||
$row['name_arabic'] = $encryptionHelper->decryptData($row['name_arabic']);
|
||||
}
|
||||
}
|
||||
|
||||
jsonSuccess($rows);
|
||||
|
||||
} else {
|
||||
jsonError("No recent driver location activity found");
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
// حارس الصلاحيات: رفع الحظر عملية إدارية، وكانت هذه النقطة بلا أي فحص دور.
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode([
|
||||
'status' => 'failure',
|
||||
'message' => 'Forbidden. Admin access required.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$phone = filterRequest("phone");
|
||||
|
||||
if (empty($phone)) {
|
||||
jsonError("Phone number is required.");
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// تشفير الرقم للمطابقة مع المخزن
|
||||
$encPhone = $encryptionHelper->encryptData($phone);
|
||||
|
||||
$sql = "DELETE FROM blacklist_driver WHERE phone = :phone";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([':phone' => $encPhone]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
jsonSuccess(null, "Driver removed from blacklist successfully.");
|
||||
} else {
|
||||
jsonError("No driver found in blacklist with this phone.");
|
||||
}
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_log("[remove_from_blacklist.php] " . $e->getMessage());
|
||||
jsonError("An internal error occurred. Please try again later.");
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
// 🔥 [Fix Broken Access Control] كان يتحقق من صلاحية التوكن فقط — أي مستخدم
|
||||
// مسجّل دخول كان يقدر يغيّر حالة أي سائق (تفعيل/رفض) أو رقم هاتفه.
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Unauthorized access. Admin role required.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$driver_id = filterRequest("id");
|
||||
$phone = filterRequest("phone");
|
||||
$status = filterRequest("status");
|
||||
|
||||
if (empty($driver_id)) {
|
||||
jsonError("Driver ID is required.");
|
||||
}
|
||||
|
||||
$updateFields = [];
|
||||
$params = [':id' => $driver_id];
|
||||
|
||||
if ($phone !== null && $phone !== '') {
|
||||
$encphone = $encryptionHelper->encryptData($phone);
|
||||
$updateFields[] = "`phone` = :phone";
|
||||
$params[':phone'] = $encphone;
|
||||
|
||||
// الفهرس يُحدَّث مع الرقم نفسه حتى لا يشير إلى القيمة القديمة
|
||||
global $blindIndex;
|
||||
if ($blindIndex) {
|
||||
$updateFields[] = "`phone_bidx` = :phone_bidx";
|
||||
$params[':phone_bidx'] = $blindIndex->index('driver.phone', $phone);
|
||||
}
|
||||
}
|
||||
|
||||
if ($status !== null && $status !== '') {
|
||||
$updateFields[] = "`status` = :status";
|
||||
$params[':status'] = $status;
|
||||
}
|
||||
|
||||
if (empty($updateFields)) {
|
||||
jsonError("No parameters provided for update.");
|
||||
}
|
||||
|
||||
$sql = "UPDATE `driver` SET " . implode(", ", $updateFields) . " WHERE `id` = :id";
|
||||
$stmt = $con->prepare($sql);
|
||||
|
||||
try {
|
||||
$stmt->execute($params);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
logAudit($con, $user_id, "تعديل بيانات سائق من لوحة التحكم", "driver", $driver_id, [
|
||||
"phone" => $phone,
|
||||
"status" => $status
|
||||
]);
|
||||
|
||||
// إذا تم تفعيل السائق، نرسل له رسالة ترحيبية عبر الواتساب لتأكيد التفعيل
|
||||
if ($status === 'active' || $status === 'actives') {
|
||||
// جلب معلومات السائق لإرسال الرسالة
|
||||
$selectSql = "SELECT `phone`, `first_name` FROM `driver` WHERE `id` = :id";
|
||||
$selectStmt = $con->prepare($selectSql);
|
||||
$selectStmt->execute([':id' => $driver_id]);
|
||||
$driverData = $selectStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($driverData) {
|
||||
$decryptedPhone = $encryptionHelper->decryptData($driverData['phone']);
|
||||
$firstName = $encryptionHelper->decryptData($driverData['first_name']);
|
||||
|
||||
$supportPhones = ['0952475740', '0952475742'];
|
||||
$randomIndex = array_rand($supportPhones);
|
||||
$phoneToUse = $supportPhones[$randomIndex];
|
||||
$randomNumber = rand(1000, 999999);
|
||||
|
||||
$messageBody = "أهلاً وسهلاً كابتن $firstName 👋\n"
|
||||
. "تم تفعيل حسابك على تطبيق *سيرو*.\n"
|
||||
. "يمكنك الآن تسجيل الدخول والبدء بالعمل مباشرة.\n"
|
||||
. "للمساعدة تواصل معنا على الرقم: $phoneToUse\n"
|
||||
. "نتمنى لك عمل موفق 🚖\n\n"
|
||||
. "معرف الرسالة: $randomNumber";
|
||||
|
||||
sendWhatsAppFromServer($decryptedPhone, $messageBody);
|
||||
}
|
||||
}
|
||||
|
||||
jsonSuccess(null, "Driver updated successfully.");
|
||||
} else {
|
||||
jsonError("No records updated or driver not found.");
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log("[updateDriverFromAdmin.php] " . $e->getMessage());
|
||||
jsonError("An internal error occurred. Please try again later.");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
// Get the data from the request
|
||||
$name = filterRequest("name");
|
||||
$education = filterRequest("education");
|
||||
$site = filterRequest("site");
|
||||
$phone = filterRequest("phone");
|
||||
$status = filterRequest("status");
|
||||
$id=filterRequest("id");
|
||||
|
||||
// Set the current timestamp for the 'created_at' field
|
||||
$created_at = date("Y-m-d H:i:s");
|
||||
|
||||
// Prepare the SQL insert query using parameterized statements to avoid SQL injection
|
||||
$sql = "INSERT INTO `employee` (`id`,`name`, `education`, `site`, `phone`, `created_at`, `status`)
|
||||
VALUES (?,?, ?, ?, ?, ?, ?)";
|
||||
|
||||
// Prepare and execute the statement
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([$id, $name, $education, $site, $phone, $created_at, $status]);
|
||||
|
||||
// Check if the query successfully inserted the record
|
||||
if ($stmt->rowCount() > 0) {
|
||||
// If a row was inserted, print success
|
||||
jsonSuccess($message = "Employee record added successfully");
|
||||
} else {
|
||||
// If no rows were inserted, print failure
|
||||
jsonError($message = "Failed to add employee record");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
// Prepare the SQL query to select all records from the employee table with a limit of 10
|
||||
$sql = "SELECT
|
||||
*
|
||||
FROM
|
||||
`employee` e
|
||||
ORDER BY
|
||||
e.created_at
|
||||
DESC
|
||||
";
|
||||
|
||||
// Prepare and execute the statement
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute();
|
||||
|
||||
// Fetch all records as an associative array
|
||||
$employee_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Check if any records were retrieved
|
||||
if ($employee_data) {
|
||||
// If records were found, print the data as JSON
|
||||
jsonSuccess($data = $employee_data);
|
||||
} else {
|
||||
// If no records were found, print a failure message
|
||||
jsonError($message = "No employee records found");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
// File: /v1/admin/error/error_list_last20.php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
try {
|
||||
$sql = "SELECT `id`, `error`, `userId`, `userType`, `phone`, `created_at`, `device`, `details`, `status`
|
||||
FROM `error`
|
||||
ORDER BY `created_at` DESC
|
||||
LIMIT 20";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute();
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
jsonSuccess($rows);
|
||||
} catch (Exception $e) {
|
||||
error_log("error_list_last20.php: " . $e->getMessage());
|
||||
jsonError($message = "Failed to fetch last 20 errors");
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
// File: /v1/admin/error/error_search_by_phone.php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
try {
|
||||
$phone = filterRequest("phone");
|
||||
if ($phone === false || $phone === null || trim($phone) === "") {
|
||||
jsonError("Phone is required");
|
||||
exit;
|
||||
}
|
||||
|
||||
// في حال مخزّن الهاتف مشفّر، طبق نفس دالتك للتشفير هنا:
|
||||
// $enc_phone = $encryptionHelper->encryptData(trim($phone));
|
||||
// ثم بدّل الحقل في WHERE إلى phone = :ph
|
||||
$sql = "SELECT `id`, `error`, `userId`, `userType`, `phone`, `created_at`, `device`, `details`, `status`
|
||||
FROM `error`
|
||||
WHERE `phone` = :ph OR `phone` LIKE :phLike
|
||||
ORDER BY `created_at` DESC
|
||||
LIMIT 20";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([
|
||||
":ph" => trim($phone),
|
||||
":phLike" => '%' . trim($phone) . '%', // يسمح بجزء من الرقم إن أردت
|
||||
]);
|
||||
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
jsonSuccess($rows);
|
||||
} catch (Exception $e) {
|
||||
error_log("error_search_by_phone.php: " . $e->getMessage());
|
||||
jsonError($message = "Failed to search errors by phone");
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../connect.php';
|
||||
|
||||
// Allow any authenticated user to report errors, but validate input
|
||||
$error = filterRequest("error");
|
||||
$userId = filterRequest("userId");
|
||||
$userType = filterRequest("userType");
|
||||
$phone = filterRequest("phone");
|
||||
$device = filterRequest("device");
|
||||
$details = filterRequest("details");
|
||||
|
||||
// Sanitize log input to prevent log injection
|
||||
$safeError = str_replace(["\r", "\n"], ' ', substr($error ?? '', 0, 500));
|
||||
$safeUserId = str_replace(["\r", "\n"], ' ', substr($userId ?? '', 0, 50));
|
||||
$safeUserType = str_replace(["\r", "\n"], ' ', substr($userType ?? '', 0, 50));
|
||||
$safeDevice = str_replace(["\r", "\n"], ' ', substr($device ?? '', 0, 200));
|
||||
$safeDetails = str_replace(["\r", "\n"], ' ', substr($details ?? '', 0, 1000));
|
||||
|
||||
$logMsg = "[$safeUserType ID: $safeUserId] Error: $safeError | Where: $safeDevice | Details: $safeDetails";
|
||||
appLog($logMsg, "APP_ERROR");
|
||||
|
||||
// جملة SQL لإدخال البيانات، مع إضافة الحقل الجديد
|
||||
// لاحظ أننا لا نرسل حقل 'status' لأنه سيأخذ القيمة الافتراضية 'new' تلقائياً في قاعدة البيانات
|
||||
$sql = "INSERT INTO `error` (`error`, `userId`, `userType`, `phone`, `device`, `details`)
|
||||
VALUES (:error, :userId, :userType, :phone, :device, :details)";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
|
||||
// ربط المتغيرات بالقيم
|
||||
$stmt->bindParam(':error', $error);
|
||||
$stmt->bindParam(':userId', $userId);
|
||||
$stmt->bindParam(':userType', $userType);
|
||||
$stmt->bindParam(':phone', $phone);
|
||||
$stmt->bindParam(':device', $device);
|
||||
$stmt->bindParam(':details', $details); // <-- ربط المتغير الجديد
|
||||
|
||||
$stmt->execute();
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
// طباعة رسالة نجاح مع تفاصيل الخطأ لسهولة التتبع في الكونسول
|
||||
jsonSuccess($error);
|
||||
} else {
|
||||
// طباعة رسالة فشل
|
||||
jsonError("Failed to save error data");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
require_once 'vendor/autoload.php'; // Include the Composer autoloader
|
||||
|
||||
use Facebook\Facebook;
|
||||
|
||||
$appId = '$appId'; // Replace with your App ID
|
||||
$appSecret = '$appSecret'; // Replace with your App Secret
|
||||
$accessToken = '$accessToken'; // Replace with the token you want to debug
|
||||
|
||||
$fb = new Facebook([
|
||||
'app_id' => $appId,
|
||||
'app_secret' => $appSecret,
|
||||
'default_graph_version' => 'v16.0', // Adjust based on your API version
|
||||
]);
|
||||
|
||||
try {
|
||||
// Generate the app token
|
||||
$appToken = $appId . '|' . $appSecret;
|
||||
|
||||
// Debug the token
|
||||
$response = $fb->get('/debug_token?input_token=' . $accessToken, $appToken);
|
||||
$tokenData = $response->getDecodedBody();
|
||||
|
||||
// Display the token details
|
||||
echo "Token Data:\n";
|
||||
print_r($tokenData);
|
||||
|
||||
if (isset($tokenData['data']['expires_at'])) {
|
||||
echo "Expires At: " . date('Y-m-d H:i:s', $tokenData['data']['expires_at']) . "\n";
|
||||
} else {
|
||||
echo "The token does not have an expiration time.\n";
|
||||
}
|
||||
} catch (Facebook\Exceptions\FacebookResponseException $e) {
|
||||
error_log("[facebook.php] Graph API Error: " . $e->getMessage());
|
||||
echo 'An error occurred while fetching Facebook data';
|
||||
} catch (Facebook\Exceptions\FacebookSDKException $e) {
|
||||
error_log("[facebook.php] SDK Error: " . $e->getMessage());
|
||||
echo 'An error occurred while processing Facebook data';
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
require_once __DIR__ . '/../../connect.php'; // Includes db connection
|
||||
|
||||
$zone_name = filterRequest('zone_name');
|
||||
$latitude = filterRequest('latitude');
|
||||
$longitude = filterRequest('longitude');
|
||||
$radius_meters = filterRequest('radius_meters');
|
||||
$country_code = filterRequest('country_code');
|
||||
$priority = filterRequest('priority') ?? 1;
|
||||
|
||||
if (empty($zone_name) || empty($latitude) || empty($longitude) || empty($radius_meters) || empty($country_code)) {
|
||||
echo json_encode(["status" => "error", "message" => "Missing required fields"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Check for overlapping zones
|
||||
// Using Haversine formula directly in SQL to find any zone where distance < (new_radius + existing_radius)
|
||||
$sql = "
|
||||
SELECT id, zone_name, radius_meters,
|
||||
(
|
||||
6371000 * acos(
|
||||
cos(radians(:new_lat)) * cos(radians(latitude)) *
|
||||
cos(radians(longitude) - radians(:new_lng)) +
|
||||
sin(radians(:new_lat)) * sin(radians(latitude))
|
||||
)
|
||||
) AS distance_meters
|
||||
FROM geofence_zones
|
||||
WHERE is_active = 1 AND country_code = :country_code
|
||||
HAVING distance_meters < (radius_meters + :new_radius)
|
||||
LIMIT 1
|
||||
";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->bindValue(':new_lat', (float) $latitude);
|
||||
$stmt->bindValue(':new_lng', (float) $longitude);
|
||||
$stmt->bindValue(':new_radius', (int) $radius_meters, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':country_code', $country_code);
|
||||
$stmt->execute();
|
||||
|
||||
$overlapping_zone = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($overlapping_zone) {
|
||||
echo json_encode([
|
||||
"status" => "error",
|
||||
"message" => "Zone overlaps with existing zone: " . $overlapping_zone['zone_name'],
|
||||
"overlap_details" => $overlapping_zone
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Insert new zone
|
||||
$insert_sql = "INSERT INTO geofence_zones (zone_name, latitude, longitude, radius_meters, priority, country_code)
|
||||
VALUES (:zone_name, :lat, :lng, :radius, :priority, :country)";
|
||||
|
||||
$insert_stmt = $con->prepare($insert_sql);
|
||||
$insert_stmt->bindValue(':zone_name', $zone_name);
|
||||
$insert_stmt->bindValue(':lat', (float) $latitude);
|
||||
$insert_stmt->bindValue(':lng', (float) $longitude);
|
||||
$insert_stmt->bindValue(':radius', (int) $radius_meters, PDO::PARAM_INT);
|
||||
$insert_stmt->bindValue(':priority', (int) $priority, PDO::PARAM_INT);
|
||||
$insert_stmt->bindValue(':country', $country_code);
|
||||
$insert_stmt->execute();
|
||||
|
||||
echo json_encode([
|
||||
"status" => "success",
|
||||
"message" => "Geofence zone added successfully",
|
||||
"zone_id" => $con->lastInsertId()
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error adding geofence zone: " . $e->getMessage());
|
||||
echo json_encode(["status" => "error", "message" => "Server error"]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
/**
|
||||
* get_heatmap.php
|
||||
* ───────────────
|
||||
* تقرأ بيانات الخريطة الحرارية المجمعة من Redis
|
||||
* البيانات مقسمة حسب الدولة (عبر Bounding Boxes في الـ Cron)
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
require_once __DIR__ . '/../../connect.php'; // Includes functions.php which has filterRequest()
|
||||
|
||||
$days = (int)(filterRequest('days') ?? 7);
|
||||
$source = filterRequest('source') ?? 'all';
|
||||
$countryCode = strtoupper(filterRequest('country_code') ?? 'all');
|
||||
|
||||
try {
|
||||
$redis = getRedisConnection();
|
||||
$cacheJson = $redis->get('siro:cache:heatmap:data');
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Redis connection failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$cacheJson) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Cache not generated yet']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$cacheData = json_decode($cacheJson, true);
|
||||
|
||||
if (!$cacheData || !isset($cacheData['data'])) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid cache data']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$limitDate = date('Y-m-d', strtotime("-$days days"));
|
||||
|
||||
$filteredLocations = [];
|
||||
$stats = ['geofence' => 0, 'app_usage' => 0, 'silent_push' => 0];
|
||||
|
||||
$dataByCountry = $cacheData['data'];
|
||||
|
||||
// تحديد الدول التي سنسحب منها
|
||||
$countriesToSearch = ($countryCode === 'ALL') ? array_keys($dataByCountry) : [$countryCode];
|
||||
|
||||
foreach ($countriesToSearch as $cc) {
|
||||
if (!isset($dataByCountry[$cc])) continue;
|
||||
|
||||
foreach ($dataByCountry[$cc] as $loc) {
|
||||
// فلتر الأيام
|
||||
if ($loc['date'] < $limitDate) continue;
|
||||
|
||||
// فلتر المصدر
|
||||
if ($source !== 'all' && $loc['source'] !== $source) continue;
|
||||
|
||||
$filteredLocations[] = [
|
||||
'latitude' => $loc['lat'],
|
||||
'longitude' => $loc['lng'],
|
||||
'source' => $loc['source']
|
||||
];
|
||||
|
||||
if (isset($stats[$loc['source']])) {
|
||||
$stats[$loc['source']]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'data' => $filteredLocations,
|
||||
'total' => count($filteredLocations),
|
||||
'stats' => $stats,
|
||||
'source' => 'redis_cache'
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
// تم إزالة دالة filterRequest من هنا لتجنب خطأ Redeclaration لأنها معرفة في functions.php
|
||||
?>
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../connect.php';
|
||||
|
||||
$sql = "SELECT
|
||||
`passengers`.`id`,
|
||||
`passengers`.`phone`,
|
||||
`passengers`.`email`,
|
||||
`passengers`.`gender`,
|
||||
`passengers`.`status`,
|
||||
`passengers`.`birthdate`,
|
||||
`passengers`.`site`,
|
||||
`passengers`.`first_name`,
|
||||
`passengers`.`last_name`,
|
||||
`passengers`.`sosPhone`,
|
||||
`passengers`.`education`,
|
||||
`passengers`.`employmentType`,
|
||||
`passengers`.`maritalStatus`,
|
||||
`passengers`.`created_at`,
|
||||
`passengers`.`updated_at`,
|
||||
(
|
||||
SELECT COUNT(`id`) FROM `passengers`
|
||||
) AS countPassenger,
|
||||
(
|
||||
SELECT COUNT(`id`) FROM `feedBack`
|
||||
) AS countFeedback,
|
||||
(
|
||||
SELECT CAST(AVG(`rating`) AS DECIMAL(10,2))
|
||||
FROM `ratingPassenger`
|
||||
WHERE `passenger_id` = `passengers`.`id`
|
||||
) AS ratingPassenger,
|
||||
(
|
||||
SELECT COUNT(`driverID`)
|
||||
FROM `ratingPassenger`
|
||||
WHERE `passenger_id` = `passengers`.`id`
|
||||
) AS countDriverRate,
|
||||
(
|
||||
SELECT COUNT(`passengerID`)
|
||||
FROM `canecl`
|
||||
WHERE `passengerID` = `passengers`.`id`
|
||||
) AS countPassengerCancel,
|
||||
(
|
||||
SELECT CAST(AVG(`rating`) AS DECIMAL(10,2))
|
||||
FROM `ratingDriver`
|
||||
WHERE `passenger_iD` = `passengers`.`id`
|
||||
) AS passengerAverageRating,
|
||||
(
|
||||
SELECT COUNT(`driver_id`)
|
||||
FROM `ratingDriver`
|
||||
WHERE `passenger_id` = `passengers`.`id`
|
||||
) AS countPassengerRate,
|
||||
(
|
||||
SELECT COUNT(`ride`.`passenger_id`)
|
||||
FROM `ride`
|
||||
WHERE `ride`.`passenger_id` = `passengers`.`id`
|
||||
) AS countPassengerRide,
|
||||
(
|
||||
SELECT `token`
|
||||
FROM `tokens`
|
||||
WHERE `tokens`.`passengerID` = `passengers`.`id`
|
||||
) AS passengerToken
|
||||
FROM
|
||||
`passengers`
|
||||
GROUP BY
|
||||
`passengers`.`id`
|
||||
ORDER BY
|
||||
countPassengerRide DESC
|
||||
LIMIT 10";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute();
|
||||
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// ✅ فك التشفير للحقول الحساسة
|
||||
foreach ($result as &$row) {
|
||||
$fieldsToDecrypt = [
|
||||
"phone", "email", "gender", "birthdate", "site",
|
||||
"first_name", "last_name", "sosPhone",
|
||||
"education", "employmentType", "maritalStatus", "passengerToken"
|
||||
];
|
||||
|
||||
foreach ($fieldsToDecrypt as $field) {
|
||||
if (isset($row[$field]) && $row[$field] !== null) {
|
||||
$decrypted = $encryptionHelper->decryptData($row[$field]);
|
||||
if ($decrypted !== false) {
|
||||
$row[$field] = $decrypted;
|
||||
} else {
|
||||
// سجل أو تجاهل القيم التي فشل فك تشفيرها
|
||||
$row[$field] = null; // أو احتفظ بالقيمة المشفرة
|
||||
error_log("Failed to decrypt field '$field' for passenger ID: " . $row['id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
jsonSuccess($data = $result);
|
||||
} else {
|
||||
jsonError("No records found");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../connect.php';
|
||||
|
||||
$passengerID = filterRequest("passengerID");
|
||||
|
||||
$sql = "SELECT
|
||||
`passengers`.`id`,
|
||||
`passengers`.`phone`,
|
||||
`passengers`.`email`,
|
||||
`passengers`.`gender`,
|
||||
`passengers`.`status`,
|
||||
`passengers`.`birthdate`,
|
||||
`passengers`.`site`,
|
||||
`passengers`.`first_name`,
|
||||
`passengers`.`last_name`,
|
||||
`passengers`.`sosPhone`,
|
||||
`passengers`.`education`,
|
||||
`passengers`.`employmentType`,
|
||||
`passengers`.`maritalStatus`,
|
||||
`passengers`.`created_at`,
|
||||
`passengers`.`updated_at`,
|
||||
(
|
||||
SELECT COUNT(`id`) FROM `passengers`
|
||||
) AS countPassenger,
|
||||
(
|
||||
SELECT COUNT(`id`) FROM `feedBack`
|
||||
) AS countFeedback,
|
||||
(
|
||||
SELECT CAST(AVG(`rating`) AS DECIMAL(10,2))
|
||||
FROM `ratingPassenger`
|
||||
WHERE `passenger_id` = `passengers`.`id`
|
||||
) AS ratingPassenger,
|
||||
(
|
||||
SELECT COUNT(`driverID`)
|
||||
FROM `ratingPassenger`
|
||||
WHERE `passenger_id` = `passengers`.`id`
|
||||
) AS countDriverRate,
|
||||
(
|
||||
SELECT COUNT(`passengerID`)
|
||||
FROM `canecl`
|
||||
WHERE `passengerID` = `passengers`.`id`
|
||||
) AS countPassengerCancel,
|
||||
(
|
||||
SELECT CAST(AVG(`rating`) AS DECIMAL(10,2))
|
||||
FROM `ratingDriver`
|
||||
WHERE `passenger_iD` = `passengers`.`id`
|
||||
) AS passengerAverageRating,
|
||||
(
|
||||
SELECT COUNT(`driver_id`)
|
||||
FROM `ratingDriver`
|
||||
WHERE `passenger_id` = `passengers`.`id`
|
||||
) AS countPassengerRate,
|
||||
(
|
||||
SELECT COUNT(`ride`.`passenger_id`)
|
||||
FROM `ride`
|
||||
WHERE `ride`.`passenger_id` = `passengers`.`id`
|
||||
) AS countPassengerRide,
|
||||
(
|
||||
SELECT `token`
|
||||
FROM `tokens`
|
||||
WHERE `tokens`.`passengerID` = `passengers`.`id`
|
||||
) AS passengerToken
|
||||
FROM
|
||||
`passengers`
|
||||
WHERE
|
||||
passengers.id = '$passengerID'
|
||||
GROUP BY
|
||||
`passengers`.`id`
|
||||
ORDER BY
|
||||
countPassengerRide DESC";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute();
|
||||
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// ✅ فك تشفير الحقول الحساسة
|
||||
foreach ($result as &$row) {
|
||||
$fieldsToDecrypt = [
|
||||
"phone", "email", "gender", "birthdate", "site",
|
||||
"first_name", "last_name", "sosPhone",
|
||||
"education", "employmentType", "maritalStatus", "passengerToken"
|
||||
];
|
||||
|
||||
foreach ($fieldsToDecrypt as $field) {
|
||||
if (isset($row[$field]) && $row[$field] !== null) {
|
||||
$row[$field] = $encryptionHelper->decryptData($row[$field]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
jsonSuccess($result);
|
||||
} else {
|
||||
jsonError("No records found");
|
||||
}
|
||||
?>
|
||||