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', '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'); } // 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 PassengerToken::create([ 'token' => $this->encryption->encrypt($request->input('fcm_token')), 'passengerID' => $passengerId, 'fingerPrint' => $request->input('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 { $request->validate([ 'phone' => '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); } // 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'); } // V1 Security: Verify device fingerprint $token = PassengerToken::where('passengerID', $passenger->id)->first(); if (!$token || $token->fingerPrint !== $request->input('fingerprint')) { return $this->failure('Device fingerprint verification failed', 403); } // V1 Security: Short-lived token (60s) with Issuer and Audience $jwt = $this->createWalletJwt($passenger->id, $request->input('fingerprint'), $audience, 60); $hmac = hash_hmac('sha256', $passenger->id, config('intaleq.wallet_hmac_secret')); return $this->success([ 'status' => 'success', 'jwt' => $jwt, 'hmac' => $hmac, 'expires_in' => 60, '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', 'aud' => 'required|string', ]); $audience = $request->input('aud'); if (!in_array($audience, config('intaleq.wallet_allowed_audiences', []))) { return $this->failure('Invalid audience', 403); } // Rate limit $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'); } // V1 Security: Verify device fingerprint $token = DriverToken::where('captain_id', $driver->id)->first(); if (!$token || $token->fingerPrint !== $request->input('fingerprint')) { return $this->failure('Device fingerprint verification failed', 403); } // V1 Security: Short-lived token (60s) with Issuer and Audience $jwt = $this->createWalletJwt($driver->id, $request->input('fingerprint'), $audience, 60); $hmac = hash_hmac('sha256', $driver->id, config('intaleq.wallet_hmac_secret')); return $this->success([ 'status' => 'success', 'jwt' => $jwt, 'hmac' => $hmac, 'expires_in' => 60, 'user_id' => $driver->id, ]); } // ══════════════════════════════════════════════ // 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'); } $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, ]); } // ══════════════════════════════════════════════ // HELPERS // ══════════════════════════════════════════════ private function createWalletJwt(string $userId, string $fingerprint, string $audience, int $expiry = 60): 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, 'fingerPrint' => $hashedFp, 'exp' => time() + $expiry, 'iat' => time(), 'iss' => 'Tripz-Wallet', 'aud' => $audience ]; return JWT::encode($payload, config('intaleq.wallet_jwt_secret'), '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, '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'); // Validate audience if needed (optional based on audio but good for security) // if (!in_array($audience, config('intaleq.allowed_audiences'))) { ... } // The user mentioned using a fixed password like 'passenger' from Flutter // and relying on fingerprint for security. // Generate a 24h JWT for the handshake (as requested to be consistent) $jwt = $this->createJwt( $request->input('id'), 'passenger', $request->input('fingerPrint'), 86400, $audience ); return response()->json([ 'status' => 'success', 'jwt' => $jwt, 'expires_in' => 86400 ]); } /** * 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', ]); $jwt = $this->createJwt( $request->input('id'), 'driver', $request->input('fingerPrint'), 86400, $request->input('aud') ); return response()->json([ 'status' => 'success', 'jwt' => $jwt, 'expires_in' => 86400 ]); } 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); } }