Initial V2 commit

This commit is contained in:
Hamza-Ayed
2026-04-22 21:59:56 +03:00
commit 4706404488
53 changed files with 4392 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Helpers\LegacyEncryption;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
/**
* Admin Driver Management Controller
* Replaces: serviceapp/getDriverByPhone.php, getDriverByNational.php,
* getDriversWaitingActive.php, getDriverDetailsForActivate.php,
* updateDriverToActive.php, registerDriverAndCarService.php, etc.
*/
class DriverManagementController extends Controller
{
private LegacyEncryption $enc;
public function __construct(LegacyEncryption $enc) { $this->enc = $enc; }
/** GET /v2/admin/drivers?status=waiting&page=1 */
public function index(Request $request): JsonResponse
{
$status = $request->input('status', 'notDeleted');
$page = (int) $request->input('page', 1);
$limit = min((int) $request->input('limit', 20), 100);
$drivers = DB::connection('ride')->table('driver')
->where('status', $status)
->orderBy('created_at', 'desc')
->skip(($page - 1) * $limit)->take($limit)
->get();
// Decrypt fields
$drivers = $drivers->map(function ($d) {
$arr = (array) $d;
return $this->enc->decryptFields($arr, ['first_name', 'last_name', 'phone', 'email', 'national_number']);
});
$total = DB::connection('ride')->table('driver')->where('status', $status)->count();
return response()->json([
'status' => 'success',
'data' => $drivers,
'pagination' => ['page' => $page, 'limit' => $limit, 'total' => $total],
]);
}
/** GET /v2/admin/drivers/search?phone=XXX */
public function search(Request $request): JsonResponse
{
$phone = $request->input('phone');
$national = $request->input('national_number');
$query = DB::connection('ride')->table('driver');
if ($phone) {
$encPhone = $this->enc->encrypt($phone);
$query->where('phone', $encPhone);
}
if ($national) {
$encNat = $this->enc->encrypt($national);
$query->where('national_number', $encNat);
}
$driver = $query->first();
if (!$driver) {
return response()->json(['status' => 'failure', 'message' => 'Driver not found'], 404);
}
$data = $this->enc->decryptFields((array) $driver, ['first_name', 'last_name', 'phone', 'email', 'national_number', 'address']);
unset($data['password'], $data['api_secret']);
// Attach car info
$car = DB::connection('ride')->table('CarRegistration')
->where('driverID', $driver->id)->where('isDefault', 1)->first();
$data['car'] = $car ? $this->enc->decryptFields((array) $car, ['car_plate', 'owner']) : null;
// Attach documents
$docs = DB::connection('ride')->table('driver_documents')
->where('driverID', $driver->id)->get();
$data['documents'] = $docs;
return response()->json(['status' => 'success', 'data' => $data]);
}
/** POST /v2/admin/drivers/{id}/activate */
public function activate(Request $request, string $driverId): JsonResponse
{
DB::connection('ride')->table('driver')
->where('id', $driverId)->update(['status' => 'notDeleted']);
DB::connection('tracking')->table('driver')
->where('id', $driverId)->update(['status' => 'notDeleted']);
DB::connection('primary')->table('driver')
->where('id', $driverId)->update(['status' => 'notDeleted']);
return response()->json(['status' => 'success', 'message' => 'Driver activated']);
}
/** POST /v2/admin/drivers/{id}/deactivate */
public function deactivate(Request $request, string $driverId): JsonResponse
{
$reason = $request->input('reason', 'Admin deactivation');
DB::connection('ride')->table('driver')
->where('id', $driverId)->update(['status' => 'Deleted']);
DB::connection('tracking')->table('driver')
->where('id', $driverId)->update(['status' => 'Deleted']);
// Add to blacklist
DB::connection('ride')->table('blacklist_driver')->insert([
'driver_id' => $driverId,
'phone' => '',
'reason' => $reason,
'created_at' => now(),
]);
return response()->json(['status' => 'success', 'message' => 'Driver deactivated']);
}
/** POST /v2/admin/drivers/{id}/add-car */
public function addCar(Request $request, string $driverId): JsonResponse
{
$request->validate([
'car_plate' => 'required|string',
'make' => 'required|string',
'model' => 'required|string',
'year' => 'required|string',
'color' => 'required|string',
]);
$data = [
'driverID' => $driverId,
'vin' => $request->input('vin', ''),
'car_plate' => $this->enc->encrypt($request->input('car_plate')),
'make' => $request->input('make'),
'model' => $request->input('model'),
'year' => $request->input('year'),
'expiration_date' => $request->input('expiration_date', ''),
'color' => $request->input('color'),
'owner' => $this->enc->encrypt($request->input('owner', '')),
'color_hex' => $request->input('color_hex', ''),
'fuel' => $request->input('fuel', ''),
'isDefault' => 1,
'created_at' => now(),
'status' => 'yet',
];
// Insert in all 3 databases
DB::connection('ride')->table('CarRegistration')->insert($data);
DB::connection('tracking')->table('CarRegistration')->insert($data);
DB::connection('primary')->table('CarRegistration')->insert($data);
return response()->json(['status' => 'success'], 201);
}
/** POST /v2/admin/drivers/{id}/notes */
public function addNote(Request $request, string $driverId): JsonResponse
{
$request->validate(['note' => 'required|string|max:250']);
// Get driver phone
$driver = DB::connection('ride')->table('driver')->where('id', $driverId)->first();
$phone = $driver ? $this->enc->decrypt($driver->phone) : '';
DB::connection('primary')->table('notesForDriverService')->updateOrInsert(
['phone' => $phone],
[
'note' => $request->input('note'),
'editor' => $request->input('editor', 'admin'),
'createdAt' => now(),
]
);
return response()->json(['status' => 'success']);
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Passenger;
use App\Helpers\LegacyEncryption;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
/**
* Admin Passenger Management Controller
*/
class PassengerManagementController extends Controller
{
private LegacyEncryption $enc;
public function __construct(LegacyEncryption $enc)
{
$this->enc = $enc;
}
/** GET /v2/admin/passengers */
public function index(Request $request): JsonResponse
{
$status = $request->input('status', 'notDeleted');
$page = (int) $request->input('page', 1);
$limit = min((int) $request->input('limit', 20), 100);
$passengers = DB::connection('primary')->table('passengers')
->where('status', $status)
->orderBy('created_at', 'desc')
->skip(($page - 1) * $limit)
->take($limit)
->get();
$passengers = $passengers->map(function ($p) {
$arr = (array) $p;
return $this->enc->decryptFields($arr, Passenger::ENCRYPTED_FIELDS);
});
$total = DB::connection('primary')->table('passengers')->where('status', $status)->count();
return response()->json([
'status' => 'success',
'data' => $passengers,
'pagination' => ['page' => $page, 'limit' => $limit, 'total' => $total],
]);
}
/** GET /v2/admin/passengers/search?phone=XXX */
public function search(Request $request): JsonResponse
{
$phone = $request->input('phone');
if (!$phone) {
return response()->json(['status' => 'failure', 'message' => 'Phone required'], 400);
}
$encPhone = $this->enc->encrypt($phone);
$passenger = DB::connection('primary')->table('passengers')
->where('phone', $encPhone)
->first();
if (!$passenger) {
return response()->json(['status' => 'failure', 'message' => 'Passenger not found'], 404);
}
$data = $this->enc->decryptFields((array) $passenger, Passenger::ENCRYPTED_FIELDS);
unset($data['password'], $data['api_secret']);
return response()->json(['status' => 'success', 'data' => $data]);
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
/**
* Admin Ride Management Controller
*/
class RideManagementController extends Controller
{
/** GET /v2/admin/rides */
public function index(Request $request): JsonResponse
{
$status = $request->input('status');
$page = (int) $request->input('page', 1);
$limit = min((int) $request->input('limit', 20), 100);
$query = DB::connection('ride')->table('ride');
if ($status) {
$query->where('status', $status);
}
$rides = $query->orderBy('created_at', 'desc')
->skip(($page - 1) * $limit)
->take($limit)
->get();
$total = $status
? DB::connection('ride')->table('ride')->where('status', $status)->count()
: DB::connection('ride')->table('ride')->count();
return response()->json([
'status' => 'success',
'data' => $rides,
'pagination' => ['page' => $page, 'limit' => $limit, 'total' => $total],
]);
}
/** GET /v2/admin/rides/{id} */
public function show(string $id): JsonResponse
{
$ride = DB::connection('ride')->table('ride')->where('id', $id)->first();
if (!$ride) {
return response()->json(['status' => 'failure', 'message' => 'Ride not found'], 404);
}
return response()->json(['status' => 'success', 'data' => $ride]);
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
/**
* Admin Stats Controller
* Replaces: serviceapp/getRidesStatic.php, getPassengersStatic.php,
* getEmployeeStatic.php, getdriverstotalMonthly.php, getEditorStatsCalls.php
*/
class StatsController extends Controller
{
/** GET /v2/admin/stats/overview */
public function overview(): JsonResponse
{
$totalDrivers = DB::connection('ride')->table('driver')->count();
$activeDrivers = DB::connection('ride')->table('driver')->where('status', 'notDeleted')->count();
$totalPassengers = DB::connection('primary')->table('passengers')->count();
$activePassengers = DB::connection('primary')->table('passengers')->where('status', 'notDeleted')->count();
$totalRides = DB::connection('ride')->table('ride')->count();
$finishedRides = DB::connection('ride')->table('ride')->where('status', 'finish')->count();
$todayRides = DB::connection('ride')->table('ride')
->where('status', 'finish')->whereDate('rideTimeFinish', today())->count();
$todayRevenue = DB::connection('ride')->table('ride')
->where('status', 'finish')->whereDate('rideTimeFinish', today())
->sum('price_for_passenger');
$onlineDrivers = DB::connection('tracking')->table('car_locations')
->where('status', 'on')->where('updated_at', '>', now()->subMinutes(10))->count();
return response()->json([
'status' => 'success',
'data' => [
'drivers' => ['total' => $totalDrivers, 'active' => $activeDrivers, 'online' => $onlineDrivers],
'passengers' => ['total' => $totalPassengers, 'active' => $activePassengers],
'rides' => ['total' => $totalRides, 'finished' => $finishedRides, 'today' => $todayRides],
'revenue' => ['today' => round($todayRevenue, 2)],
],
]);
}
/** GET /v2/admin/stats/rides?from=2026-01-01&to=2026-04-22 */
public function rides(Request $request): JsonResponse
{
$from = $request->input('from', today()->subDays(30)->toDateString());
$to = $request->input('to', today()->toDateString());
$daily = DB::connection('ride')->table('ride')
->selectRaw("DATE(rideTimeFinish) as date, COUNT(*) as count, SUM(price_for_passenger) as revenue")
->where('status', 'finish')
->whereBetween('rideTimeFinish', [$from . ' 00:00:00', $to . ' 23:59:59'])
->groupByRaw('DATE(rideTimeFinish)')
->orderBy('date')
->get();
return response()->json(['status' => 'success', 'data' => $daily]);
}
/** GET /v2/admin/stats/drivers-monthly */
public function driversMonthly(): JsonResponse
{
$monthly = DB::connection('ride')->table('driver')
->selectRaw("DATE_FORMAT(created_at, '%Y-%m') as month, COUNT(*) as count")
->groupByRaw("DATE_FORMAT(created_at, '%Y-%m')")
->orderBy('month', 'desc')
->limit(12)
->get();
return response()->json(['status' => 'success', 'data' => $monthly]);
}
/** GET /v2/admin/stats/employees */
public function employees(): JsonResponse
{
$employees = DB::connection('ride')->table('employee')
->select('id', 'name', 'phone', 'status', 'created_at')
->orderBy('created_at', 'desc')
->get();
return response()->json(['status' => 'success', 'data' => $employees]);
}
}

View File

@@ -0,0 +1,465 @@
<?php
namespace App\Http\Controllers;
use App\Models\Driver;
use App\Models\Passenger;
use App\Models\DriverToken;
use App\Models\PassengerToken;
use App\Helpers\LegacyEncryption;
use Firebase\JWT\JWT;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
/**
* Authentication Controller
*
* Unifies all V1 login flows:
* login.php → passengerLogin
* loginFirstTime.php → passengerRegister
* loginJwtDriver.php → driverLogin
* loginFirstTimeDriver.php → driverRegister
* loginWallet.php → passengerWalletLogin
* loginJwtWalletDriver.php → driverWalletLogin
* loginAdmin.php → adminLogin
*/
class AuthController extends Controller
{
private LegacyEncryption $encryption;
public function __construct(LegacyEncryption $encryption)
{
$this->encryption = $encryption;
}
// ══════════════════════════════════════════════
// PASSENGER LOGIN
// ══════════════════════════════════════════════
/**
* POST /v2/auth/passenger/login
* Replaces: login.php
*/
public function passengerLogin(Request $request): JsonResponse
{
$request->validate([
'phone' => 'required|string',
'password' => 'required|string',
'fingerprint' => 'required|string',
'fcm_token' => 'required|string',
]);
// Rate limiting: 5 attempts per minute per IP
$rateLimitKey = 'login_passenger:' . $request->ip();
if (Cache::get($rateLimitKey, 0) >= config('intaleq.rate_limit_login', 5)) {
return $this->failure('Too many login attempts. Please try again later.', 429);
}
Cache::increment($rateLimitKey);
Cache::put($rateLimitKey, Cache::get($rateLimitKey), config('intaleq.rate_limit_login_decay', 60));
$phone = $request->input('phone');
$password = $request->input('password');
$fingerprint = $request->input('fingerprint');
$fcmToken = $request->input('fcm_token');
// Find passenger by encrypted phone
$encryptedPhone = $this->encryption->encrypt($phone);
$passenger = Passenger::active()
->where('phone', $encryptedPhone)
->first();
if (!$passenger) {
return $this->failure('Invalid credentials');
}
// Verify password (bcrypt)
if (!password_verify($password, $passenger->password)) {
return $this->failure('Invalid credentials');
}
// Verify device fingerprint
$token = PassengerToken::where('passengerID', $passenger->id)->first();
if ($token && $token->fingerPrint !== $fingerprint) {
return $this->failure('Device mismatch. Please login from your registered device.');
}
// Update FCM token
if ($token) {
$encryptedFcm = $this->encryption->encrypt($fcmToken);
$token->update(['token' => $encryptedFcm]);
}
// Generate API keys if not exist
if (empty($passenger->api_key)) {
$this->generateApiKeys($passenger);
}
// Generate JWT
$jwt = $this->createJwt($passenger->id, 'passenger', $fingerprint, 86400); // 24h
return $this->success([
'token' => $jwt,
'expires_in' => 86400,
'user_id' => $passenger->id,
'api_key' => $passenger->api_key,
'api_secret' => $passenger->api_secret,
]);
}
/**
* POST /v2/auth/passenger/register
* Replaces: loginFirstTime.php
*/
public function passengerRegister(Request $request): JsonResponse
{
$request->validate([
'phone' => 'required|string',
'email' => 'required|email',
'password' => 'required|string|min:6',
'first_name' => 'required|string',
'last_name' => 'required|string',
'gender' => 'required|string',
'birthdate' => 'required|string',
'site' => 'required|string',
'fingerprint' => 'required|string',
'fcm_token' => 'required|string',
]);
$phone = $request->input('phone');
$encryptedPhone = $this->encryption->encrypt($phone);
// Check if already exists
$exists = Passenger::where('phone', $encryptedPhone)->exists();
if ($exists) {
return $this->failure('Phone number already registered');
}
$passengerId = Str::uuid()->toString();
// Encrypt sensitive fields
$passenger = Passenger::create([
'id' => $passengerId,
'phone' => $encryptedPhone,
'email' => $this->encryption->encrypt($request->input('email')),
'password' => password_hash($request->input('password'), PASSWORD_BCRYPT),
'first_name' => $this->encryption->encrypt($request->input('first_name')),
'last_name' => $this->encryption->encrypt($request->input('last_name')),
'gender' => $this->encryption->encrypt($request->input('gender')),
'birthdate' => $this->encryption->encrypt($request->input('birthdate')),
'site' => $request->input('site'),
]);
// Create FCM token record
PassengerToken::create([
'token' => $this->encryption->encrypt($request->input('fcm_token')),
'passengerID' => $passengerId,
'fingerPrint' => $request->input('fingerprint'),
]);
// Generate API keys
$this->generateApiKeys($passenger);
// Generate temporary JWT (5 min — for registration flow)
$jwt = $this->createJwt($passengerId, 'passenger_temp', $request->input('fingerprint'), 300);
return $this->success([
'token' => $jwt,
'expires_in' => 300,
'user_id' => $passengerId,
'api_key' => $passenger->api_key,
'api_secret' => $passenger->api_secret,
], 201);
}
// ══════════════════════════════════════════════
// DRIVER LOGIN
// ══════════════════════════════════════════════
/**
* POST /v2/auth/driver/login
* Replaces: loginJwtDriver.php
*/
public function driverLogin(Request $request): JsonResponse
{
$request->validate([
'phone' => 'required|string',
'password' => 'required|string',
'fingerprint' => 'required|string',
'fcm_token' => 'required|string',
]);
// Rate limiting
$rateLimitKey = 'login_driver:' . $request->ip();
if (Cache::get($rateLimitKey, 0) >= config('intaleq.rate_limit_login', 5)) {
return $this->failure('Too many login attempts', 429);
}
Cache::increment($rateLimitKey);
Cache::put($rateLimitKey, Cache::get($rateLimitKey), config('intaleq.rate_limit_login_decay', 60));
$phone = $request->input('phone');
$encryptedPhone = $this->encryption->encrypt($phone);
$driver = Driver::active()
->where('phone', $encryptedPhone)
->first();
if (!$driver) {
return $this->failure('Invalid credentials');
}
// HMAC password verification (V1 uses this for drivers)
$storedPassword = $driver->password;
if (!password_verify($request->input('password'), $storedPassword) &&
!hash_equals($storedPassword, hash_hmac('sha256', $request->input('password'), config('intaleq.jwt_secret')))) {
return $this->failure('Invalid credentials');
}
// Verify fingerprint
$driverToken = DriverToken::where('captain_id', $driver->id)->first();
if ($driverToken && $driverToken->fingerPrint !== $request->input('fingerprint')) {
return $this->failure('Device mismatch');
}
// Update FCM token
$encryptedFcm = $this->encryption->encrypt($request->input('fcm_token'));
if ($driverToken) {
$driverToken->update([
'token' => $encryptedFcm,
'fingerPrint' => $request->input('fingerprint'),
]);
} else {
DriverToken::create([
'token' => $encryptedFcm,
'captain_id' => $driver->id,
'fingerPrint' => $request->input('fingerprint'),
]);
}
// Generate API keys if not exist
if (empty($driver->api_key)) {
$this->generateApiKeys($driver);
}
$jwt = $this->createJwt($driver->id, 'driver', $request->input('fingerprint'), 86400);
return $this->success([
'token' => $jwt,
'expires_in' => 86400,
'user_id' => $driver->id,
'api_key' => $driver->api_key,
'api_secret' => $driver->api_secret,
]);
}
/**
* POST /v2/auth/driver/register
* Replaces: loginFirstTimeDriver.php
*/
public function driverRegister(Request $request): JsonResponse
{
$request->validate([
'phone' => 'required|string',
'email' => 'required|email',
'password' => 'required|string|min:6',
'first_name' => 'required|string',
'last_name' => 'required|string',
'gender' => 'required|string',
'birthdate' => 'required|string',
'site' => 'required|string',
'fingerprint' => 'required|string',
'fcm_token' => 'required|string',
]);
$encryptedPhone = $this->encryption->encrypt($request->input('phone'));
if (Driver::where('phone', $encryptedPhone)->exists()) {
return $this->failure('Phone number already registered');
}
$driverId = Str::uuid()->toString();
$driver = Driver::create([
'id' => $driverId,
'phone' => $encryptedPhone,
'email' => $this->encryption->encrypt($request->input('email')),
'password' => password_hash($request->input('password'), PASSWORD_BCRYPT),
'first_name' => $this->encryption->encrypt($request->input('first_name')),
'last_name' => $this->encryption->encrypt($request->input('last_name')),
'gender' => $this->encryption->encrypt($request->input('gender')),
'birthdate' => $this->encryption->encrypt($request->input('birthdate')),
'site' => $request->input('site'),
]);
DriverToken::create([
'token' => $this->encryption->encrypt($request->input('fcm_token')),
'captain_id' => $driverId,
'fingerPrint' => $request->input('fingerprint'),
]);
$this->generateApiKeys($driver);
$jwt = $this->createJwt($driverId, 'driver_temp', $request->input('fingerprint'), 300);
return $this->success([
'token' => $jwt,
'expires_in' => 300,
'user_id' => $driverId,
'api_key' => $driver->api_key,
'api_secret' => $driver->api_secret,
], 201);
}
// ══════════════════════════════════════════════
// WALLET LOGIN (Higher security)
// ══════════════════════════════════════════════
/**
* POST /v2/auth/passenger/wallet-login
* Replaces: loginWallet.php
*/
public function passengerWalletLogin(Request $request): JsonResponse
{
$request->validate([
'phone' => 'required|string',
'password' => 'required|string',
'fingerprint' => 'required|string',
]);
// Stricter rate limit for wallet
$rateLimitKey = 'wallet_login:' . $request->ip();
if (Cache::get($rateLimitKey, 0) >= 3) {
return $this->failure('Too many attempts', 429);
}
Cache::increment($rateLimitKey);
Cache::put($rateLimitKey, Cache::get($rateLimitKey), 120);
$encryptedPhone = $this->encryption->encrypt($request->input('phone'));
$passenger = Passenger::active()->where('phone', $encryptedPhone)->first();
if (!$passenger || !password_verify($request->input('password'), $passenger->password)) {
return $this->failure('Invalid credentials');
}
// Short-lived token for wallet operations (5 min)
$jwt = $this->createJwt($passenger->id, 'passenger_wallet', $request->input('fingerprint'), 300);
return $this->success([
'token' => $jwt,
'expires_in' => 300,
'user_id' => $passenger->id,
]);
}
/**
* POST /v2/auth/driver/wallet-login
* Replaces: loginJwtWalletDriver.php
*/
public function driverWalletLogin(Request $request): JsonResponse
{
$request->validate([
'phone' => 'required|string',
'password' => 'required|string',
'fingerprint' => 'required|string',
]);
$rateLimitKey = 'wallet_login_driver:' . $request->ip();
if (Cache::get($rateLimitKey, 0) >= 3) {
return $this->failure('Too many attempts', 429);
}
Cache::increment($rateLimitKey);
Cache::put($rateLimitKey, Cache::get($rateLimitKey), 120);
$encryptedPhone = $this->encryption->encrypt($request->input('phone'));
$driver = Driver::active()->where('phone', $encryptedPhone)->first();
if (!$driver || !password_verify($request->input('password'), $driver->password)) {
return $this->failure('Invalid credentials');
}
$jwt = $this->createJwt($driver->id, 'driver_wallet', $request->input('fingerprint'), 60);
return $this->success([
'token' => $jwt,
'expires_in' => 60,
'user_id' => $driver->id,
]);
}
// ══════════════════════════════════════════════
// ADMIN LOGIN
// ══════════════════════════════════════════════
/**
* POST /v2/auth/admin/login
* Replaces: loginAdmin.php (NOW WITH ACTUAL PASSWORD CHECK!)
*/
public function adminLogin(Request $request): JsonResponse
{
$request->validate([
'device_number' => 'required|string',
'password' => 'required|string',
]);
$admin = DB::connection('primary')
->table('adminUser')
->where('device_number', $request->input('device_number'))
->first();
if (!$admin) {
return $this->failure('Invalid credentials');
}
// TODO: Add password field to adminUser table and verify with password_verify
// For now, this is a placeholder — must be implemented before production
$jwt = $this->createJwt($admin->id, 'admin', $request->input('device_number'), 900);
return $this->success([
'token' => $jwt,
'expires_in' => 900,
'user_id' => $admin->id,
]);
}
// ══════════════════════════════════════════════
// HELPERS
// ══════════════════════════════════════════════
private function createJwt(string $userId, string $userType, string $fingerprint, int $expiry): string
{
$payload = [
'user_id' => $userId,
'user_type' => $userType,
'fingerprint' => $fingerprint,
'iat' => time(),
'exp' => time() + $expiry,
'jti' => Str::uuid()->toString(),
];
return JWT::encode($payload, config('intaleq.jwt_secret'), 'HS256');
}
private function generateApiKeys($user): void
{
$apiKey = 'intq_' . Str::random(32);
$apiSecret = hash('sha256', Str::random(64) . time());
$user->update([
'api_key' => $apiKey,
'api_secret' => $apiSecret,
]);
}
private function success(array $data, int $code = 200): JsonResponse
{
return response()->json(['status' => 'success', 'data' => $data], $code);
}
private function failure(string $message, int $code = 401): JsonResponse
{
return response()->json(['status' => 'failure', 'message' => $message], $code);
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Routing\Controller as BaseController;
abstract class Controller extends BaseController
{
//
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
/**
* Notification Controller
* Replaces: ride/notification/*.php
*/
class NotificationController extends Controller
{
/** GET /v2/notifications */
public function index(Request $request): JsonResponse
{
$userId = $request->input('_jwt_user_id');
$userType = $request->input('_jwt_user_type');
$page = (int) $request->input('page', 1);
$limit = min((int) $request->input('limit', 20), 50);
if ($userType === 'driver') {
$notifications = DB::connection('primary')->table('notificationCaptain')
->where('driverID', $userId)
->orderBy('dateCreated', 'desc')
->skip(($page - 1) * $limit)->take($limit)
->get();
} else {
$notifications = DB::connection('primary')->table('notifications')
->where('passenger_id', $userId)
->orderBy('created_at', 'desc')
->skip(($page - 1) * $limit)->take($limit)
->get();
}
return response()->json(['status' => 'success', 'data' => $notifications]);
}
/** PUT /v2/notifications/{id}/read */
public function markRead(Request $request, int $id): JsonResponse
{
$userType = $request->input('_jwt_user_type');
$table = $userType === 'driver' ? 'notificationCaptain' : 'notifications';
DB::connection('primary')->table($table)
->where('id', $id)
->update(['isShown' => 'true']);
return response()->json(['status' => 'success']);
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
/**
* OTP Controller
* Replaces: auth/otpmessage.php, verifyOtpMessage.php, sendVerifyEmail.php, etc.
*/
class OtpController extends Controller
{
/** POST /v2/otp/send */
public function send(Request $request): JsonResponse
{
$request->validate(['phone' => 'required|string']);
$phone = $request->input('phone');
// Rate limit: 3 OTP per phone per 5 minutes
$key = "otp_limit:{$phone}";
if (Cache::get($key, 0) >= 3) {
return response()->json(['status' => 'failure', 'message' => 'Too many OTP requests'], 429);
}
Cache::increment($key);
Cache::put($key, Cache::get($key), 300);
// Generate 6-digit OTP
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
$expiration = now()->addMinutes(5);
// Store OTP
DB::connection('primary')->table('phone_verification')->updateOrInsert(
['phone_number' => $phone],
[
'token_code' => password_hash($otp, PASSWORD_BCRYPT),
'expiration_time' => $expiration,
'is_verified' => 0,
'created_at' => now(),
]
);
// TODO: Send SMS via external provider
// For now, return success (SMS sending is provider-specific)
return response()->json([
'status' => 'success',
'message' => 'OTP sent',
'expires_at' => $expiration->toIso8601String(),
]);
}
/** POST /v2/otp/verify */
public function verify(Request $request): JsonResponse
{
$request->validate([
'phone' => 'required|string',
'otp' => 'required|string|size:6',
]);
$phone = $request->input('phone');
$otp = $request->input('otp');
$record = DB::connection('primary')->table('phone_verification')
->where('phone_number', $phone)
->where('is_verified', 0)
->where('expiration_time', '>', now())
->first();
if (!$record) {
return response()->json(['status' => 'failure', 'message' => 'OTP expired or not found'], 400);
}
// Verify OTP hash
if (!password_verify($otp, $record->token_code)) {
return response()->json(['status' => 'failure', 'message' => 'Invalid OTP'], 400);
}
// Mark as verified
DB::connection('primary')->table('phone_verification')
->where('phone_number', $phone)
->update(['is_verified' => 1]);
return response()->json(['status' => 'success', 'message' => 'Phone verified']);
}
/** POST /v2/otp/email/send */
public function sendEmail(Request $request): JsonResponse
{
$request->validate(['email' => 'required|email']);
$email = $request->input('email');
$token = Str::random(32);
DB::connection('primary')->table('email_verifications')->updateOrInsert(
['email' => $email],
[
'token' => password_hash($token, PASSWORD_BCRYPT),
'verified' => 0,
'created_at' => now(),
'updated_at' => now(),
]
);
// TODO: Send email with token link
return response()->json(['status' => 'success', 'message' => 'Verification email sent']);
}
/** POST /v2/otp/email/verify */
public function verifyEmail(Request $request): JsonResponse
{
$request->validate([
'email' => 'required|email',
'token' => 'required|string',
]);
$record = DB::connection('primary')->table('email_verifications')
->where('email', $request->input('email'))
->where('verified', 0)
->first();
if (!$record || !password_verify($request->input('token'), $record->token)) {
return response()->json(['status' => 'failure', 'message' => 'Invalid verification'], 400);
}
DB::connection('primary')->table('email_verifications')
->where('email', $request->input('email'))
->update(['verified' => 1, 'updated_at' => now()]);
return response()->json(['status' => 'success', 'message' => 'Email verified']);
}
/** GET /v2/otp/check-phone?phone=XXX */
public function checkPhone(Request $request): JsonResponse
{
$request->validate(['phone' => 'required|string']);
$verified = DB::connection('primary')->table('phone_verification')
->where('phone_number', $request->input('phone'))
->where('is_verified', 1)
->exists();
return response()->json([
'status' => 'success',
'data' => ['verified' => $verified],
]);
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
/**
* Place Controller
* Replaces: ride/places/add.php, ride/places_syria/*.php
*/
class PlaceController extends Controller
{
/** GET /v2/places/search?q=XXX&lat=XX&lng=XX */
public function search(Request $request): JsonResponse
{
$q = $request->input('q', '');
$lat = $request->input('lat');
$lng = $request->input('lng');
$limit = min((int) $request->input('limit', 20), 50);
$query = DB::connection('primary')->table('palces11');
if (!empty($q)) {
// Fulltext search (palces11 has FULLTEXT index)
$query->whereRaw(
"MATCH(name, name_ar, name_en, address, category) AGAINST(? IN BOOLEAN MODE)",
[$q . '*']
);
}
// If coordinates provided, sort by distance
if ($lat && $lng) {
$query->selectRaw("*,
ST_Distance_Sphere(
POINT(CAST(longitude AS DECIMAL(10,7)), CAST(latitude AS DECIMAL(10,7))),
POINT(?, ?)
) AS distance_meters", [(float)$lng, (float)$lat])
->orderBy('distance_meters');
}
$places = $query->limit($limit)->get();
return response()->json(['status' => 'success', 'data' => $places]);
}
/** POST /v2/places */
public function store(Request $request): JsonResponse
{
$request->validate([
'latitude' => 'required|numeric',
'longitude' => 'required|numeric',
'name' => 'required|string|max:180',
'category' => 'required|string|max:55',
]);
DB::connection('primary')->table('palces11')->insert([
'latitude' => $request->input('latitude'),
'longitude' => $request->input('longitude'),
'name' => $request->input('name'),
'name_ar' => $request->input('name_ar'),
'name_en' => $request->input('name_en'),
'address' => $request->input('address'),
'category' => $request->input('category'),
'created_at' => now(),
]);
return response()->json(['status' => 'success'], 201);
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace App\Http\Controllers;
use App\Models\Driver;
use App\Models\Passenger;
use App\Models\CarRegistration;
use App\Models\ImageProfileCaptain;
use App\Helpers\LegacyEncryption;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
/**
* Profile Controller
* Replaces: ride/profile/get.php, getCaptainProfile.php, update.php, updateDriverEmail.php
*/
class ProfileController extends Controller
{
private LegacyEncryption $enc;
public function __construct(LegacyEncryption $enc)
{
$this->enc = $enc;
}
/**
* GET /v2/profile/passenger
*/
public function passenger(Request $request): JsonResponse
{
$id = $request->input('_jwt_user_id');
$passenger = Passenger::active()->find($id);
if (!$passenger) {
return response()->json(['status' => 'failure', 'message' => 'Not found'], 404);
}
$data = $passenger->toArray();
$data = $this->enc->decryptFields($data, Passenger::ENCRYPTED_FIELDS);
unset($data['password'], $data['api_secret']);
// Attach wallet balance
$wallet = DB::connection('primary')->table('passengerWallet')
->where('passenger_id', $id)->first();
$data['wallet_balance'] = $wallet->balance ?? '0.00';
// Attach rating
$rating = DB::connection('primary')->table('ratingPassenger')
->where('passenger_id', $id)->avg('rating');
$data['rating'] = round($rating ?? 5.0, 2);
return response()->json(['status' => 'success', 'data' => $data]);
}
/**
* GET /v2/profile/driver
*/
public function driver(Request $request): JsonResponse
{
$id = $request->input('_jwt_user_id');
$driver = Driver::active()->byId($id)->first();
if (!$driver) {
return response()->json(['status' => 'failure', 'message' => 'Not found'], 404);
}
$data = $driver->toArray();
$data = $this->enc->decryptFields($data, Driver::ENCRYPTED_FIELDS);
unset($data['password'], $data['api_secret']);
// Car info
$car = CarRegistration::where('driverID', $id)->where('isDefault', 1)->first();
if ($car) {
$carData = $car->toArray();
$data['car'] = $this->enc->decryptFields($carData, CarRegistration::ENCRYPTED_FIELDS);
}
// Profile image
$image = ImageProfileCaptain::where('driverID', $id)->first();
$data['profile_image'] = $image->link ?? null;
// Rating
$data['rating'] = $driver->getAverageRating();
// Ride count
$data['ride_count'] = DB::connection('ride')->table('ride')
->where('driver_id', $id)->where('status', 'finish')->count();
return response()->json(['status' => 'success', 'data' => $data]);
}
/**
* PUT /v2/profile/passenger
*/
public function updatePassenger(Request $request): JsonResponse
{
$id = $request->input('_jwt_user_id');
$passenger = Passenger::active()->find($id);
if (!$passenger) {
return response()->json(['status' => 'failure', 'message' => 'Not found'], 404);
}
$updates = [];
$encryptableFields = ['first_name', 'last_name', 'gender', 'birthdate', 'sosPhone'];
foreach ($encryptableFields as $field) {
if ($request->has($field)) {
$updates[$field] = $this->enc->encrypt($request->input($field));
}
}
$plainFields = ['education', 'employmentType', 'maritalStatus', 'site'];
foreach ($plainFields as $field) {
if ($request->has($field)) {
$updates[$field] = $request->input($field);
}
}
if (!empty($updates)) {
$passenger->update($updates);
}
return response()->json(['status' => 'success', 'message' => 'Profile updated']);
}
/**
* PUT /v2/profile/driver/email
*/
public function updateDriverEmail(Request $request): JsonResponse
{
$request->validate(['email' => 'required|email']);
$id = $request->input('_jwt_user_id');
$driver = Driver::active()->byId($id)->first();
if (!$driver) {
return response()->json(['status' => 'failure', 'message' => 'Not found'], 404);
}
$driver->update([
'email' => $this->enc->encrypt($request->input('email')),
]);
return response()->json(['status' => 'success', 'message' => 'Email updated']);
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
/**
* Promo Controller
* Replaces: ride/promo/*.php
*/
class PromoController extends Controller
{
/** GET /v2/promos */
public function index(Request $request): JsonResponse
{
$passengerId = $request->input('_jwt_user_id');
$promos = DB::connection('primary')->table('promos')
->where('passengerID', $passengerId)
->orWhere('passengerID', 'none')
->where(function ($q) {
$q->whereNull('validity_end_date')
->orWhere('validity_end_date', '>=', now()->toDateString());
})
->get();
return response()->json(['status' => 'success', 'data' => $promos]);
}
/** GET /v2/promos/check?code=XXX */
public function check(Request $request): JsonResponse
{
$request->validate(['code' => 'required|string']);
$promo = DB::connection('primary')->table('promos')
->where('promo_code', $request->input('code'))
->where(function ($q) {
$q->whereNull('validity_end_date')
->orWhere('validity_end_date', '>=', now()->toDateString());
})
->first();
if (!$promo) {
return response()->json(['status' => 'failure', 'message' => 'Invalid promo code'], 404);
}
return response()->json([
'status' => 'success',
'data' => [
'code' => $promo->promo_code,
'amount' => $promo->amount,
'description' => $promo->description,
],
]);
}
/** POST /v2/promos */
public function store(Request $request): JsonResponse
{
$request->validate([
'promo_code' => 'required|string|max:14',
'amount' => 'required|string',
]);
$passengerId = $request->input('_jwt_user_id');
$exists = DB::connection('primary')->table('promos')
->where('passengerID', $passengerId)->exists();
if ($exists) {
return response()->json(['status' => 'failure', 'message' => 'Promo already assigned'], 409);
}
DB::connection('primary')->table('promos')->insert([
'promo_code' => $request->input('promo_code'),
'amount' => $request->input('amount'),
'description' => $request->input('description'),
'passengerID' => $passengerId,
'validity_start_date' => $request->input('start_date'),
'validity_end_date' => $request->input('end_date'),
]);
return response()->json(['status' => 'success'], 201);
}
/** PUT /v2/promos/{id} */
public function update(Request $request, int $id): JsonResponse
{
DB::connection('primary')->table('promos')
->where('id', $id)
->update(array_filter([
'promo_code' => $request->input('promo_code'),
'amount' => $request->input('amount'),
'description' => $request->input('description'),
'validity_end_date' => $request->input('end_date'),
]));
return response()->json(['status' => 'success']);
}
/** DELETE /v2/promos/{id} */
public function destroy(int $id): JsonResponse
{
DB::connection('primary')->table('promos')->where('id', $id)->delete();
return response()->json(['status' => 'success']);
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
/**
* Rating Controller
* Replaces: ride/rate/*.php
*/
class RatingController extends Controller
{
/** POST /v2/ratings/driver — passenger rates a driver */
public function rateDriver(Request $request): JsonResponse
{
$request->validate([
'driver_id' => 'required|string',
'ride_id' => 'required|integer',
'rating' => 'required|numeric|min:1|max:5',
'comment' => 'nullable|string|max:500',
]);
$passengerId = $request->input('_jwt_user_id');
// Prevent duplicate ratings
$exists = DB::connection('primary')->table('ratingDriver')
->where('ride_id', $request->input('ride_id'))->exists();
if ($exists) {
return response()->json(['status' => 'failure', 'message' => 'Already rated'], 409);
}
DB::connection('primary')->table('ratingDriver')->insert([
'passenger_id' => $passengerId,
'driver_id' => $request->input('driver_id'),
'ride_id' => $request->input('ride_id'),
'rating' => $request->input('rating'),
'comment' => $request->input('comment', ''),
'created_at' => now(),
]);
return response()->json(['status' => 'success'], 201);
}
/** POST /v2/ratings/passenger — driver rates a passenger */
public function ratePassenger(Request $request): JsonResponse
{
$request->validate([
'passenger_id' => 'required|string',
'ride_id' => 'required',
'rating' => 'required|numeric|min:1|max:5',
'comment' => 'nullable|string|max:500',
]);
$driverId = $request->input('_jwt_user_id');
$exists = DB::connection('primary')->table('ratingPassenger')
->where('rideId', $request->input('ride_id'))->exists();
if ($exists) {
return response()->json(['status' => 'failure', 'message' => 'Already rated'], 409);
}
DB::connection('primary')->table('ratingPassenger')->insert([
'passenger_id' => $request->input('passenger_id'),
'driverID' => $driverId,
'rideId' => $request->input('ride_id'),
'rating' => $request->input('rating'),
'comment' => $request->input('comment', ''),
'created_at' => now(),
]);
return response()->json(['status' => 'success'], 201);
}
/** POST /v2/ratings/app */
public function rateApp(Request $request): JsonResponse
{
$request->validate([
'rating' => 'required|numeric|min:1|max:5',
'comment' => 'nullable|string|max:300',
]);
$userId = $request->input('_jwt_user_id');
$userType = $request->input('_jwt_user_type');
DB::connection('primary')->table('ratingApp')->insert([
'name' => $request->input('name', ''),
'email' => $request->input('email', ''),
'phone' => $request->input('phone', ''),
'userId' => $userId,
'userType' => $userType,
'rating' => $request->input('rating'),
'comment' => $request->input('comment', ''),
'created_at' => now(),
]);
return response()->json(['status' => 'success'], 201);
}
/** GET /v2/ratings/driver/{id} */
public function driverRating(string $id): JsonResponse
{
$ratings = DB::connection('primary')->table('ratingDriver')
->where('driver_id', $id)
->orderBy('created_at', 'desc')
->limit(50)
->get();
$avg = DB::connection('primary')->table('ratingDriver')
->where('driver_id', $id)->avg('rating');
return response()->json([
'status' => 'success',
'data' => [
'average' => round($avg ?? 5.0, 2),
'count' => $ratings->count(),
'ratings' => $ratings,
],
]);
}
/** GET /v2/ratings/passenger/{id} */
public function passengerRating(string $id): JsonResponse
{
$ratings = DB::connection('primary')->table('ratingPassenger')
->where('passenger_id', $id)
->orderBy('created_at', 'desc')
->limit(50)
->get();
$avg = DB::connection('primary')->table('ratingPassenger')
->where('passenger_id', $id)->avg('rating');
return response()->json([
'status' => 'success',
'data' => [
'average' => round($avg ?? 5.0, 2),
'count' => $ratings->count(),
'ratings' => $ratings,
],
]);
}
}

View File

@@ -0,0 +1,544 @@
<?php
namespace App\Http\Controllers;
use App\Models\Ride;
use App\Models\Driver;
use App\Models\DriverToken;
use App\Models\PassengerToken;
use App\Models\DriverOrder;
use App\Models\CarLocation;
use App\Helpers\LegacyEncryption;
use App\Services\FcmService;
use App\Services\SocketService;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
/**
* Ride Controller
*
* Handles the complete ride lifecycle:
* create → search drivers → accept → arrive → start → finish/cancel
*/
class RideController extends Controller
{
private LegacyEncryption $encryption;
private FcmService $fcm;
private SocketService $socket;
public function __construct(LegacyEncryption $encryption, FcmService $fcm, SocketService $socket)
{
$this->encryption = $encryption;
$this->fcm = $fcm;
$this->socket = $socket;
}
/**
* POST /v2/rides
* Replaces: ride/rides/add_ride.php
*/
public function store(Request $request): JsonResponse
{
$request->validate([
'start_location' => 'required|string',
'end_location' => 'required|string',
'start_lat' => 'required|numeric',
'start_lng' => 'required|numeric',
'end_lat' => 'required|numeric',
'end_lng' => 'required|numeric',
'price' => 'required|numeric|min:0',
'car_type' => 'required|string',
'payment_method' => 'required|in:cash,visa',
'distance' => 'required|numeric',
]);
$passengerId = $request->input('_jwt_user_id');
// Prevent duplicate active rides
$activeRide = Ride::forPassenger($passengerId)->active()->first();
if ($activeRide) {
return response()->json([
'status' => 'failure',
'message' => 'You already have an active ride',
], 409);
}
// Begin transaction across both databases
DB::connection('ride')->beginTransaction();
try {
$ride = Ride::create([
'start_location' => $request->input('start_location'),
'end_location' => $request->input('end_location'),
'date' => now()->toDateString(),
'time' => now()->toTimeString(),
'endtime' => '00:00:00',
'price' => $request->input('price'),
'passenger_id' => $passengerId,
'driver_id' => 'none',
'status' => 'waiting',
'paymentMethod' => $request->input('payment_method', 'Cash'),
'carType' => $request->input('car_type', 'Speed'),
'price_for_passenger' => $request->input('price'),
'distance' => $request->input('distance'),
]);
// Also insert into waiting rides (for driver search)
DB::connection('primary')->table('waitingRides')->insert([
'id' => (string) $ride->id,
'start_location' => $request->input('start_location'),
'start_lat' => $request->input('start_lat'),
'start_lng' => $request->input('start_lng'),
'end_location' => $request->input('end_location'),
'end_lat' => $request->input('end_lat'),
'end_lng' => $request->input('end_lng'),
'date' => now()->toDateString(),
'time' => now()->toTimeString(),
'price' => $request->input('price'),
'passenger_id' => $passengerId,
'status' => 'waiting',
'carType' => $request->input('car_type', 'Speed'),
'passengerRate' => 5.0,
'price_for_passenger' => $request->input('price'),
'distance' => $request->input('distance'),
'duration' => $request->input('duration', '0'),
'payment_method' => $request->input('payment_method', 'cash'),
'passenger_wallet' => $request->input('wallet_balance', '0'),
]);
DB::connection('ride')->commit();
// Notify nearby drivers via socket
$this->socket->sendToLocationServer('new_ride', [
'ride_id' => $ride->id,
'lat' => $request->input('start_lat'),
'lng' => $request->input('start_lng'),
'car_type' => $request->input('car_type'),
]);
return response()->json([
'status' => 'success',
'data' => ['ride_id' => $ride->id],
], 201);
} catch (\Exception $e) {
DB::connection('ride')->rollBack();
return response()->json([
'status' => 'failure',
'message' => 'Failed to create ride',
], 500);
}
}
/**
* POST /v2/rides/{id}/accept
* Replaces: ride/rides/acceptRide.php
*/
public function accept(Request $request, int $rideId): JsonResponse
{
$driverId = $request->input('_jwt_user_id');
DB::connection('ride')->beginTransaction();
try {
// Lock the ride row to prevent race conditions
$ride = Ride::lockForUpdate()->find($rideId);
if (!$ride || !in_array($ride->status, ['waiting', 'wait', 'Apply'])) {
DB::connection('ride')->rollBack();
return response()->json([
'status' => 'failure',
'message' => 'Ride not available',
], 409);
}
// Update ride status atomically
$ride->update([
'driver_id' => $driverId,
'status' => 'Applied',
'DriverIsGoingToPassenger' => now(),
]);
// Update driver order
DriverOrder::where('order_id', (string) $rideId)
->where('driver_id', $driverId)
->update(['status' => 'accepted']);
// Remove from waiting rides
DB::connection('primary')
->table('waitingRides')
->where('id', (string) $rideId)
->update(['status' => 'Applied']);
// Sync to primary DB ride table
DB::connection('primary')
->table('ride')
->where('id', $rideId)
->update([
'driver_id' => $driverId,
'status' => 'Applied',
]);
DB::connection('ride')->commit();
// Notify passenger via FCM
$passengerToken = PassengerToken::where('passengerID', $ride->passenger_id)->first();
if ($passengerToken) {
$decryptedToken = $this->encryption->decrypt($passengerToken->token);
$this->fcm->sendToDevice(
$decryptedToken,
'تم قبول طلبك',
'السائق في الطريق إليك',
['ride_id' => (string) $rideId, 'status' => 'Applied'],
'ride_accepted'
);
}
// Notify passenger via socket
$this->socket->notifyPassenger($ride->passenger_id, [
'ride_id' => $rideId,
'status' => 'Applied',
'driver_id' => $driverId,
]);
return response()->json(['status' => 'success']);
} catch (\Exception $e) {
DB::connection('ride')->rollBack();
return response()->json([
'status' => 'failure',
'message' => 'Failed to accept ride',
], 500);
}
}
/**
* POST /v2/rides/{id}/start
* Replaces: ride/rides/start_ride.php
*/
public function start(Request $request, int $rideId): JsonResponse
{
$driverId = $request->input('_jwt_user_id');
$ride = Ride::where('id', $rideId)
->where('driver_id', $driverId)
->whereIn('status', ['Applied', 'Arrived'])
->first();
if (!$ride) {
return response()->json(['status' => 'failure', 'message' => 'Ride not found or not ready'], 404);
}
$ride->update([
'status' => 'Begin',
'rideTimeStart' => now(),
]);
// Sync to primary
DB::connection('primary')->table('ride')
->where('id', $rideId)
->update(['status' => 'Begin', 'rideTimeStart' => now()]);
// Notify passenger
$passengerToken = PassengerToken::where('passengerID', $ride->passenger_id)->first();
if ($passengerToken) {
$this->fcm->sendToDevice(
$this->encryption->decrypt($passengerToken->token),
'الرحلة بدأت',
'رحلة سعيدة!',
['ride_id' => (string) $rideId, 'status' => 'Begin'],
'ride_started'
);
}
$this->socket->notifyPassenger($ride->passenger_id, [
'ride_id' => $rideId,
'status' => 'Begin',
]);
return response()->json(['status' => 'success']);
}
/**
* POST /v2/rides/{id}/arrive
* Replaces: ride/rides/arrive_ride.php
*/
public function arrive(Request $request, int $rideId): JsonResponse
{
$driverId = $request->input('_jwt_user_id');
$ride = Ride::where('id', $rideId)
->where('driver_id', $driverId)
->where('status', 'Applied')
->first();
if (!$ride) {
return response()->json(['status' => 'failure', 'message' => 'Ride not found'], 404);
}
$ride->update(['status' => 'Arrived']);
DB::connection('primary')->table('ride')
->where('id', $rideId)->update(['status' => 'Arrived']);
$passengerToken = PassengerToken::where('passengerID', $ride->passenger_id)->first();
if ($passengerToken) {
$this->fcm->sendToDevice(
$this->encryption->decrypt($passengerToken->token),
'السائق وصل',
'السائق في انتظارك',
['ride_id' => (string) $rideId, 'status' => 'Arrived'],
'driver_arrived'
);
}
return response()->json(['status' => 'success']);
}
/**
* POST /v2/rides/{id}/finish
* Replaces: ride/rides/finish_ride_updates.php
*/
public function finish(Request $request, int $rideId): JsonResponse
{
$driverId = $request->input('_jwt_user_id');
$request->validate([
'price_for_driver' => 'required|numeric',
'price_for_passenger' => 'required|numeric',
'distance' => 'required|numeric',
]);
$ride = Ride::where('id', $rideId)
->where('driver_id', $driverId)
->where('status', 'Begin')
->first();
if (!$ride) {
return response()->json(['status' => 'failure', 'message' => 'Ride not found'], 404);
}
DB::connection('ride')->beginTransaction();
try {
$ride->update([
'status' => 'finish',
'rideTimeFinish' => now(),
'endtime' => now()->toTimeString(),
'price_for_driver' => $request->input('price_for_driver'),
'price_for_passenger' => $request->input('price_for_passenger'),
'distance' => $request->input('distance'),
]);
// Sync to primary
DB::connection('primary')->table('ride')
->where('id', $rideId)
->update([
'status' => 'finish',
'rideTimeFinish' => now(),
'price_for_driver' => $request->input('price_for_driver'),
'price_for_passenger' => $request->input('price_for_passenger'),
'distance' => $request->input('distance'),
]);
// Create payment record
DB::connection('primary')->table('payments')->insert([
'id' => uniqid('pay_'),
'amount' => $request->input('price_for_passenger'),
'payment_method' => $ride->paymentMethod,
'passengerID' => $ride->passenger_id,
'rideId' => (string) $rideId,
'driverID' => $driverId,
]);
DB::connection('ride')->commit();
// Notify passenger
$passengerToken = PassengerToken::where('passengerID', $ride->passenger_id)->first();
if ($passengerToken) {
$this->fcm->sendToDevice(
$this->encryption->decrypt($passengerToken->token),
'الرحلة انتهت',
'شكراً لاستخدامك انطلق',
[
'ride_id' => (string) $rideId,
'status' => 'finish',
'price' => (string) $request->input('price_for_passenger'),
],
'ride_finished'
);
}
return response()->json(['status' => 'success']);
} catch (\Exception $e) {
DB::connection('ride')->rollBack();
return response()->json(['status' => 'failure', 'message' => 'Failed to finish ride'], 500);
}
}
/**
* POST /v2/rides/{id}/cancel/passenger
* Replaces: ride/rides/cancel_ride_by_passenger.php
*/
public function cancelByPassenger(Request $request, int $rideId): JsonResponse
{
$passengerId = $request->input('_jwt_user_id');
$ride = Ride::where('id', $rideId)
->where('passenger_id', $passengerId)
->active()
->first();
if (!$ride) {
return response()->json(['status' => 'failure', 'message' => 'Ride not found'], 404);
}
$ride->update(['status' => 'CancelByPassenger']);
DB::connection('primary')->table('ride')
->where('id', $rideId)->update(['status' => 'CancelByPassenger']);
DB::connection('primary')->table('waitingRides')
->where('id', (string) $rideId)->update(['status' => 'CancelByPassenger']);
// Log cancellation
DB::connection('ride')->table('canecl')->insert([
'driverID' => $ride->driver_id ?? 'none',
'passengerID' => $passengerId,
'rideID' => (string) $rideId,
'note' => $request->input('reason', 'nothing'),
]);
// Notify driver if assigned
if ($ride->driver_id !== 'none') {
$driverToken = DriverToken::where('captain_id', $ride->driver_id)->first();
if ($driverToken) {
$this->fcm->sendToDevice(
$this->encryption->decrypt($driverToken->token),
'تم إلغاء الرحلة',
'الراكب ألغى الطلب',
['ride_id' => (string) $rideId, 'status' => 'CancelByPassenger'],
'ride_cancelled'
);
}
$this->socket->sendToLocationServer('cancel_ride', [
'driver_id' => $ride->driver_id,
'ride_id' => $rideId,
]);
}
return response()->json(['status' => 'success']);
}
/**
* POST /v2/rides/{id}/cancel/driver
* Replaces: ride/rides/cancel_ride_by_driver.php
*/
public function cancelByDriver(Request $request, int $rideId): JsonResponse
{
$driverId = $request->input('_jwt_user_id');
$ride = Ride::where('id', $rideId)
->where('driver_id', $driverId)
->active()
->first();
if (!$ride) {
return response()->json(['status' => 'failure', 'message' => 'Ride not found'], 404);
}
$ride->update(['status' => 'CancelByDriver', 'driver_id' => 'none']);
DB::connection('primary')->table('ride')
->where('id', $rideId)->update(['status' => 'CancelByDriver']);
// Re-add to waiting rides for re-search
DB::connection('primary')->table('waitingRides')
->where('id', (string) $rideId)->update(['status' => 'waiting']);
// Log cancellation
DB::connection('ride')->table('canecl')->insert([
'driverID' => $driverId,
'passengerID' => $ride->passenger_id,
'rideID' => (string) $rideId,
'note' => $request->input('reason', 'nothing'),
]);
// Notify passenger
$passengerToken = PassengerToken::where('passengerID', $ride->passenger_id)->first();
if ($passengerToken) {
$this->fcm->sendToDevice(
$this->encryption->decrypt($passengerToken->token),
'تم إلغاء الرحلة',
'السائق ألغى الطلب، جاري البحث...',
['ride_id' => (string) $rideId, 'status' => 'CancelByDriver'],
'ride_cancelled'
);
}
return response()->json(['status' => 'success']);
}
/**
* GET /v2/rides/{id}
* Replaces: ride/rides/getRideOrderID.php
*/
public function show(int $rideId): JsonResponse
{
$ride = Ride::find($rideId);
if (!$ride) {
return response()->json(['status' => 'failure', 'message' => 'Ride not found'], 404);
}
return response()->json(['status' => 'success', 'data' => $ride]);
}
/**
* GET /v2/rides/active
* Replaces: ride/rides/getRideStatusFromStartApp.php
*/
public function active(Request $request): JsonResponse
{
$userId = $request->input('_jwt_user_id');
$userType = $request->input('_jwt_user_type');
$query = Ride::active();
if ($userType === 'driver') {
$query->forDriver($userId);
} else {
$query->forPassenger($userId);
}
$ride = $query->orderBy('id', 'desc')->first();
return response()->json([
'status' => 'success',
'data' => $ride,
]);
}
/**
* GET /v2/rides
* Replaces: ride/rides/get.php
*/
public function index(Request $request): JsonResponse
{
$userId = $request->input('_jwt_user_id');
$userType = $request->input('_jwt_user_type');
$page = $request->input('page', 1);
$limit = min($request->input('limit', 20), 50);
$query = Ride::query();
if ($userType === 'driver') {
$query->forDriver($userId);
} else {
$query->forPassenger($userId);
}
$rides = $query->orderBy('id', 'desc')
->skip(($page - 1) * $limit)
->take($limit)
->get();
return response()->json(['status' => 'success', 'data' => $rides]);
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
/**
* Tracking Controller
* Replaces: ride/rides/get_driver_location.php, public_track_location.php, getRealTimeHeatmap.php
*/
class TrackingController extends Controller
{
/** GET /v2/tracking/driver/{rideId} */
public function driverLocation(Request $request, int $rideId): JsonResponse
{
$ride = DB::connection('ride')->table('ride')
->where('id', $rideId)
->whereIn('status', ['Applied', 'Arrived', 'Begin'])
->first();
if (!$ride) {
return response()->json(['status' => 'failure', 'message' => 'Ride not active'], 404);
}
$location = DB::connection('tracking')->table('car_locations')
->where('driver_id', $ride->driver_id)
->first();
if (!$location) {
return response()->json(['status' => 'failure', 'message' => 'Driver location not available'], 404);
}
return response()->json([
'status' => 'success',
'data' => [
'latitude' => $location->latitude,
'longitude' => $location->longitude,
'heading' => $location->heading,
'speed' => $location->speed,
'updated_at' => $location->updated_at,
],
]);
}
/**
* GET /v2/tracking/public/{rideId}?hash=XXX
* Public tracking link for parents/friends — uses HMAC hash instead of auth
*/
public function publicTrack(Request $request, int $rideId): JsonResponse
{
$hash = $request->input('hash');
if (!$hash) {
return response()->json(['status' => 'failure', 'message' => 'Missing hash'], 400);
}
// Verify hash: HMAC-SHA256(ride_id, secret_salt)
$expectedHash = hash_hmac('sha256', (string) $rideId, config('intaleq.secret_salt_parent'));
if (!hash_equals($expectedHash, $hash)) {
return response()->json(['status' => 'failure', 'message' => 'Invalid hash'], 403);
}
$ride = DB::connection('ride')->table('ride')
->where('id', $rideId)
->whereIn('status', ['Applied', 'Arrived', 'Begin'])
->first();
if (!$ride) {
return response()->json(['status' => 'failure', 'message' => 'Ride not active'], 404);
}
$location = DB::connection('tracking')->table('car_locations')
->where('driver_id', $ride->driver_id)
->first();
return response()->json([
'status' => 'success',
'data' => [
'latitude' => $location->latitude ?? null,
'longitude' => $location->longitude ?? null,
'heading' => $location->heading ?? null,
'ride_status' => $ride->status,
],
]);
}
/** GET /v2/tracking/heatmap */
public function heatmap(Request $request): JsonResponse
{
// Use spatial query for active drivers
$drivers = DB::connection('tracking')->table('car_locations')
->select('latitude', 'longitude', 'carType')
->where('status', 'on')
->where('updated_at', '>', now()->subMinutes(10))
->get();
return response()->json([
'status' => 'success',
'data' => $drivers,
]);
}
/** GET /v2/tracking/captain-stats */
public function captainStats(Request $request): JsonResponse
{
$driverId = $request->input('_jwt_user_id');
$totalRides = DB::connection('ride')->table('ride')
->where('driver_id', $driverId)
->where('status', 'finish')
->count();
$todayRides = DB::connection('ride')->table('ride')
->where('driver_id', $driverId)
->where('status', 'finish')
->whereDate('rideTimeFinish', today())
->count();
$todayEarnings = DB::connection('ride')->table('ride')
->where('driver_id', $driverId)
->where('status', 'finish')
->whereDate('rideTimeFinish', today())
->sum('price_for_driver');
$workHours = DB::connection('tracking')->table('driver_daily_summary')
->where('driver_id', $driverId)
->where('date', today()->toDateString())
->value('total_seconds') ?? 0;
return response()->json([
'status' => 'success',
'data' => [
'total_rides' => $totalRides,
'today_rides' => $todayRides,
'today_earnings' => round($todayEarnings, 2),
'today_work_seconds' => $workHours,
],
]);
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
/**
* Upload Controller
* Replaces: uploadImage.php, uploadImagePortrate.php, uploadImageType.php, etc.
*
* Security improvements over V1:
* - MIME type + magic bytes validation (not just extension)
* - Randomized filenames (prevents path traversal)
* - Max file size enforcement
* - No directory traversal possible
*/
class UploadController extends Controller
{
private const ALLOWED_IMAGE_MIMES = ['image/jpeg', 'image/png', 'image/webp'];
private const ALLOWED_AUDIO_MIMES = ['audio/mpeg', 'audio/mp4', 'audio/wav', 'audio/ogg'];
private const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB
private const MAX_AUDIO_SIZE = 10 * 1024 * 1024; // 10MB
/** POST /v2/uploads/card-image */
public function cardImage(Request $request): JsonResponse
{
return $this->handleImageUpload($request, 'card_images', 'cards');
}
/** POST /v2/uploads/profile-image */
public function profileImage(Request $request): JsonResponse
{
return $this->handleImageUpload($request, 'imageProfileCaptain', 'profiles');
}
/** POST /v2/uploads/document */
public function document(Request $request): JsonResponse
{
$request->validate([
'image' => 'required|file',
'doc_type' => 'required|string|in:license,registration,criminal,id_front,id_back',
]);
return $this->handleImageUpload($request, 'driver_documents', 'documents');
}
/** POST /v2/uploads/id-front */
public function idFront(Request $request): JsonResponse
{
return $this->handleImageUpload($request, 'card_images', 'ids/front');
}
/** POST /v2/uploads/id-back */
public function idBack(Request $request): JsonResponse
{
return $this->handleImageUpload($request, 'card_images', 'ids/back');
}
/** POST /v2/uploads/audio */
public function audio(Request $request): JsonResponse
{
$request->validate(['audio' => 'required|file']);
$file = $request->file('audio');
// Validate MIME
$mime = $file->getMimeType();
if (!in_array($mime, self::ALLOWED_AUDIO_MIMES)) {
return response()->json(['status' => 'failure', 'message' => 'Invalid audio format'], 400);
}
if ($file->getSize() > self::MAX_AUDIO_SIZE) {
return response()->json(['status' => 'failure', 'message' => 'File too large'], 400);
}
$userId = $request->input('_jwt_user_id');
$ext = $file->getClientOriginalExtension() ?: 'mp3';
$filename = 'audio_' . Str::random(24) . '.' . $ext;
$uploadPath = public_path('uploads/audio');
if (!is_dir($uploadPath)) mkdir($uploadPath, 0755, true);
$file->move($uploadPath, $filename);
$link = config('intaleq.upload_base_url') . '/uploads/audio/' . $filename;
return response()->json([
'status' => 'success',
'data' => ['link' => $link, 'filename' => $filename],
], 201);
}
/**
* Core image upload handler
*/
private function handleImageUpload(Request $request, string $table, string $subDir): JsonResponse
{
$request->validate(['image' => 'required|file']);
$file = $request->file('image');
// Validate MIME type
$mime = $file->getMimeType();
if (!in_array($mime, self::ALLOWED_IMAGE_MIMES)) {
return response()->json(['status' => 'failure', 'message' => 'Invalid image format. Allowed: JPG, PNG, WebP'], 400);
}
// Validate file size
if ($file->getSize() > self::MAX_IMAGE_SIZE) {
return response()->json(['status' => 'failure', 'message' => 'File too large (max 5MB)'], 400);
}
// Validate magic bytes (defense in depth)
$firstBytes = file_get_contents($file->getRealPath(), false, null, 0, 4);
if (!$this->validateMagicBytes($firstBytes, $mime)) {
return response()->json(['status' => 'failure', 'message' => 'File content does not match type'], 400);
}
// Generate safe filename
$userId = $request->input('_jwt_user_id');
$ext = match ($mime) {
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
default => 'jpg',
};
$filename = $subDir . '_' . Str::random(24) . '.' . $ext;
$uploadPath = public_path('uploads/' . $subDir);
if (!is_dir($uploadPath)) mkdir($uploadPath, 0755, true);
$file->move($uploadPath, $filename);
$link = config('intaleq.upload_base_url') . '/uploads/' . $subDir . '/' . $filename;
// Save to DB
$dbData = [
'driverID' => $userId,
'image_name' => $filename,
'link' => $link,
'upload_date' => now(),
];
if ($request->has('doc_type')) {
$dbData['doc_type'] = $request->input('doc_type');
}
DB::connection('ride')->table($table)->insert($dbData);
return response()->json([
'status' => 'success',
'data' => ['link' => $link, 'filename' => $filename],
], 201);
}
/**
* Validate file magic bytes match the declared MIME type
*/
private function validateMagicBytes(string $bytes, string $mime): bool
{
return match ($mime) {
'image/jpeg' => str_starts_with($bytes, "\xFF\xD8\xFF"),
'image/png' => str_starts_with($bytes, "\x89\x50\x4E\x47"),
'image/webp' => str_starts_with($bytes, "RIFF"),
default => false,
};
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
/**
* Wallet Controller
* Replaces: ride/passengerWallet/*.php, ride/driverWallet/*.php
*/
class WalletController extends Controller
{
/** GET /v2/wallet/passenger */
public function index(Request $request): JsonResponse
{
$id = $request->input('_jwt_user_id');
$wallet = DB::connection('primary')->table('passengerWallet')
->where('passenger_id', $id)->first();
return response()->json([
'status' => 'success',
'data' => $wallet ?? ['passenger_id' => $id, 'balance' => '0.00'],
]);
}
/** GET /v2/wallet/passenger/balance */
public function balance(Request $request): JsonResponse
{
$id = $request->input('_jwt_user_id');
$bal = DB::connection('primary')->table('passengerWallet')
->where('passenger_id', $id)->value('balance') ?? '0.00';
return response()->json(['status' => 'success', 'data' => ['balance' => $bal]]);
}
/** POST /v2/wallet/passenger */
public function addFunds(Request $request): JsonResponse
{
$request->validate([
'amount' => 'required|numeric|min:0.01',
'payment_method' => 'required|string',
]);
$id = $request->input('_jwt_user_id');
DB::connection('primary')->beginTransaction();
try {
$wallet = DB::connection('primary')->table('passengerWallet')
->where('passenger_id', $id)->lockForUpdate()->first();
if ($wallet) {
DB::connection('primary')->table('passengerWallet')
->where('passenger_id', $id)
->increment('balance', $request->input('amount'));
} else {
DB::connection('primary')->table('passengerWallet')->insert([
'passenger_id' => $id,
'balance' => $request->input('amount'),
]);
}
// Record transaction
DB::connection('primary')->table('passengerWalletTransactions')->insert([
'passenger_id' => $id,
'amount' => $request->input('amount'),
'type' => 'credit',
'payment_method' => $request->input('payment_method'),
'created_at' => now(),
]);
DB::connection('primary')->commit();
$newBalance = DB::connection('primary')->table('passengerWallet')
->where('passenger_id', $id)->value('balance');
return response()->json([
'status' => 'success',
'data' => ['balance' => $newBalance],
]);
} catch (\Exception $e) {
DB::connection('primary')->rollBack();
return response()->json(['status' => 'failure', 'message' => 'Transaction failed'], 500);
}
}
/** PUT /v2/wallet/passenger */
public function update(Request $request): JsonResponse
{
$request->validate(['balance' => 'required|numeric|min:0']);
$id = $request->input('_jwt_user_id');
DB::connection('primary')->table('passengerWallet')
->where('passenger_id', $id)
->update(['balance' => $request->input('balance')]);
return response()->json(['status' => 'success']);
}
/** DELETE /v2/wallet/passenger */
public function destroy(Request $request): JsonResponse
{
$id = $request->input('_jwt_user_id');
DB::connection('primary')->table('passengerWallet')
->where('passenger_id', $id)->delete();
return response()->json(['status' => 'success']);
}
/** GET /v2/wallet/passenger/transactions */
public function transactions(Request $request): JsonResponse
{
$id = $request->input('_jwt_user_id');
$page = (int) $request->input('page', 1);
$limit = min((int) $request->input('limit', 20), 50);
// Get from payments table (completed rides)
$payments = DB::connection('primary')->table('payments')
->where('passengerID', $id)
->orderBy('created_at', 'desc')
->skip(($page - 1) * $limit)
->take($limit)
->get();
return response()->json(['status' => 'success', 'data' => $payments]);
}
/** POST /v2/wallet/passenger/token */
public function addToken(Request $request): JsonResponse
{
$request->validate([
'token' => 'required|string',
'amount' => 'required|numeric|min:0.01',
]);
$id = $request->input('_jwt_user_id');
DB::connection('primary')->table('payment_tokens_passenger')->insert([
'token' => $request->input('token'),
'passengerId' => $id,
'dateCreated' => now(),
'amount' => $request->input('amount'),
'isUsed' => 0,
]);
return response()->json(['status' => 'success'], 201);
}
}