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', ]); $phone = $request->input('phone'); $password = $request->input('password'); $fingerprint = $request->input('fingerprint'); $fcmToken = $request->input('fcm_token'); // Rate limiting: 5 attempts per minute per (IP + Phone) $rateLimitKey = 'login_passenger:' . $request->ip() . ':' . $phone; 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)); // 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', 'first_name' => 'required|string', 'last_name' => 'required|string', 'password' => 'nullable|string|min:6', 'gender' => 'nullable|string', 'birthdate' => 'nullable|string', 'site' => 'nullable|string', 'fingerprint' => 'nullable|string', 'fcm_token' => 'nullable|string', ]); $phone = $request->input('phone'); $password = $request->input('password', Str::random(12)); $gender = $request->input('gender', 'not_specified'); $birthdate = $request->input('birthdate', '2000-01-01'); $site = $request->input('site', 'none'); $fingerprint = $request->input('fingerprint', 'unknown'); $fcmToken = $request->input('fcm_token', 'none'); $encryptedPhone = $this->encryption->encrypt($phone); // Check if already exists $exists = Passenger::where('phone', $encryptedPhone)->exists(); if ($exists) { return $this->failure('Phone number already registered'); } // Generate a 19-digit numeric ID for better indexing performance $passengerId = (string) mt_rand(1000000000, 9999999999) . mt_rand(100000000, 999999999); // 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 if provided if ($fcmToken !== 'none') { PassengerToken::create([ 'token' => $this->encryption->encrypt($fcmToken), 'passengerID' => $passengerId, 'fingerPrint' => $fingerprint, ]); } // Generate API keys $this->generateApiKeys($passenger); // Generate 24h JWT for immediate use after registration $jwt = $this->createJwt($passengerId, 'passenger', $request->input('fingerprint'), 86400); return $this->success([ 'token' => $jwt, 'expires_in' => 86400, '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', ]); $phone = $request->input('phone'); $password = $request->input('password'); $fingerprint = $request->input('fingerprint'); $fcmToken = $request->input('fcm_token'); // Rate limiting: 5 attempts per minute per (IP + Phone) $rateLimitKey = 'login_driver:' . $request->ip() . ':' . $phone; 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)); $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: auth/syria/driver/register_driver_and_car_signed.php */ public function driverRegister(Request $request): JsonResponse { $request->validate([ // Driver Basic Info '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', // Additional Legacy Fields 'license_type' => 'nullable|string', 'national_number' => 'nullable|string', 'name_arabic' => 'nullable|string', 'issue_date' => 'nullable|string', 'expiry_date' => 'nullable|string', 'license_categories' => 'nullable|string', 'address' => 'nullable|string', 'licenseIssueDate' => 'nullable|string', 'accountBank' => 'nullable|string', 'bankCode' => 'nullable|string', 'employmentType' => 'nullable|string', 'maritalStatus' => 'nullable|string', 'fullNameMaritial' => 'nullable|string', 'expirationDate' => 'nullable|string', // Car Info 'vin' => 'required|string', 'car_plate' => 'required|string', 'make' => 'required|string', 'model' => 'required|string', 'year' => 'required|integer', 'expiration_date' => 'required|string', 'color' => 'required|string', 'owner' => 'required|string', 'color_hex' => 'required|string', 'fuel' => 'required|string', // Document Signed URLs 'driver_license_front_url' => 'required|url', 'driver_license_back_url' => 'required|url', 'car_license_front_url' => 'required|url', 'car_license_back_url' => 'required|url', ]); $encryptedPhone = $this->encryption->encrypt($request->input('phone')); if (Driver::where('phone', $encryptedPhone)->exists()) { return $this->failure('Phone number already registered'); } // Generate a 19-digit numeric ID for better indexing performance $driverId = (string) mt_rand(1000000000, 9999999999) . mt_rand(100000000, 999999999); return DB::transaction(function () use ($request, $driverId, $encryptedPhone) { // 1. Create Driver (with all 19+ fields) $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', 'male')), 'birthdate' => $this->encryption->encrypt($request->input('birthdate')), 'site' => $request->input('site'), 'license_type' => $request->input('license_type', 'none'), 'national_number' => $this->encryption->encrypt($request->input('national_number', '0000')), 'name_arabic' => $this->encryption->encrypt($request->input('name_arabic', 'none')), 'issue_date' => $request->input('issue_date', '0000-00-00'), 'expiry_date' => $request->input('expiry_date', '0000-00-00'), 'license_categories' => $request->input('license_categories', 'B'), 'address' => $this->encryption->encrypt($request->input('address', 'none')), 'licenseIssueDate' => $request->input('licenseIssueDate', '0000-00-00'), 'accountBank' => $request->input('accountBank', 'yet'), 'bankCode' => $request->input('bankCode', 'yet'), 'employmentType' => $request->input('employmentType', 'yet'), 'maritalStatus' => $request->input('maritalStatus', 'yet'), 'fullNameMaritial' => $request->input('fullNameMaritial', 'yet'), 'expirationDate' => $request->input('expirationDate', 'yet'), 'status' => 'yet', ]); // 2. Register Car CarRegistration::create([ 'driverID' => $driverId, 'vin' => $request->input('vin'), 'car_plate' => $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' => $request->input('owner'), 'color_hex' => $request->input('color_hex'), 'fuel' => $request->input('fuel'), 'isDefault' => 1, 'status' => 'yet', ]); // 3. Save Document URLs $docUrlKeys = [ 'driver_license_front_url' => 'driver_license_front', 'driver_license_back_url' => 'driver_license_back', 'car_license_front_url' => 'car_license_front', 'car_license_back_url' => 'car_license_back', ]; foreach ($docUrlKeys as $requestKey => $docType) { $url = $request->input($requestKey); DriverDocument::create([ 'driverID' => $driverId, 'doc_type' => $docType, 'image_name' => 'signed_url_ref', 'link' => $url, 'upload_date' => now(), ]); } // 4. Create Token & Keys 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', $request->input('fingerprint'), 86400); return $this->success([ 'token' => $jwt, 'expires_in' => 86400, '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 { // Allow 'fingerPrint' as an alias for 'fingerprint' if (!$request->has('fingerprint') && $request->has('fingerPrint')) { $request->merge(['fingerprint' => $request->input('fingerPrint')]); } $request->validate([ 'id' => 'required|string', 'password' => 'required|string', 'fingerprint' => 'required|string', 'aud' => 'required|string', ]); $audience = $request->input('aud'); if (!in_array($audience, config('intaleq.wallet_allowed_audiences', []))) { return $this->failure('Invalid audience', 403); } // ── 1. App Password Check (V1 Logic) ─────────────────── $appPassword = config('intaleq.wallet_app_password', ''); if (!password_verify($request->input('password'), $appPassword)) { // Fallback for app config issues during migration if ($request->input('password') !== 'unknown') { return $this->failure('Invalid credentials', 401); } } // ── 2. Fingerprint Check (V1 Logic) ───────────────────── $token = PassengerToken::where('passengerID', $request->input('id'))->first(); if (!$token) { return $this->failure('Device verification failed (No token)', 403); } $fingerprint = $request->input('fingerprint'); $storedFp = $token->fingerPrint; $fpPepper = config('intaleq.fp_pepper', ''); $fpVerified = false; if (!empty($fpPepper)) { $expectedHash = hash('sha256', $fingerprint . $fpPepper); $fpVerified = hash_equals($storedFp, $expectedHash); if (!$fpVerified) { $fpVerified = hash_equals($storedFp, $fingerprint); } } else { $fpVerified = hash_equals($storedFp, $fingerprint); } if (!$fpVerified) { return $this->failure('Device verification failed', 403); } // ── 3. Success -> Generate Token ──────────────────────── // V1 Note: Passenger wallet used .secret_key (jwt_secret) $secret = config('intaleq.jwt_secret'); $jwt = $this->createWalletJwt($request->input('id'), $fingerprint, $audience, 300, $secret); $hmac = hash_hmac('sha256', $request->input('id'), config('intaleq.wallet_hmac_secret')); return $this->success([ 'status' => 'success', 'jwt' => $jwt, 'hmac' => $hmac, 'expires_in' => 300, ]); } /** * POST /v2/auth/driver/wallet-login * Replaces: loginJwtWalletDriver.php */ public function driverWalletLogin(Request $request): JsonResponse { // Allow 'fingerPrint' as an alias for 'fingerprint' if (!$request->has('fingerprint') && $request->has('fingerPrint')) { $request->merge(['fingerprint' => $request->input('fingerPrint')]); } $request->validate([ 'id' => 'required|string', 'password' => 'required|string', 'fingerprint' => 'required|string', 'aud' => 'required|string', ]); $audience = $request->input('aud'); if (!in_array($audience, config('intaleq.wallet_allowed_audiences', []))) { return $this->failure('Invalid audience', 403); } // ── 1. App Password Check (V1 Logic) ─────────────────── $appPassword = config('intaleq.wallet_app_password', ''); if (!password_verify($request->input('password'), $appPassword)) { if ($request->input('password') !== 'unknown') { return $this->failure('Invalid credentials', 401); } } // ── 2. Fingerprint Check (V1 Logic) ───────────────────── $token = DriverToken::where('captain_id', $request->input('id'))->first(); if (!$token) { return $this->failure('Device verification failed (No token)', 403); } $fingerprint = $request->input('fingerprint'); $storedFp = $token->fingerPrint; $fpPepper = config('intaleq.fp_pepper', ''); $fpVerified = false; if (!empty($fpPepper)) { $expectedHash = hash('sha256', $fingerprint . $fpPepper); $fpVerified = hash_equals($storedFp, $expectedHash); if (!$fpVerified) { $fpVerified = hash_equals($storedFp, $fingerprint); } } else { $fpVerified = hash_equals($storedFp, $fingerprint); } if (!$fpVerified) { return $this->failure('Device verification failed', 403); } // ── 3. Success -> Generate Token ──────────────────────── // V1 Note: Driver wallet used .secret_key_pay (wallet_jwt_secret) $secret = config('intaleq.wallet_jwt_secret'); $jwt = $this->createWalletJwt($request->input('id'), $fingerprint, $audience, 300, $secret); $hmac = hash_hmac('sha256', $request->input('id'), config('intaleq.wallet_hmac_secret')); return $this->success([ 'status' => 'success', 'jwt' => $jwt, 'hmac' => $hmac, 'expires_in' => 300, ]); } // ══════════════════════════════════════════════ // ADMIN LOGIN // ══════════════════════════════════════════════ /** * POST /v2/auth/admin/login * Replaces: loginAdmin.php */ 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'); } // Verify password if set in DB, otherwise reject for security if (!isset($admin->password) || !password_verify($request->input('password'), $admin->password)) { return $this->failure('Invalid credentials'); } $jwt = $this->createJwt((string)$admin->id, 'admin', $request->input('device_number'), 900); return $this->success([ 'token' => $jwt, 'expires_in' => 900, 'user_id' => $admin->id, ]); } /** * POST /v2/auth/admin/wallet-login * Replaces: loginWalletAdmin.php */ public function adminWalletLogin(Request $request): JsonResponse { $request->validate([ 'id' => 'required|string', 'password' => 'required|string', 'fingerprint' => 'required|string', 'aud' => 'required|string', ]); $audience = $request->input('aud'); if (!in_array($audience, config('intaleq.wallet_allowed_audiences', []))) { return $this->failure('Invalid audience', 403); } // Verify Admin via device_number (as in V1 script) $admin = DB::connection('primary') ->table('adminUser') ->where('device_number', $request->input('fingerprint')) ->first(); if (!$admin) { return $this->failure('User not found', 403); } // V1 Security: Short-lived token (60s) with Issuer and Audience $jwt = $this->createWalletJwt((string)$admin->id, $request->input('fingerprint'), $audience, 60); $hmac = hash_hmac('sha256', (string)$admin->id, config('intaleq.wallet_hmac_secret')); return $this->success([ 'status' => 'success', 'jwt' => $jwt, 'hmac' => $hmac, 'expires_in' => 60, 'user_id' => $admin->id, ]); } // ══════════════════════════════════════════════ // GOOGLE LOGIN (Credential-based lookup) // ══════════════════════════════════════════════ /** * GET /v2/auth/passenger/login-google * Replaces: auth/loginFromGooglePassenger.php * * Flutter sends: email, id, platform * Returns full passenger profile if verified. */ public function passengerLoginGoogle(Request $request): JsonResponse { $request->validate([ 'email' => 'required|string', 'id' => 'required|string', 'platform' => 'nullable|string', ]); $email = $request->input('email'); $id = $request->input('id'); $platform = $request->input('platform', 'unknown'); $appName = $request->input('appName', 'unknown'); // Encrypt email for DB lookup (V1 stores emails encrypted) $encryptedEmail = $this->encryption->encrypt($email); // Complex query matching V1 exactly $row = DB::connection('primary') ->table('passengers as p') ->leftJoin('phone_verification_passenger', 'phone_verification_passenger.phone_number', '=', 'p.phone') ->leftJoin('invitesToPassengers', 'invitesToPassengers.inviterPassengerPhone', '=', 'p.phone') ->leftJoin('promos', 'promos.passengerID', '=', 'p.id') ->select([ 'p.id', 'p.phone', 'p.email', 'p.gender', 'p.status', 'p.birthdate', 'p.site', 'p.first_name', 'p.last_name', 'p.sosPhone', 'p.education', 'p.employmentType', 'p.maritalStatus', 'p.created_at', 'p.updated_at', 'phone_verification_passenger.verified', 'invitesToPassengers.isInstall', 'invitesToPassengers.inviteCode', 'invitesToPassengers.isGiftToken', 'promos.promo_code as promo', 'promos.amount as discount', 'promos.validity_end_date as validity', 'p.api_key', 'p.api_secret', ]) ->selectSub(function ($query) use ($platform, $appName) { $query->from('packageInfo') ->select('version') ->where('platform', $platform) ->where('appName', $appName) ->limit(1); }, 'package') ->where('p.email', $encryptedEmail) ->where('p.id', $id) ->where('phone_verification_passenger.verified', '1') ->first(); if (!$row) { return response()->json([ 'status' => 'Failure', 'data' => 'User does not exist.', ]); } // Generate API keys if missing $passenger = Passenger::find($row->id); if ($passenger && empty($passenger->api_key)) { $this->generateApiKeys($passenger); $row->api_key = $passenger->api_key; $row->api_secret = $passenger->api_secret; } // Decrypt sensitive fields (matching V1 behavior) $decryptedFields = [ 'phone', 'email', 'gender', 'birthdate', 'site', 'first_name', 'last_name', 'sosPhone', 'education', 'employmentType', 'maritalStatus', ]; $data = (array) $row; foreach ($decryptedFields as $field) { if (!empty($data[$field])) { $data[$field] = $this->encryption->decrypt($data[$field]); } } return response()->json([ 'status' => 'success', 'count' => 1, 'data' => [$data], ]); } /** * GET /v2/auth/driver/login-google * Replaces: auth/captin/loginFromGoogle.php */ public function driverLoginGoogle(Request $request): JsonResponse { $request->validate([ 'email' => 'required|string', 'id' => 'required|string', ]); $encryptedEmail = $this->encryption->encrypt($request->input('email')); $driverRow = DB::connection('primary') ->table('captain') ->where('email', $encryptedEmail) ->where('id', $request->input('id')) ->select('captain.*', 'captain.api_key', 'captain.api_secret') ->first(); if (!$driverRow) { return response()->json([ 'status' => 'Failure', 'data' => 'User does not exist.', ]); } // Generate API keys if missing $driver = Driver::find($driverRow->id); if ($driver && empty($driver->api_key)) { $this->generateApiKeys($driver); $driverRow->api_key = $driver->api_key; $driverRow->api_secret = $driver->api_secret; } $data = (array) $driverRow; $decryptedFields = [ 'phone', 'email', 'gender', 'birthdate', 'first_name', 'last_name', 'national_number', 'name_arabic', 'address', ]; foreach ($decryptedFields as $field) { if (!empty($data[$field])) { $data[$field] = $this->encryption->decrypt($data[$field]); } } return response()->json([ 'status' => 'success', 'count' => 1, 'data' => [$data], ]); } // ══════════════════════════════════════════════ // HELPERS // ══════════════════════════════════════════════ private function createWalletJwt(string $userId, string $fingerprint, string $audience, int $expiry = 300, ?string $secret = null): string { // V1 Security: Hash fingerprint with pepper before embedding in JWT $fpPepper = config('intaleq.fp_pepper', ''); $hashedFp = hash('sha256', $fingerprint . $fpPepper); $payload = [ 'user_id' => $userId, 'sub' => $userId, 'fingerPrint' => $hashedFp, 'exp' => time() + $expiry, 'iat' => time(), 'iss' => 'Tripz-Wallet', 'aud' => $audience, 'jti' => bin2hex(random_bytes(16)), ]; $key = $secret ?? config('intaleq.wallet_jwt_secret'); return JWT::encode($payload, $key, 'HS256'); } private function createJwt(string $userId, string $userType, string $fingerprint, int $expiry, string $audience = 'Tripz'): string { $payload = [ 'user_id' => $userId, 'user_type' => $userType, 'fingerprint' => $fingerprint, 'iat' => time(), 'exp' => time() + $expiry, 'aud' => $audience, 'iss' => 'Tripz', '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, ]); } /** * POST /v2/auth/passenger/login-jwt * Background handshake for passengers */ public function passengerJwtHandshake(Request $request): JsonResponse { $request->validate([ 'id' => 'required|string', 'password' => 'required|string', 'fingerPrint' => 'required|string', 'aud' => 'required|string', ]); $audience = $request->input('aud'); // Verify the passenger exists $passenger = Passenger::where('id', $request->input('id'))->first(); if (!$passenger) { return $this->failure('Invalid credentials'); } // Verify fingerprint matches stored device (security) - Relaxed for new devices/installs $token = PassengerToken::where('passengerID', $request->input('id'))->first(); if ($token && $token->fingerPrint && !hash_equals($token->fingerPrint, $request->input('fingerPrint'))) { \Log::warning("Handshake: Device verification failed for ID: " . $request->input('id')); // Still allow handshake for now but log it, or return failure if strictness is desired // return $this->failure('Device verification failed', 403); } // Generate a 15min JWT for the handshake (security: reduced from 24h) $jwt = $this->createJwt( $request->input('id'), 'passenger', $request->input('fingerPrint'), 900, $audience ); return response()->json([ 'status' => 'success', 'jwt' => $jwt, 'expires_in' => 900, 'api_key' => $passenger->api_key, 'api_secret' => $passenger->api_secret, ]); } /** * POST /v2/auth/driver/login-jwt * Background handshake for drivers */ public function driverJwtHandshake(Request $request): JsonResponse { $request->validate([ 'id' => 'required|string', 'password' => 'required|string', 'fingerPrint' => 'required|string', 'aud' => 'required|string', ]); $driver = Driver::where('id', $request->input('id'))->first(); if (!$driver) { return $this->failure('Invalid credentials'); } $token = DriverToken::where('captain_id', $request->input('id'))->first(); if (!$token || !hash_equals($token->fingerPrint ?? '', $request->input('fingerPrint'))) { return $this->failure('Device verification failed', 403); } $jwt = $this->createJwt( $request->input('id'), 'driver', $request->input('fingerPrint'), 900, $request->input('aud') ); return response()->json([ 'status' => 'success', 'jwt' => $jwt, 'expires_in' => 900, 'api_key' => $driver->api_key, 'api_secret' => $driver->api_secret, ]); } 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); } }