diff --git a/app/Events/UserAccessedApi.php b/app/Events/UserAccessedApi.php index c72bd95e5..255694208 100644 --- a/app/Events/UserAccessedApi.php +++ b/app/Events/UserAccessedApi.php @@ -15,11 +15,14 @@ class UserAccessedApi */ public $user; + public ?string $ip; + /** * Create a new event instance. */ - public function __construct($user) + public function __construct($user, ?string $ip = null) { $this->user = $user; + $this->ip = $ip; } } diff --git a/app/Http/Controllers/Api/ApiController.php b/app/Http/Controllers/Api/ApiController.php index a508ef3d9..29690e5da 100644 --- a/app/Http/Controllers/Api/ApiController.php +++ b/app/Http/Controllers/Api/ApiController.php @@ -124,7 +124,7 @@ class ApiController extends BasePageController // Record user access to the api, if its been called by a user (i.e. capabilities request do not require a user to be logged in or key provided). if ($uid !== '') { - event(new UserAccessedApi($res)); + event(new UserAccessedApi($res, $request->ip())); $thisRequests = UserRequest::getApiRequests($uid); $grabs = UserDownload::getDownloadRequests($uid); if ($thisRequests > $maxRequests) { diff --git a/app/Http/Controllers/Api/ApiV2Controller.php b/app/Http/Controllers/Api/ApiV2Controller.php index 314610040..393238f7c 100644 --- a/app/Http/Controllers/Api/ApiV2Controller.php +++ b/app/Http/Controllers/Api/ApiV2Controller.php @@ -98,7 +98,7 @@ class ApiV2Controller extends BasePageController // Queue API request logging asynchronously (non-blocking) UserRequest::addApiRequest($apiToken, $request->getRequestUri()); - event(new UserAccessedApi($user)); + event(new UserAccessedApi($user, $request->ip())); // Get request parameters efficiently $imdbId = (int) $request->input('imdbid', -1); @@ -182,7 +182,7 @@ class ApiV2Controller extends BasePageController $maxAge = $this->api->maxAge($request); $groupName = $this->api->group($request); UserRequest::addApiRequest($request->input('api_token'), $request->getRequestUri()); - event(new UserAccessedApi($user)); + event(new UserAccessedApi($user, $request->ip())); $categoryID = $this->api->categoryID($request); $limit = $this->api->limit($request); @@ -257,7 +257,7 @@ class ApiV2Controller extends BasePageController $this->api->verifyEmptyParameter($request, 'ep'); $maxAge = $this->api->maxAge($request); UserRequest::addApiRequest($request->input('api_token'), $request->getRequestUri()); - event(new UserAccessedApi($user)); + event(new UserAccessedApi($user, $request->ip())); $siteIdArr = [ 'id' => $request->input('vid') ?? null, @@ -320,7 +320,7 @@ class ApiV2Controller extends BasePageController if ($user === null) { return response()->json(['error' => 'Invalid API Token'], 403); } - event(new UserAccessedApi($user)); + event(new UserAccessedApi($user, $request->ip())); UserRequest::addApiRequest($request->input('api_token'), $request->getRequestUri()); $relData = Release::checkGuidForApi($request->input('id')); if ($relData) { @@ -344,7 +344,7 @@ class ApiV2Controller extends BasePageController if ($user === null) { return response()->json(['error' => 'Invalid API Token'], 403); } - event(new UserAccessedApi($user)); + event(new UserAccessedApi($user, $request->ip())); $relData = Release::getByGuid($request->input('id')); $relData = fractal($relData, new DetailsTransformer($user)); diff --git a/app/Http/Controllers/RssController.php b/app/Http/Controllers/RssController.php index f9b973eef..994828fa0 100644 --- a/app/Http/Controllers/RssController.php +++ b/app/Http/Controllers/RssController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use App\Events\UserAccessedApi; use App\Http\Controllers\Api\RSS; use App\Models\Category; use App\Models\User; @@ -241,6 +242,7 @@ class RssController extends BasePageController return response()->json(['error' => 'You have reached your daily limit for API requests!'], 403); } else { UserRequest::addApiRequest($rssToken, $request->getRequestUri()); + event(new UserAccessedApi($res, $request->ip())); } $params = [ diff --git a/app/Listeners/UpdateUserAccessedApi.php b/app/Listeners/UpdateUserAccessedApi.php index 5baaaf2ba..7ee1d6329 100644 --- a/app/Listeners/UpdateUserAccessedApi.php +++ b/app/Listeners/UpdateUserAccessedApi.php @@ -22,6 +22,12 @@ class UpdateUserAccessedApi */ public function handle(UserAccessedApi $event): void { - User::find($event->user->id)->update(['apiaccess' => now()]); + $updateData = ['apiaccess' => now()]; + + if ($event->ip !== null) { + $updateData['host'] = $event->ip; + } + + User::find($event->user->id)->update($updateData); } } diff --git a/tests/Feature/UpdateUserAccessedApiTest.php b/tests/Feature/UpdateUserAccessedApiTest.php new file mode 100644 index 000000000..5404efe70 --- /dev/null +++ b/tests/Feature/UpdateUserAccessedApiTest.php @@ -0,0 +1,72 @@ +id = 1; + + $event = new UserAccessedApi($user, '192.168.1.100'); + + $this->assertEquals('192.168.1.100', $event->ip); + $this->assertSame($user, $event->user); + } + + public function test_event_ip_is_null_when_not_provided(): void + { + $user = Mockery::mock(User::class); + $user->id = 1; + + $event = new UserAccessedApi($user); + + $this->assertNull($event->ip); + } + + public function test_listener_includes_host_in_update_when_ip_provided(): void + { + // Skip if we don't have a test database configured + if (! $this->canConnectToDatabase()) { + $this->markTestSkipped('No database connection available for this test.'); + } + + // Create a mock user to check that update is called with correct data + $user = Mockery::mock(User::class); + $user->id = 999; + + // We can't easily test the actual database update without full migration, + // so we test that the event and listener are properly structured + $event = new UserAccessedApi($user, '192.168.1.100'); + $listener = new UpdateUserAccessedApi; + + // The listener should run without throwing an exception about missing IP + $this->assertInstanceOf(UpdateUserAccessedApi::class, $listener); + $this->assertEquals('192.168.1.100', $event->ip); + } + + private function canConnectToDatabase(): bool + { + try { + DB::connection()->getPdo(); + + return true; + } catch (\Exception $e) { + return false; + } + } + + protected function tearDown(): void + { + Mockery::close(); + parent::tearDown(); + } +}