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>
This commit is contained in:
Hamza-Ayed
2026-07-27 05:10:29 +03:00
co-authored by Claude Opus 5
commit 92dc6b3641
3654 changed files with 896478 additions and 0 deletions
+18
View File
@@ -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
}
]
}
+1
View File
@@ -0,0 +1 @@
bd73a4b3-6e9c-4611-af5a-f87b31b166be
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{"session_id": 1782053786182, "last_ping": 1782053786182}
+35
View File
@@ -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.
+103
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
/build
+64
View File
@@ -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)
}
+21
View File
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

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)
}
}
+6
View File
@@ -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
}
+23
View File
@@ -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
+32
View File
@@ -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" }
Binary file not shown.
+6
View File
@@ -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
Vendored Executable
+185
View File
@@ -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" "$@"
+89
View File
@@ -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
+23
View File
@@ -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")
+199
View File
@@ -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)
+6
View File
@@ -0,0 +1,6 @@
.DS_Store
logs/
*.log
error_log
.gemini/
portrate_captain_image/
View File
+94
View File
@@ -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");
}
+79
View File
@@ -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");
}
?>
+54
View File
@@ -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();
+99
View File
@@ -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.");
}
+83
View File
@@ -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();
+65
View File
@@ -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";
}
+46
View File
@@ -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.");
}
?>
+88
View File
@@ -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()
]);
}
View File
+24
View File
@@ -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");
}
?>
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

+28
View File
@@ -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"
]);
}
?>
+19
View File
@@ -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()]);
}
View File
+48
View File
@@ -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.");
}
+33
View File
@@ -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.");
}
+203
View File
@@ -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);
}
+94
View File
@@ -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.");
}
+80
View File
@@ -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();
+56
View File
@@ -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 ---");
?>
+137
View File
@@ -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());
}
+46
View File
@@ -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("رمز التحقق غير صالح أو منتهي.");
}
+77
View File
@@ -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");
}
?>
+54
View File
@@ -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.");
}
+30
View File
@@ -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.");
}
+48
View File
@@ -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.");
}
?>
+31
View File
@@ -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");
}
?>
+29
View File
@@ -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");
}
?>
+18
View File
@@ -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");
}
+46
View File
@@ -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");
}
?>
+39
View File
@@ -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';
}
+76
View File
@@ -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"]);
}
?>
+77
View File
@@ -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
?>
+100
View File
@@ -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");
}
?>

Some files were not shown because too many files have changed in this diff Show More