encryption = $encryption; } // ══════════════════════════════════════════════ // PASSENGER LOGIN & REGISTRATION // ══════════════════════════════════════════════ /** * POST /v2/auth/passenger/login * Replaces: loginJwtPassenger.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)); // Flexible phone lookup (encrypted/raw, international/local) $rawPhone = $phone; $localPhone = '0' . substr($phone, 3); $encRawPhone = $this->encryption->encrypt($rawPhone); $encLocalPhone = $this->encryption->encrypt($localPhone); $passenger = Passenger::active() ->whereIn('phone', [$rawPhone, $localPhone, $encRawPhone, $encLocalPhone]) ->first(); if (!$passenger) { return $this->failure('Invalid credentials'); } // HMAC password verification (V1 uses this for passengers) $storedPassword = $passenger->password; if (!password_verify($password, $storedPassword) && !hash_equals($storedPassword, hash_hmac('sha256', $password, config('intaleq.jwt_secret')))) { return $this->failure('Invalid credentials'); } // Verify fingerprint $passengerToken = PassengerToken::where('passengerID', $passenger->id)->first(); if ($passengerToken && $passengerToken->fingerPrint !== $fingerprint) { return $this->failure('Device mismatch'); } // Update FCM token $encryptedFcm = $this->encryption->encrypt($fcmToken); if ($passengerToken) { $passengerToken->update([ 'token' => $encryptedFcm, 'fingerPrint' => $fingerprint, ]); } else { PassengerToken::create([ 'token' => $encryptedFcm, 'passengerID' => $passenger->id, 'fingerPrint' => $fingerprint, ]); } // Generate API keys if missing if (empty($passenger->api_key)) { $this->generateApiKeys($passenger); } $jwt = $this->createJwt($passenger->id, 'passenger', $fingerprint, 86400); 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($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($gender), 'birthdate' => $this->encryption->encrypt($birthdate), 'site' => $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', $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 */ 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'); $encryptedPhone = $this->encryption->encrypt($phone); $driver = Driver::active()->where('phone', $encryptedPhone)->first(); if (!$driver) { return $this->failure('Invalid credentials'); } if (!password_verify($request->input('password'), $driver->password) && !hash_equals($driver->password, hash_hmac('sha256', $request->input('password'), config('intaleq.jwt_secret')))) { return $this->failure('Invalid credentials'); } $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, ]); } // ══════════════════════════════════════════════ // WALLET LOGIN // ══════════════════════════════════════════════ public function passengerWalletLogin(Request $request): JsonResponse { 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', ]); $secret = config('intaleq.jwt_secret'); $jwt = $this->createWalletJwt($request->input('id'), $request->input('fingerprint'), $request->input('aud'), 300, $secret); $hmac = hash_hmac('sha256', $request->input('id'), config('intaleq.wallet_hmac_secret')); return $this->success([ 'jwt' => $jwt, 'hmac' => $hmac, 'expires_in' => 300, ]); } // ══════════════════════════════════════════════ // ADMIN LOGIN // ══════════════════════════════════════════════ 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 || !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, ]); } public function adminWalletLogin(Request $request): JsonResponse { $request->validate([ 'id' => 'required|string', 'password' => 'required|string', 'fingerprint' => 'required|string', 'aud' => 'required|string', ]); $admin = DB::connection('primary')->table('adminUser')->where('id', $request->input('id'))->first(); if (!$admin) return $this->failure('Not found'); $jwt = $this->createWalletJwt((string)$admin->id, $request->input('fingerprint'), $request->input('aud'), 60); $hmac = hash_hmac('sha256', (string)$admin->id, config('intaleq.wallet_hmac_secret')); return $this->success([ 'jwt' => $jwt, 'hmac' => $hmac, 'expires_in' => 60, 'user_id' => $admin->id, ]); } // ══════════════════════════════════════════════ // GOOGLE LOGIN (Legacy V1 Compatibility) // ══════════════════════════════════════════════ public function passengerLoginGoogle(Request $request): JsonResponse { $request->validate([ 'email' => 'required|string', 'id' => 'required|string', ]); $email = $request->input('email'); $id = $request->input('id'); // Check if email is already encrypted (contains non-alphanumeric chars usually) $searchEmail = (str_contains($email, '+') || str_contains($email, '/')) ? $email : $this->encryption->encrypt($email); $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', ]) ->where('p.email', $searchEmail) ->where('p.id', $id) ->first(); if (!$row) { return response()->json(['status' => 'Failure', 'data' => 'User does not exist.']); } // Decrypt all fields for Flutter $data = (array) $row; foreach ($data as $key => $value) { if (is_string($value) && !in_array($key, ['id', 'status', 'created_at', 'updated_at', 'verified', 'isInstall', 'isGiftToken', 'api_key', 'api_secret'])) { $dec = $this->encryption->decrypt($value); if ($dec) $data[$key] = $dec; } } // Flutter expects data as a List return response()->json([ 'status' => 'success', 'count' => 1, 'data' => [$data], ]); } // ══════════════════════════════════════════════ // JWT HELPERS // ══════════════════════════════════════════════ private function createJwt(string $userId, string $userType, string $fingerprint, int $expiry, ?string $audience = null): string { $payload = [ 'user_id' => $userId, 'user_type' => $userType, 'fingerprint' => $fingerprint, 'iat' => time(), 'exp' => time() + $expiry, 'aud' => $audience ?? 'mobile-app', 'iss' => 'Tripz', 'jti' => bin2hex(random_bytes(16)), ]; return JWT::encode($payload, config('intaleq.jwt_secret'), 'HS256'); } private function createWalletJwt(string $userId, string $fingerprint, string $audience, int $expiry = 300, ?string $secret = null): string { $payload = [ 'user_id' => $userId, 'fingerPrint' => hash('sha256', $fingerprint . config('intaleq.fp_pepper', '')), 'exp' => time() + $expiry, 'iat' => time(), 'iss' => 'Tripz-Wallet', 'aud' => $audience, 'jti' => bin2hex(random_bytes(16)), ]; return JWT::encode($payload, $secret ?? config('intaleq.jwt_secret'), 'HS256'); } private function generateApiKeys($model) { $model->api_key = bin2hex(random_bytes(16)); $model->api_secret = bin2hex(random_bytes(32)); DB::connection('primary')->table($model->getTable()) ->where('idn', $model->idn ?? $model->id) ->update([ 'api_key' => $model->api_key, 'api_secret' => $model->api_secret ]); } }