diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index d09a91f..3d3175a 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -189,6 +189,15 @@ class AuthController extends Controller $passenger = Passenger::find($request->input('id')); if (!$passenger) return $this->failure('User not found'); + // Security Check: Verify fingerprint matches stored token + $storedToken = DB::connection('primary')->table('tokens') + ->where('passengerID', $passenger->id) + ->first(); + + if ($storedToken && !hash_equals((string)$storedToken->fingerPrint, (string)$request->input('fingerPrint'))) { + return $this->failure('Security mismatch: Invalid device fingerprint', 403); + } + if (empty($passenger->api_key)) { $this->generateApiKeys($passenger); } @@ -210,6 +219,15 @@ class AuthController extends Controller $driver = Driver::find($request->input('id')); if (!$driver) return $this->failure('User not found'); + // Security Check: Verify fingerprint matches stored token + $storedToken = DB::connection('primary')->table('captainToken') + ->where('captain_id', $driver->id) + ->first(); + + if ($storedToken && !hash_equals((string)$storedToken->fingerPrint, (string)$request->input('fingerPrint'))) { + return $this->failure('Security mismatch: Invalid device fingerprint', 403); + } + if (empty($driver->api_key)) { $this->generateApiKeys($driver); } diff --git a/app/Http/Controllers/InviteController.php b/app/Http/Controllers/InviteController.php index 3791ed0..402f9b5 100644 --- a/app/Http/Controllers/InviteController.php +++ b/app/Http/Controllers/InviteController.php @@ -88,7 +88,7 @@ class InviteController extends Controller } catch (\Exception $e) { return response()->json([ 'status' => 'failure', - 'message' => 'Database error: ' . $e->getMessage() + 'message' => 'An error occurred' ]); } } @@ -162,7 +162,7 @@ class InviteController extends Controller } catch (\Exception $e) { return response()->json([ 'status' => 'failure', - 'message' => 'Database error: ' . $e->getMessage() + 'message' => 'An error occurred' ]); } } diff --git a/app/Http/Controllers/MiscController.php b/app/Http/Controllers/MiscController.php index c37d314..433bfad 100644 --- a/app/Http/Controllers/MiscController.php +++ b/app/Http/Controllers/MiscController.php @@ -83,15 +83,8 @@ class MiscController extends Controller /** GET /v2/misc/help-center */ public function getHelpCenter(Request $request): JsonResponse { - $driverId = $request->input('driverID'); + $driverId = $request->attributes->get('_jwt_user_id'); - if (!$driverId) { - return response()->json([ - 'status' => 'failure', - 'message' => 'driverID is required' - ]); - } - $data = DB::connection('primary')->table('helpCenter') ->where('driverID', $driverId) ->orderBy('datecreated', 'desc') @@ -113,23 +106,15 @@ class MiscController extends Controller /** GET /v2/misc/tips */ public function getTips(Request $request): JsonResponse { - $driverId = $request->input('driverID'); - $passengerId = $request->input('passendgerID'); - - if (!$driverId && !$passengerId) { - return response()->json([ - 'status' => 'failure', - 'message' => 'driverID or passendgerID is required' - ]); - } + $userId = $request->attributes->get('_jwt_user_id'); + $userType = $request->attributes->get('_jwt_user_type'); $query = DB::connection('primary')->table('tips'); - if ($driverId) { - $query->where('driverID', $driverId); - } - if ($passengerId) { - $query->orWhere('passendgerID', $passengerId); + if ($userType === 'driver') { + $query->where('driverID', $userId); + } else { + $query->where('passengerID', $userId); } $data = $query->get(); @@ -168,17 +153,16 @@ class MiscController extends Controller /** POST /v2/misc/help-center */ public function storeHelpCenter(Request $request): JsonResponse { - $driverId = $request->input('driverID'); - $passengerId = $request->input('passengerID'); + $userId = $request->attributes->get('_jwt_user_id'); $helpQuestion = $request->input('helpQuestion'); - if ((!$driverId && !$passengerId) || !$helpQuestion) { - return response()->json(['status' => 'failure', 'message' => 'Missing parameters']); + if (!$helpQuestion) { + return response()->json(['status' => 'failure', 'message' => 'Missing help question']); } try { DB::connection('primary')->table('helpCenter')->insert([ - 'driverID' => $driverId ?? $passengerId, + 'driverID' => $userId, 'helpQuestion' => $helpQuestion, 'datecreated' => now() ]); @@ -193,12 +177,12 @@ class MiscController extends Controller /** POST /v2/misc/tips */ public function storeTips(Request $request): JsonResponse { - $passengerId = $request->input('passengerID'); + $passengerId = $request->attributes->get('_jwt_user_id'); // From JWT $driverId = $request->input('driverID'); $rideId = $request->input('rideID'); $tipAmount = $request->input('tipAmount'); - if (!$passengerId || !$driverId || !$rideId || !$tipAmount) { + if (!$driverId || !$rideId || !$tipAmount) { return response()->json(['status' => 'failure', 'message' => 'Missing parameters']); } diff --git a/app/Http/Controllers/NotificationController.php b/app/Http/Controllers/NotificationController.php index 9137e83..831a7e3 100644 --- a/app/Http/Controllers/NotificationController.php +++ b/app/Http/Controllers/NotificationController.php @@ -49,8 +49,12 @@ class NotificationController extends Controller $userType = $request->attributes->get('_jwt_user_type'); $table = $userType === 'driver' ? 'notificationCaptain' : 'notifications'; + $userId = $request->attributes->get('_jwt_user_id'); + $userField = $userType === 'driver' ? 'driverID' : 'passenger_id'; + DB::connection('primary')->table($table) ->where('id', $id) + ->where($userField, $userId) ->update(['isShown' => 'true']); return response()->json(['status' => 'success']); @@ -82,9 +86,10 @@ class NotificationController extends Controller 'affected' => $affected ]); } catch (\Throwable $e) { + \Log::error('NotificationController Update Error: ' . $e->getMessage()); return response()->json([ 'status' => 'failure', - 'message' => 'Internal error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() + 'message' => 'Internal server error occurred' ], 500); } } diff --git a/app/Http/Controllers/OtpController.php b/app/Http/Controllers/OtpController.php index 19c313f..bac3fe5 100644 --- a/app/Http/Controllers/OtpController.php +++ b/app/Http/Controllers/OtpController.php @@ -122,7 +122,6 @@ class OtpController extends Controller return $this->success([ 'message' => 'OTP process initiated', 'isRegistered' => !is_null($passenger), - 'passenger' => $passenger, 'expires_at' => $expiration->toIso8601String(), ]); } diff --git a/app/Http/Controllers/PromoController.php b/app/Http/Controllers/PromoController.php index 68ec261..caaf82d 100644 --- a/app/Http/Controllers/PromoController.php +++ b/app/Http/Controllers/PromoController.php @@ -24,8 +24,10 @@ class PromoController extends Controller $passengerId = $request->attributes->get('_jwt_user_id'); $promos = DB::connection('primary')->table('promos') - ->where('passengerID', $passengerId) - ->orWhere('passengerID', 'none') + ->where(function ($q) use ($passengerId) { + $q->where('passengerID', $passengerId) + ->orWhere('passengerID', 'none'); + }) ->where(function ($q) { $q->whereNull('validity_end_date') ->orWhere('validity_end_date', '>=', now()->toDateString()); @@ -93,8 +95,11 @@ class PromoController extends Controller /** PUT /v2/promos/{id} */ public function update(Request $request, int $id): JsonResponse { + $passengerId = $request->attributes->get('_jwt_user_id'); + DB::connection('primary')->table('promos') ->where('id', $id) + ->where('passengerID', $passengerId) ->update(array_filter([ 'promo_code' => $request->input('promo_code'), 'amount' => $request->input('amount'), @@ -106,9 +111,13 @@ class PromoController extends Controller } /** DELETE /v2/promos/{id} */ - public function destroy(int $id): JsonResponse + public function destroy(Request $request, int $id): JsonResponse { - DB::connection('primary')->table('promos')->where('id', $id)->delete(); + $passengerId = $request->attributes->get('_jwt_user_id'); + DB::connection('primary')->table('promos') + ->where('id', $id) + ->where('passengerID', $passengerId) + ->delete(); return response()->json(['status' => 'success']); } } diff --git a/app/Http/Controllers/RatingController.php b/app/Http/Controllers/RatingController.php index 7feab1e..6566e21 100644 --- a/app/Http/Controllers/RatingController.php +++ b/app/Http/Controllers/RatingController.php @@ -156,35 +156,23 @@ class RatingController extends Controller /** GET /v2/ratings/app — Legacy GET support */ public function getAppFeedback(Request $request): JsonResponse { - $passengerId = $request->input('passengerId'); - - if (!$passengerId) { - return response()->json(['status' => 'failure', 'message' => 'passengerId is required']); - } + $passengerId = $request->attributes->get('_jwt_user_id'); $data = DB::connection('primary')->table('feedBack') ->where('passengerId', $passengerId) - ->orderBy('datecreated', 'desc') ->get(); - - if ($data->isEmpty()) { - return response()->json(['status' => 'failure', 'message' => 'No feedback found']); - } - - return response()->json([ - 'status' => 'success', - 'message' => $data - ]); + + return response()->json(['status' => 'success', 'message' => $data]); } /** POST /v2/ratings/app — Legacy POST support */ public function storeAppFeedback(Request $request): JsonResponse { - $passengerId = $request->input('passengerId'); + $passengerId = $request->attributes->get('_jwt_user_id'); $feedBack = $request->input('feedBack'); - if (!$passengerId || !$feedBack) { - return response()->json(['status' => 'failure', 'message' => 'Missing parameters']); + if (!$feedBack) { + return response()->json(['status' => 'failure', 'message' => 'Missing feedback text']); } // V1 Encrypts this data @@ -200,7 +188,8 @@ class RatingController extends Controller return response()->json(['status' => 'success', 'message' => 'Feedback saved successfully']); } catch (\Exception $e) { - return response()->json(['status' => 'failure', 'message' => 'Database error: ' . $e->getMessage()]); + \Log::error('RatingController Feedback Error: ' . $e->getMessage()); + return response()->json(['status' => 'failure', 'message' => 'An error occurred while saving feedback']); } } } diff --git a/app/Http/Controllers/RideController.php b/app/Http/Controllers/RideController.php index 842e499..3e3e817 100644 --- a/app/Http/Controllers/RideController.php +++ b/app/Http/Controllers/RideController.php @@ -505,11 +505,18 @@ class RideController extends Controller * GET /v2/rides/{id} * Replaces: ride/rides/getRideOrderID.php */ - public function show(int $rideId): JsonResponse + public function show(Request $request, int $rideId): JsonResponse { - $ride = Ride::find($rideId); + $userId = $request->attributes->get('_jwt_user_id'); + $ride = Ride::where('id', $rideId) + ->where(function($q) use ($userId) { + $q->where('passenger_id', $userId) + ->orWhere('driver_id', $userId); + }) + ->first(); + if (!$ride) { - return response()->json(['status' => 'failure', 'message' => 'Ride not found'], 404); + return response()->json(['status' => 'failure', 'message' => 'Ride not found or access denied'], 404); } return response()->json(['status' => 'success', 'data' => $ride]); } diff --git a/app/Http/Controllers/SupportController.php b/app/Http/Controllers/SupportController.php new file mode 100644 index 0000000..cde0953 --- /dev/null +++ b/app/Http/Controllers/SupportController.php @@ -0,0 +1,217 @@ +encryption = $encryption; + } + + /** + * POST /v2/support/complaints + * يحلل الشكوى باستخدام الذكاء الاصطناعي (Gemini) ويحفظ النتيجة + */ + public function storeComplaint(Request $request): JsonResponse + { + $userId = $request->attributes->get('_jwt_user_id'); + + $request->validate([ + 'ride_id' => 'required|string', + 'complaint_text' => 'required|string', + 'audio_link' => 'nullable|string', + ]); + + $rideId = $request->input('ride_id'); + $complaintText = $request->input('complaint_text'); + $audioLink = $request->input('audio_link'); + + // 1. جلب بيانات الرحلة + $ride = DB::connection('ride')->table('ride') + ->where('id', $rideId) + ->first(); + + if (!$ride) { + return response()->json(['status' => 'failure', 'message' => 'Ride not found'], 404); + } + + $passengerId = $ride->passenger_id; + $driverId = $ride->driver_id; + + // 2. جلب الملفات التعريفية والسلوك + $passengerProfile = $this->getPassengerFullProfile($passengerId); + $driverProfile = $this->getDriverFullProfile($driverId); + $driverBehavior = DB::connection('tracking')->table('driver_behavior') + ->where('trip_id', $rideId) + ->where('driver_id', $driverId) + ->first(); + + // 3. بناء الـ Prompt لـ Gemini + $prompt = $this->buildGeminiPrompt($ride, $passengerProfile, $driverProfile, $driverBehavior, $complaintText, $audioLink); + + // 4. استدعاء Gemini API + $apiKey = config('services.gemini.key') ?? env('GEMINI_API_KEY'); + if (!$apiKey) { + Log::error('Gemini API Key missing in SupportController'); + // Fallback: save without AI if key is missing? + // Better to fail if AI is expected. + } + + try { + $response = Http::timeout(60)->post("https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key={$apiKey}", [ + 'contents' => [ + ['parts' => [['text' => $prompt]]] + ] + ]); + + if ($response->failed()) { + throw new \Exception('Gemini API failed: ' . $response->body()); + } + + $aiData = $response->json(); + $analysisRaw = $aiData['candidates'][0]['content']['parts'][0]['text'] ?? ''; + $analysisJson = trim(preg_replace('/```json|```/', '', $analysisRaw)); + $analysis = json_decode($analysisJson, true); + + if (!$analysis || !isset($analysis['passengerReport'])) { + throw new \Exception('Failed to parse AI response: ' . $analysisRaw); + } + + // 5. حفظ الشكوى والتحليل + $fullDescription = $complaintText . ($audioLink ? "\n\n[Audio Attached: {$audioLink}]" : ""); + + DB::connection('primary')->table('complaint')->insert([ + 'ride_id' => $rideId, + 'passenger_id' => $passengerId, + 'driver_id' => $driverId, + 'complaint_type' => $analysis['complaint_type'] ?? 'General', + 'description' => $fullDescription, + 'statusComplaint' => 'Resolved', + 'resolution' => $analysisJson, + 'passenger_report' => json_encode($analysis['passengerReport'], JSON_UNESCAPED_UNICODE), + 'driver_report' => json_encode($analysis['driverReport'], JSON_UNESCAPED_UNICODE), + 'cs_solutions' => json_encode($analysis['customerServiceSolutions'], JSON_UNESCAPED_UNICODE), + 'fault_determination' => $analysis['fault_determination'] ?? 'N/A', + 'complaint_nature' => $analysis['complaint_nature'] ?? 'N/A', + 'date_filed' => now(), + 'date_resolved' => now(), + ]); + + return response()->json([ + 'status' => 'success', + 'message' => 'Complaint processed successfully.', + 'passenger_response' => $analysis['passengerReport'], + 'driver_response' => $analysis['driverReport'] + ]); + + } catch (\Exception $e) { + Log::error('SupportController AI Error: ' . $e->getMessage()); + + // Fallback: Just save as pending if AI fails + DB::connection('primary')->table('complaint')->insert([ + 'ride_id' => $rideId, + 'passenger_id' => $passengerId, + 'driver_id' => $driverId, + 'complaint_type' => 'General', + 'description' => $complaintText . ($audioLink ? " [Audio: $audioLink]" : ""), + 'statusComplaint' => 'Open', + 'date_filed' => now(), + ]); + + return response()->json([ + 'status' => 'success', + 'message' => 'Complaint received (Manual Review required).', + 'passenger_response' => ['title' => 'تم استلام شكواك', 'body' => 'جاري مراجعة طلبك من قبل فريق الدعم.'], + 'driver_response' => ['title' => 'بلاغ جديد', 'body' => 'تم تسجيل بلاغ بخصوص رحلتك الأخيرة.'] + ]); + } + } + + private function getPassengerFullProfile($id) + { + $p = DB::connection('primary')->table('passengers')->where('id', $id)->first(); + if (!$p) return null; + + return [ + 'info' => [ + 'id' => $p->id, + 'full_name' => trim($this->encryption->decrypt($p->first_name) . ' ' . $this->encryption->decrypt($p->last_name)), + 'created_at' => $p->created_at + ], + 'ratings' => DB::connection('primary')->table('ratingPassenger')->where('passenger_id', $id) + ->selectRaw('AVG(rating) as avg_rating, COUNT(id) as total_ratings')->first(), + 'comments' => DB::connection('primary')->table('ratingPassenger')->where('passenger_id', $id) + ->whereNotNull('comment')->orderBy('created_at', 'desc')->limit(5)->pluck('comment') + ]; + } + + private function getDriverFullProfile($id) + { + $d = DB::connection('primary')->table('driver')->where('id', $id)->first(); + if (!$d) return null; + + return [ + 'info' => [ + 'id' => $d->id, + 'full_name' => trim($this->encryption->decrypt($d->first_name) . ' ' . $this->encryption->decrypt($d->last_name)), + 'created_at' => $d->created_at + ], + 'ratings' => DB::connection('primary')->table('ratingDriver')->where('driver_id', $id) + ->selectRaw('AVG(rating) as avg_rating, COUNT(id) as total_ratings')->first(), + 'comments' => DB::connection('primary')->table('ratingDriver')->where('driver_id', $id) + ->whereNotNull('comment')->orderBy('created_at', 'desc')->limit(5)->pluck('comment') + ]; + } + + private function buildGeminiPrompt($ride, $pProfile, $dProfile, $behavior, $text, $audio) + { + return " +أنت خبير في حل النزاعات في خدمات نقل الركاب لتطبيق intaleqapp.com. قم بتحليل الشكوى التالية بين راكب وسائق بناءً على البيانات الشاملة التالية: + +**1. تفاصيل الرحلة:** +" . json_encode($ride, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . " + +**2. ملف الراكب:** +" . json_encode($pProfile, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . " + +**3. ملف السائق:** +" . json_encode($dProfile, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . " + +**4. بيانات سلوك السائق (في هذه الرحلة):** +" . json_encode($behavior, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . " + +**5. الشكوى نفسها:** +- نص الشكوى من الراكب: '" . $text . "' +- رابط تسجيل صوتي للشكوى (إن وجد): " . $audio . " + +**مهمتك هي:** +1. تحليل جميع البيانات المتاحة لتحديد الطرف المخطئ على الأرجح. +2. تحديد ما إذا كانت الشكوى كيدية أم حقيقية. +3. **تصنيف الشكوى** (مثال: سلوك السائق، مشكلة في الأجرة، مسار الرحلة، حالة السيارة، أخرى). +4. اقتراح حلين واضحين ومختلفين لفريق خدمة العملاء. +5. كتابة تقرير موجز ومناسب للراكب. +6. كتابة تقرير موجز ومناسب للسائق. + +**الخرج المطلوب:** +أعد الرد بصيغة JSON فقط، بدون أي نصوص إضافية، وباللغة العربية (لهجة مصرية)، بالهيكل التالي: +{ + \"customerServiceSolutions\": [\"الحل المقترح الأول\", \"الحل المقترح الثاني\"], + \"passengerReport\": { \"title\": \"بخصوص شكوتك في رحلة Intaleq\", \"body\": \"رسالة واضحة للراكب بنتيجة الشكوى\" }, + \"driverReport\": { \"title\": \"بخصوص بلاغ رحلتك الأخيرة في Intaleq\", \"body\": \"رسالة واضحة للسائق بنتيجة الشكوى\" }, + \"fault_determination\": \"الطرف المخطئ (الراكب/السائق/كلاهما/غير واضح)\", + \"complaint_nature\": \"طبيعة الشكوى (حقيقية/كيدية/نزاع بسيط)\", + \"complaint_type\": \"تصنيف الشكوى الذي حددته\" +} +"; + } +} diff --git a/app/Http/Controllers/WalletController.php b/app/Http/Controllers/WalletController.php index fbb2f44..5fc176c 100644 --- a/app/Http/Controllers/WalletController.php +++ b/app/Http/Controllers/WalletController.php @@ -116,15 +116,6 @@ class WalletController extends Controller return response()->json(['status' => 'success']); } - /** DELETE /v2/wallet/passenger */ - public function destroy(Request $request): JsonResponse - { - $id = $request->attributes->get('_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 diff --git a/routes/api.php b/routes/api.php index be1673b..fb11f8a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -29,6 +29,7 @@ use App\Http\Controllers\NotificationController; use App\Http\Controllers\MiscController; use App\Http\Controllers\InviteController; use App\Http\Controllers\DriverDocController; +use App\Http\Controllers\SupportController; /* |-------------------------------------------------------------------------- @@ -68,9 +69,6 @@ Route::prefix('v2/auth')->group(function () { // Admin Error Logging (public — accepts error reports from Flutter apps) Route::post('v2/admin/errors', [MiscController::class, 'logClientError']); -Route::post('v2/notifications/token', [NotificationController::class, 'updateToken']); -Route::get('v2/notifications/token', [NotificationController::class, 'getToken']); - // OTP (public, but rate-limited) Route::prefix('v2/otp')->middleware('throttle:10,1')->group(function () { Route::post('/send', [OtpController::class, 'send']); @@ -150,6 +148,7 @@ Route::prefix('v2')->middleware(['hmac.auth', 'jwt.auth'])->group(function () { Route::get('/notifications', [NotificationController::class, 'index']); Route::post('/notifications/update', [NotificationController::class, 'updateNotification']); Route::get('/notifications/token', [NotificationController::class, 'getToken']); + Route::post('/notifications/token', [NotificationController::class, 'updateToken']); Route::put('/notifications/{id}/read', [NotificationController::class, 'markRead']); // ── Misc ── @@ -173,6 +172,9 @@ Route::prefix('v2')->middleware(['hmac.auth', 'jwt.auth'])->group(function () { Route::get('/driver/registration-car', [DriverDocController::class, 'getCarReg']); Route::post('/driver/registration-car', [DriverDocController::class, 'storeCarReg']); Route::post('/driver/scams', [DriverDocController::class, 'reportScam']); + + // ── Support ── + Route::post('/support/complaints', [SupportController::class, 'storeComplaint']); }); // ══════════════════════════════════════════════