├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── config.php ├── database └── migrations │ ├── create_bans_table.php │ └── metas_field_to_bans_table.php └── src ├── Banhammer.php ├── BanhammerServiceProvider.php ├── Commands ├── ClearBans.php └── DeleteExpired.php ├── Events ├── ModelWasBanned.php └── ModelWasUnbanned.php ├── Exceptions └── BanhammerException.php ├── IP.php ├── Middleware ├── AuthBanned.php ├── BlockByCountry.php ├── IPBanned.php └── LogoutBanned.php ├── Models └── Ban.php ├── Observers └── BanObserver.php ├── Services └── IpApiService.php └── Traits └── Bannable.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `banhammer` will be documented in this file. 4 | 5 | ## v2.3.2 - 2024-09-20 6 | 7 | - The banned scope now accepts a boolean parameter to retrieve unbanned models as well, e.g., `Model::banned(false)`. Big thanks to @onlime for the contribution! 8 | 9 | ## v2.3.1 - 2024-08-23 10 | 11 | - PR #15 Fix the return type for the fallback_url. Thanks to @dannydinges 12 | 13 | ## v2.3.0 - 2024-05-24 14 | 15 | - Added UUID support @KieranLProctor 16 | - Added Configurable model support @KieranLProctor 17 | 18 | ## v2.2.0 - 2024-03-12 19 | 20 | - Laravel 11 support 21 | 22 | ## v2.1.0 - 2024-01-07 23 | 24 | - [feature request]: Expiration date for banned IPs #10 25 | Is it now possible to add an expiration date when banning an IP (or multiple) 26 | 27 | ``` 28 | IP::ban("8.8.8.8", [], now()->addMinutes(10)); 29 | 30 | 31 | 32 | 33 | ``` 34 | ## v2.0.0 - 2024-01-07 35 | 36 | This new version introduce the block by country middleware 37 | 38 | ## v1.2.0 - 2023-03-02 39 | 40 | - Adding Metas (cutom properties) to bans. @mepsd 41 | - You may have to run `php artisan migrate` if you are upgrading from v1.1.x 42 | 43 | ## v1.1.5 - 2023-02-21 44 | 45 | - Fix : Update cache on unban expired command 46 | 47 | ## v1.1.4 - 2023-02-21 48 | 49 | - Adding created by relation in IPs collection. 50 | - Removing ID in IPs collection. 51 | - Grouping by IPs to prevent duplicate IPs with the banned() method on IP. 52 | - Caching IP list for better performances 53 | 54 | ## v1.1.3 - 2023-02-13 55 | 56 | - Fix nullable attribute expired_at 57 | 58 | ## v1.1.2 - 2023-02-13 59 | 60 | - Fix missing alias middleware 61 | 62 | ## v1.1.1 - 2023-02-13 63 | 64 | - New `logout.banned` middleware 65 | - Removing auto logging out on `auth.banned`middleware 66 | 67 | ## v1.1.0 - 2023-02-12 68 | 69 | - New IP Class 70 | - Scopes expired(), notExpired() 71 | - Documentation 72 | 73 | ## v1.0.0 - 2023-02-12 74 | 75 | First release 76 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) mchev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Banhammer, a Model, IP and Country ban package for Laravel 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/mchev/banhammer.svg?style=flat-square)](https://packagist.org/packages/mchev/banhammer) 4 | [![GitHub Tests Action Status](https://github.com/laravel/pint/workflows/tests/badge.svg)](https://github.com/mchev/banhammer/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/mchev/banhammer.svg?style=flat-square)](https://packagist.org/packages/mchev/banhammer) 6 | [![Package for laravel](https://img.shields.io/badge/Package%20for%20Laravel-grey.svg?style=flat-square&logo=laravel&logoColor=white)](https://packagist.org/packages/mchev/banhammer) 7 | 8 | Banhammer for Laravel offers a very simple way to ban any Model by ID and by IP. It also allows to block requests by IP addresses. 9 | 10 | Banned models can have an expiration date and will be automatically unbanned using the Scheduler. 11 | 12 | ## Table of Contents 13 | 1. [Introduction](#banhammer-a-model-and-ip-ban-package-for-laravel) 14 | 2. [Version Compatibility](#version-compatibility) 15 | 3. [Installation](#installation) 16 | 4. [Usage](#usage) 17 | - [Making a Model Bannable](#usage) 18 | - [Ban / Unban](#ban--unban) 19 | - [IP](#ip) 20 | - [Metas](#metas) 21 | - [Blocking Access from Specific Countries](#blocking-access-from-specific-countries) 22 | - [Middleware](#middleware) 23 | - [Scheduler](#scheduler) 24 | - [Events](#events) 25 | - [Miscellaneous](#misc) 26 | 5. [UUIDs](#uuids) 27 | 6. [Upgrading To 2.0 from 1.x](#upgrading-to-20-from-1x) 28 | 7. [Testing](#testing) 29 | 8. [Changelog](#changelog) 30 | 9. [Roadmap / Todo](#roadmap--todo) 31 | 10. [Contributing](#contributing) 32 | 11. [Security Vulnerabilities](#security-vulnerabilities) 33 | 12. [Credits](#credits) 34 | 13. [License](#license) 35 | 36 | ## Version Compatibility 37 | 38 | Compatible with Laravel 9, 10, 11 and 12. 39 | 40 | ## Installation 41 | 42 | You can install the package via composer: 43 | 44 | ```bash 45 | composer require mchev/banhammer 46 | ``` 47 | 48 | Then publish and run the migrations with: 49 | 50 | > To use UUIDs see [UUIDs](#uuids) 51 | 52 | ```bash 53 | php artisan vendor:publish --provider="Mchev\Banhammer\BanhammerServiceProvider" --tag="migrations" 54 | php artisan migrate 55 | ``` 56 | 57 | You can publish the config file with: 58 | 59 | ```bash 60 | php artisan vendor:publish --provider="Mchev\Banhammer\BanhammerServiceProvider" --tag="config" 61 | ``` 62 | 63 | It is possible to define the table name, the model and the fallback_url in the `config/ban.php` file. 64 | 65 | ## Usage 66 | 67 | To make a model bannable, add the `Mchev\Banhammer\Traits\Bannable` trait to the model: 68 | ```php 69 | You can add the Bannable trait on as many models as you want (Team, Group, User, etc.). 82 | 83 | ### Ban / Unban 84 | 85 | Simple ban 86 | ```php 87 | $user->ban(); 88 | ``` 89 | 90 | > Without the expired_at attribute specified, the user will be banned forever. 91 | 92 | IP Ban 93 | ```php 94 | $user->ban([ 95 | 'ip' => $user->ip, 96 | ]); 97 | ``` 98 | 99 | Full 100 | > All attributes are optional 101 | ```php 102 | $model->ban([ 103 | 'created_by_type' => 'App\Models\User', 104 | 'created_by_id' => 1, 105 | 'comment' => "You've been evil", 106 | 'ip' => "8.8.8.8", 107 | 'expired_at' => Carbon::now()->addDays(7), 108 | 'metas' => [ 109 | 'route' => request()->route()->getName(), 110 | 'user_agent' => request()->header('user-agent') 111 | ] 112 | ]); 113 | ``` 114 | 115 | Shorthand 116 | ```php 117 | $user->banUntil('2 days'); 118 | ``` 119 | 120 | Check if model is banned. 121 | > You can create custom middlewares using these methods. 122 | ```php 123 | $model->isBanned(); 124 | $model->isNotBanned(); 125 | ``` 126 | 127 | List model bans 128 | ```php 129 | // All model bans 130 | $bans = $model->bans()->get(); 131 | 132 | // Expired bans 133 | $expired = $model->bans()->expired()->get(); 134 | 135 | // Not expired and permanent bans 136 | $notExpired = $model->bans()->notExpired()->get(); 137 | ``` 138 | 139 | Filters 140 | ```php 141 | $bannedTeams = Team::banned()->get(); 142 | $notBannedTeams = Team::notBanned()->get(); 143 | ``` 144 | 145 | > Alternatively to `notBanned()` you may also use the `banned()` scope to filter not-banned models: `Team::banned(false)`. Like this, you could simply use the `banned` scope for e.g. [spatie/laravel-query-builder](https://spatie.be/docs/laravel-query-builder/v5/features/filtering#content-scope-filters) [Scope Filters](https://spatie.be/docs/laravel-query-builder/v5/features/filtering#content-scope-filters), instead of using more complex ways to apply either `banned` or `notBanned` scopes. 146 | 147 | Unban 148 | 149 | ```php 150 | $user->unban(); 151 | ``` 152 | 153 | ### IP 154 | 155 | Ban IPs 156 | ```php 157 | use Mchev\Banhammer\IP; 158 | 159 | IP::ban("8.8.8.8"); 160 | IP::ban(["8.8.8.8", "4.4.4.4"]); 161 | 162 | // Ban IP with expiration date 163 | IP::ban("8.8.8.8", [], now()->addMinutes(10)); 164 | 165 | // Full 166 | IP::ban( 167 | "8.8.8.8", 168 | [ 169 | "MetaKey1" => "MetaValue1", 170 | "MetaKey2" => "MetaValue2", 171 | ], 172 | now()->addMinutes(10) 173 | ); 174 | ``` 175 | 176 | Unban IPs 177 | ```php 178 | use Mchev\Banhammer\IP; 179 | 180 | IP::unban("8.8.8.8"); 181 | IP::unban(["8.8.8.8", "4.4.4.4"]); 182 | ``` 183 | 184 | List all banned IPs 185 | ```php 186 | use Mchev\Banhammer\IP; 187 | 188 | $ips = IP::banned()->get(); // Collection 189 | $ips = IP::banned()->pluck('ip')->toArray(); // Array 190 | ``` 191 | 192 | ### Metas 193 | 194 | Ban IP with metas 195 | ```php 196 | use Mchev\Banhammer\IP; 197 | 198 | IP::ban("8.8.8.8", [ 199 | 'route' => request()->route()->getName(), 200 | 'user_agent' => request()->header('user-agent') 201 | ]); 202 | ``` 203 | 204 | Metas usage 205 | ```php 206 | $ban->setMeta('username', 'Jane'); 207 | $ban->getMeta('username'); // Jane 208 | $ban->hasMeta('username'); // boolean 209 | $ban->forgetMeta('username'); 210 | ``` 211 | 212 | Filtering by Meta 213 | ```php 214 | IP::banned()->whereMeta('username', 'Jane')->get(); 215 | // OR 216 | $users->bans()->whereMeta('username', 'Jane')->get(); 217 | // OR 218 | $users->whereBansMeta('username', 'Jane')->get(); 219 | ``` 220 | 221 | ### Blocking Access from Specific Countries 222 | 223 | To enhance the security of your application, you can restrict access from specific countries by enabling the country-blocking feature in the configuration file. Follow these simple steps: 224 | 225 | 1. Open your Banhammer configuration file (config/ban.php). 226 | 227 | 2. Set the 'block_by_country' configuration option to true to enable country-based blocking. 228 | 229 | ```php 230 | 'block_by_country' => true, 231 | ``` 232 | 233 | 3. Specify the list of countries you want to block by adding their country codes to the 'blocked_countries' array. 234 | 235 | ```php 236 | 'blocked_countries' => ['FR', 'ES'], 237 | ``` 238 | 239 | By configuring these settings, you effectively block access to your application for users originating from the specified countries. This helps improve the security and integrity of your system by preventing unwanted traffic from regions you've identified as potential risks. 240 | 241 | **Important Notice:** 242 | The Banhammer package utilizes the free version of ip-api.com for geolocation data. Keep in mind that their endpoints have a rate limit of 45 HTTP requests per minute from a single IP address. If you exceed this limit, your requests will be throttled, and you may receive a 429 HTTP status code until your rate limit window is reset. 243 | 244 | > **Developer Note:** 245 | > While Banhammer currently relies on the free version of [ip-api.com](https://ip-api.com/) for geolocation data, I'm open to exploring better alternatives. If you have suggestions for a more robust or efficient solution, or if you'd like to contribute improvements, please feel free to [open an issue](https://github.com/mchev/banhammer/issues) or submit a [pull request](https://github.com/mchev/banhammer/pulls). 246 | 247 | ### Middleware 248 | To prevent banned users from accessing certain parts of your application, simply add the `auth.banned` middleware on the concerned routes. 249 | ```php 250 | Route::middleware(['auth.banned'])->group(function () { 251 | // ... 252 | }); 253 | ``` 254 | 255 | To prevent banned ips from accessing certain parts of your application, simply add the `ip.banned` middleware on the concerned routes. 256 | ```php 257 | Route::middleware(['ip.banned'])->group(function () { 258 | // ... 259 | }); 260 | ``` 261 | 262 | To block and logout banned Users or IP, add the `logout.banned` middleware: 263 | ```php 264 | Route::middleware(['logout.banned'])->group(function () { 265 | // ... 266 | }); 267 | ``` 268 | 269 | > If you use the `logout.banned` middleware, it is not necessary to cumulate the other middlewares. 270 | 271 | > If you want to block IPs on every HTTP request of your application, list `Mchev\Banhammer\Middleware\IPBanned` in the `$middleware` property of your `app/Http/Kernel.php` class. 272 | 273 | ### Scheduler 274 | 275 | > ⚠ IMPORTANT 276 | 277 | In order to be able to automatically delete expired bans, you must have a cron job set up on your server to run the Laravel Scheduled Jobs 278 | 279 | > [Running the scheduler](https://laravel.com/docs/9.x/scheduling#running-the-scheduler) 280 | 281 | > [Configure Scheduler on Forge](https://forge.laravel.com/docs/1.0/resources/scheduler.html#laravel-scheduled-jobs) 282 | 283 | ### Events 284 | 285 | If entity is banned `Mchev\Banhammer\Events\ModelWasBanned` event is fired. 286 | 287 | Is entity is unbanned `Mchev\Banhammer\Events\ModelWasUnbanned` event is fired. 288 | 289 | ### MISC 290 | 291 | To manually unban expired bans : 292 | ```php 293 | use Mchev\Banhammer\Banhammer; 294 | 295 | Banhammer::unbanExpired(); 296 | ``` 297 | 298 | Or you can use the command: 299 | ```bash 300 | php artisan banhammer:unban 301 | ``` 302 | 303 | To permanently delete all the expired bans : 304 | ```php 305 | use Mchev\Banhammer\Banhammer; 306 | 307 | Banhammer::clear(); 308 | ``` 309 | 310 | Or you can use the command: 311 | ```bash 312 | php artisan banhammer:clear 313 | ``` 314 | 315 | ## UUIDs 316 | 317 | To use UUIDs make sure you publish and edit the migration files. 318 | 319 | ```bash 320 | php artisan vendor:publish --provider="Mchev\Banhammer\BanhammerServiceProvider" --tag="migrations" 321 | ``` 322 | 323 | ```diff 324 | - $table->id(); 325 | + $table->uuid('id'); 326 | ``` 327 | 328 | You will then need to make a model that extends `Mchev\Banhammer\Models\Ban`: 329 | 330 | ```php 331 | Although most of the methods needed are already available from the base model, you can add any additional methods here. 344 | 345 | Finally update the published `ban.php` config file to load the model you have created: 346 | 347 | ```diff 348 | /* 349 | |-------------------------------------------------------------------------- 350 | | Model Name 351 | |-------------------------------------------------------------------------- 352 | | 353 | | Specify the model which you want to use as your Ban model. 354 | | 355 | */ 356 | 357 | - 'model' => \Mchev\Banhammer\Models\Ban::class, 358 | + 'model' => \App\Models\YouBanClass::class, 359 | ``` 360 | 361 | ## Upgrading To 2.0 from 1.x 362 | 363 | To upgrade to Banhammer version 2.0, follow these simple steps: 364 | 365 | 1. Update the package version in your application's `composer.json` file: 366 | 367 | ```json 368 | "require": { 369 | "mchev/banhammer": "^2.0" 370 | } 371 | ``` 372 | 373 | 2. Run the following command in your terminal: 374 | 375 | ```bash 376 | composer update mchev/banhammer 377 | ``` 378 | 379 | 3. Update the configuration 380 | 381 | 1. Update the configuration 382 | - Backup your previous configuration file located at `config/ban.php`. 383 | - Force republish the new configuration using the command: 384 | ```bash 385 | php artisan vendor:publish --provider="Mchev\Banhammer\BanhammerServiceProvider" --tag="config" --force 386 | ``` 387 | - Review the new configuration file and make any necessary edits. 388 | 389 | ## Testing 390 | 391 | ```bash 392 | composer test 393 | ``` 394 | 395 | ## Changelog 396 | 397 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 398 | 399 | ## Roadmap / Todo 400 | 401 | - [ ] Laravel pulse card (ips banned, block by country enabled, etc.). 402 | - [x] Block by country feature 403 | 404 | ## Contributing 405 | 406 | To encourage active collaboration, Banhammer strongly encourages pull requests, not just bug reports. Pull requests will only be reviewed when marked as "ready for review" (not in the "draft" state) and all tests for new features are passing. Lingering, non-active pull requests left in the "draft" state will be closed after a few days. 407 | 408 | However, if you file a bug report, your issue should contain a title and a clear description of the issue. You should also include as much relevant information as possible and a code sample that demonstrates the issue. The goal of a bug report is to make it easy for yourself - and others - to replicate the bug and develop a fix. 409 | 410 | Remember, bug reports are created in the hope that others with the same problem will be able to collaborate with you on solving it. Do not expect that the bug report will automatically see any activity or that others will jump to fix it. 411 | 412 | ## Security Vulnerabilities 413 | 414 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 415 | 416 | ## Credits 417 | 418 | - Inspired by [laravel-ban](https://github.com/cybercog/laravel-ban) from [cybercog](https://github.com/cybercog) 419 | 420 | ## License 421 | 422 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 423 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mchev/banhammer", 3 | "description": "Banhammer for Laravel allows you to ban any Model by key and by IP.", 4 | "keywords": [ 5 | "mchev", 6 | "laravel", 7 | "bans-for-laravel", 8 | "ban", 9 | "bannable", 10 | "ip", 11 | "country" 12 | ], 13 | "homepage": "https://github.com/mchev/banhammer", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "mchev", 18 | "email": "martin@pegase.io", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": "^8.0" 24 | }, 25 | "require-dev": { 26 | "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", 27 | "phpunit/phpunit": "^9.6|^10.5|^11.5.3" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Mchev\\Banhammer\\": "src" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Mchev\\Banhammer\\Tests\\": "tests" 37 | } 38 | }, 39 | "scripts": { 40 | "test": "vendor/bin/phpunit", 41 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 42 | }, 43 | "config": { 44 | "sort-packages": true 45 | }, 46 | "extra": { 47 | "laravel": { 48 | "providers": [ 49 | "Mchev\\Banhammer\\BanhammerServiceProvider" 50 | ] 51 | } 52 | }, 53 | "minimum-stability": "dev", 54 | "prefer-stable": true 55 | } 56 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | 'bans', 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Model Name 20 | |-------------------------------------------------------------------------- 21 | | 22 | | Specify the model which you want to use as your Ban model. 23 | | 24 | */ 25 | 26 | 'model' => \Mchev\Banhammer\Models\Ban::class, 27 | 28 | /* 29 | |-------------------------------------------------------------------------- 30 | | Where to Redirect Banned Users 31 | |-------------------------------------------------------------------------- 32 | | 33 | | Define the URL to which users will be redirected when attempting to log in 34 | | after being banned. If not defined, the banned user will be redirected to 35 | | the previous page they tried to access. 36 | | 37 | */ 38 | 39 | 'fallback_url' => null, // Examples: null (default), "/oops", "/login" 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | 403 Message 44 | |-------------------------------------------------------------------------- 45 | | 46 | | The message that will be displayed if no fallback URL is defined for banned users. 47 | | 48 | */ 49 | 50 | 'messages' => [ 51 | 'user' => 'Your account has been banned.', 52 | 'ip' => 'Access from your IP address is restricted.', 53 | 'country' => 'Access from your country is restricted.', 54 | ], 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | Block by Country 59 | |-------------------------------------------------------------------------- 60 | | 61 | | Determine whether to block users based on their country. This setting uses 62 | | the value of BANHAMMER_BLOCK_BY_COUNTRY from the environment. Enabling this 63 | | feature may result in up to 45 HTTP requests per minute with the free version 64 | | of https://ip-api.com/. 65 | | 66 | */ 67 | 68 | 'block_by_country' => env('BANHAMMER_BLOCK_BY_COUNTRY', false), 69 | 70 | /* 71 | |-------------------------------------------------------------------------- 72 | | List of Blocked Countries 73 | |-------------------------------------------------------------------------- 74 | | 75 | | Specify the countries where users will be blocked if 'block_by_country' is true. 76 | | Add country codes to the array to restrict access from those countries. 77 | | 78 | */ 79 | 80 | 'blocked_countries' => [], // Examples: ['US', 'CA', 'GB', 'FR', 'ES', 'DE'] 81 | 82 | /* 83 | |-------------------------------------------------------------------------- 84 | | Cache Duration for IP Geolocation 85 | |-------------------------------------------------------------------------- 86 | | 87 | | This configuration option determines the duration, in minutes, for which 88 | | the IP geolocation data will be stored in the cache. This helps prevent 89 | | excessive requests and enables the middleware to efficiently determine 90 | | whether to block a request based on the user's country. 91 | | 92 | */ 93 | 'cache_duration' => 120, // Duration in minutes 94 | 95 | ]; 96 | -------------------------------------------------------------------------------- /database/migrations/create_bans_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->nullableMorphs('bannable'); 17 | $table->nullableMorphs('created_by'); 18 | $table->text('comment')->nullable(); 19 | $table->string('ip', 45)->nullable(); 20 | $table->timestamp('expired_at')->nullable(); 21 | $table->softDeletes(); 22 | $table->timestamps(); 23 | $table->index('ip'); 24 | $table->index('expired_at'); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | */ 31 | public function down(): void 32 | { 33 | Schema::dropIfExists(config('ban.table')); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/metas_field_to_bans_table.php: -------------------------------------------------------------------------------- 1 | json('metas')->nullable(); 17 | }); 18 | } 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down(): void 25 | { 26 | if (Schema::hasColumn(config('ban.table'), 'metas')) { 27 | Schema::table(config('ban.table'), function (Blueprint $table) { 28 | $table->dropColumn('metas'); 29 | }); 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/Banhammer.php: -------------------------------------------------------------------------------- 1 | delete(); 12 | Cache::put('banned-ips', IP::banned()->pluck('ip')->toArray()); 13 | } 14 | 15 | public static function clear(): void 16 | { 17 | config('ban.model')::onlyTrashed()->forceDelete(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/BanhammerServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->make(Router::class); 26 | $router->aliasMiddleware('auth.banned', AuthBanned::class); 27 | $router->aliasMiddleware('ip.banned', IPBanned::class); 28 | $router->aliasMiddleware('logout.banned', LogoutBanned::class); 29 | 30 | if (config('ban.block_by_country')) { 31 | $router->pushMiddlewareToGroup('web', BlockByCountry::class); 32 | } 33 | 34 | if ($this->app->runningInConsole()) { 35 | // Publishing the config. 36 | $this->publishes([ 37 | __DIR__.'/../config/config.php' => config_path('ban.php'), 38 | ], 'config'); 39 | 40 | // Publishing migrations 41 | $this->publishes([ 42 | __DIR__.'/../database/migrations' => database_path('migrations'), 43 | ], 'migrations'); 44 | 45 | // Registering package commands. 46 | $this->commands([ 47 | ClearBans::class, 48 | DeleteExpired::class, 49 | ]); 50 | } 51 | 52 | $this->app->booted(function () { 53 | $schedule = $this->app->make(Schedule::class); 54 | $schedule->command('banhammer:unban')->everyMinute(); 55 | }); 56 | 57 | } 58 | 59 | /** 60 | * Register the application services. 61 | */ 62 | public function register() 63 | { 64 | // Automatically apply the package configuration 65 | $this->mergeConfigFrom(__DIR__.'/../config/config.php', 'ban'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Commands/ClearBans.php: -------------------------------------------------------------------------------- 1 | info('All expired bans have been permanently deleted.'); 18 | 19 | return self::SUCCESS; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Commands/DeleteExpired.php: -------------------------------------------------------------------------------- 1 | info('All expired bans have been deleted.'); 18 | 19 | return self::SUCCESS; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Events/ModelWasBanned.php: -------------------------------------------------------------------------------- 1 | getMessage()}"); 25 | } 26 | 27 | /** 28 | * Render the exception into an HTTP response. 29 | */ 30 | public function render(Request $request): Response|RedirectResponse 31 | { 32 | return (config('ban.fallback_url')) 33 | ? redirect(config('ban.fallback_url')) 34 | : abort(403, $this->getMessage()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/IP.php: -------------------------------------------------------------------------------- 1 | $ip, 18 | 'metas' => count($metas) ? $metas : null, 19 | 'expired_at' => $date, 20 | ]); 21 | } 22 | } 23 | } 24 | 25 | public static function unban(string|array $ips): void 26 | { 27 | $ips = (array) $ips; 28 | config('ban.model')::whereIn('ip', $ips)->delete(); 29 | Cache::put('banned-ips', self::banned()->pluck('ip')->toArray()); 30 | } 31 | 32 | public static function isBanned(string $ip): bool 33 | { 34 | return config('ban.model')::where('ip', $ip) 35 | ->notExpired() 36 | ->exists(); 37 | } 38 | 39 | public static function banned(): Builder 40 | { 41 | return config('ban.model')::whereNotNull('ip') 42 | ->with('createdBy') 43 | ->notExpired(); 44 | } 45 | 46 | public static function getBannedIPsFromCache(): array 47 | { 48 | return Cache::has('banned-ips') 49 | ? Cache::get('banned-ips') 50 | : self::banned()->pluck('ip')->unique()->toArray(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Middleware/AuthBanned.php: -------------------------------------------------------------------------------- 1 | user() && $request->user()->isBanned()) { 14 | throw new BanhammerException(config('ban.messages.user')); 15 | } 16 | 17 | return $next($request); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Middleware/BlockByCountry.php: -------------------------------------------------------------------------------- 1 | ip(); 25 | 26 | if (! is_null($ip)) { 27 | $cacheKey = 'banhammer_'.$ip; 28 | $cachedResult = Cache::get($cacheKey); 29 | 30 | if ($cachedResult === null) { 31 | try { 32 | $geolocationData = $this->ipApiService->getGeolocationData($ip); 33 | 34 | if ($geolocationData['status'] === 'fail') { 35 | Log::notice('Banhammer country check failure: '.$geolocationData['message'], [ 36 | 'ip' => $ip, 37 | ]); 38 | Cache::put($cacheKey, 'allowed', now()->addMinutes(config('ban.cache_duration'))); 39 | } else { 40 | if (in_array($geolocationData['countryCode'], $blockedCountries)) { 41 | Cache::put($cacheKey, 'blocked', now()->addMinutes(config('ban.cache_duration'))); 42 | throw new BanhammerException(config('ban.messages.country')); 43 | } 44 | } 45 | 46 | } catch (\Exception $e) { 47 | Log::debug('Banhammer Exception: '.$e->getMessage(), [ 48 | 'ip' => $ip, 49 | 'country' => $geolocationData['countryCode'] ?? null, 50 | ]); 51 | throw new BanhammerException(config('ban.messages.country')); 52 | } 53 | } elseif ($cachedResult === 'blocked') { 54 | throw new BanhammerException(config('ban.messages.country')); 55 | } 56 | } 57 | } 58 | 59 | return $next($request); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Middleware/IPBanned.php: -------------------------------------------------------------------------------- 1 | ip() && in_array($request->ip(), $bannedIPs)) { 19 | throw new BanhammerException(config('ban.messages.ip')); 20 | } 21 | } catch (\Exception $e) { 22 | // Log the exception 23 | Log::error('IPBanned Middleware Exception: '.$e->getMessage(), ['exception' => $e]); 24 | 25 | throw $e; 26 | } 27 | 28 | return $next($request); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Middleware/LogoutBanned.php: -------------------------------------------------------------------------------- 1 | user() && $request->user()->isBanned() 15 | || $request->ip() && in_array($request->ip(), IP::getBannedIPsFromCache())) { 16 | if ($request->user()) { 17 | auth()->logout(); 18 | $request->session()->invalidate(); 19 | $request->session()->regenerateToken(); 20 | } 21 | 22 | throw new BanhammerException(config('ban.messages.user')); 23 | } 24 | 25 | return $next($request); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Models/Ban.php: -------------------------------------------------------------------------------- 1 | 'datetime', 28 | 'metas' => 'array', 29 | ]; 30 | 31 | protected function expiredAt(): Attribute 32 | { 33 | return new Attribute( 34 | set: fn (null|string|Carbon $value) => (! is_null($value) && ! $value instanceof Carbon) ? Carbon::parse($value) : $value, 35 | ); 36 | } 37 | 38 | /** 39 | * Get the parent bannable model. 40 | */ 41 | public function bannable(): MorphTo 42 | { 43 | return $this->morphTo(); 44 | } 45 | 46 | /** 47 | * Entity responsible for ban. 48 | */ 49 | public function createdBy(): MorphTo 50 | { 51 | return $this->morphTo('created_by'); 52 | } 53 | 54 | public function scopePermanent(Builder $query): void 55 | { 56 | $query->whereNull('expired_at'); 57 | } 58 | 59 | public function scopeNotPermanent(Builder $query): void 60 | { 61 | $query->whereNotNull('expired_at'); 62 | } 63 | 64 | public function scopeExpired(Builder $query): void 65 | { 66 | $query->notPermanent()->where('expired_at', '<=', Carbon::now()->format('Y-m-d H:i:s')); 67 | } 68 | 69 | public function scopeNotExpired(Builder $query): void 70 | { 71 | $query->where('expired_at', '>', Carbon::now()->format('Y-m-d H:i:s')) 72 | ->orWhereNull('expired_at'); 73 | } 74 | 75 | public function scopeWhereMeta(Builder $query, string $name, $value): void 76 | { 77 | $query->whereJsonContains('metas->'.$name, $value); 78 | } 79 | 80 | public function hasMeta(string $propertyName): bool 81 | { 82 | return Arr::has($this->metas, $propertyName); 83 | } 84 | 85 | /** 86 | * Get the value of meta with the given name. 87 | * 88 | * @param mixed $default 89 | */ 90 | public function getMeta(string $propertyName, $default = null): mixed 91 | { 92 | return Arr::get($this->metas, $propertyName, $default); 93 | } 94 | 95 | /** 96 | * Set the value of meta with the given name. 97 | * 98 | * @param mixed $value 99 | * @return $this 100 | */ 101 | public function setMeta(string $name, $value): self 102 | { 103 | $meta = $this->metas; 104 | Arr::set($meta, $name, $value); 105 | $this->metas = $meta; 106 | 107 | return $this; 108 | } 109 | 110 | /** 111 | * Forget the value of meta with the given name. 112 | * 113 | * @param mixed $value 114 | * @return $this 115 | */ 116 | public function forgetMeta(string $name): self 117 | { 118 | $meta = $this->metas; 119 | Arr::forget($meta, $name); 120 | $this->metas = $meta; 121 | 122 | return $this; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Observers/BanObserver.php: -------------------------------------------------------------------------------- 1 | user(); 15 | if ($user && is_null($ban->created_by_type) && is_null($ban->created_by_id)) { 16 | $ban->fill([ 17 | 'created_by_type' => $user->getMorphClass(), 18 | 'created_by_id' => $user->getKey(), 19 | ]); 20 | } 21 | } 22 | 23 | public function created($ban): void 24 | { 25 | event(new ModelWasBanned($ban->bannable(), $ban)); 26 | $this->updateCachedIps($ban); 27 | } 28 | 29 | public function deleted($ban): void 30 | { 31 | event(new ModelWasUnbanned($ban->bannable())); 32 | $this->updateCachedIps($ban); 33 | } 34 | 35 | public function updateCachedIps($ban): void 36 | { 37 | if ($ban->ip) { 38 | Cache::put('banned-ips', IP::banned()->pluck('ip')->unique()->toArray()); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Services/IpApiService.php: -------------------------------------------------------------------------------- 1 | addDay(), function () use ($ip) { 22 | $response = Http::get("http://ip-api.com/json/{$ip}?fields=status,message,countryCode,query"); 23 | 24 | return $response->json(); 25 | }); 26 | 27 | return $geolocationData; 28 | } catch (\Exception $e) { 29 | // Log the error 30 | Log::error('IP-API Service Error: '.$e->getMessage(), ['exception' => $e]); 31 | 32 | // Handle the error as needed 33 | return null; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Traits/Bannable.php: -------------------------------------------------------------------------------- 1 | morphMany(config('ban.model'), 'bannable'); 17 | } 18 | 19 | /** 20 | * Check if the model is banned. 21 | */ 22 | public function isBanned(): bool 23 | { 24 | return $this->bans->first(function ($ban) { 25 | return $ban->expired_at === null || $ban->expired_at->isFuture(); 26 | }) !== null; 27 | } 28 | 29 | /** 30 | * Check if the model is not banned. 31 | */ 32 | public function isNotBanned(): bool 33 | { 34 | return ! $this->isBanned(); 35 | } 36 | 37 | /** 38 | * Ban the model with the specified attributes. 39 | */ 40 | public function ban(array $attributes = []): Ban 41 | { 42 | return $this->bans()->create($attributes); 43 | } 44 | 45 | /** 46 | * Ban the model until the specified date. 47 | */ 48 | public function banUntil(string $date): Ban 49 | { 50 | return $this->ban([ 51 | 'expired_at' => $date, 52 | ]); 53 | } 54 | 55 | /** 56 | * Unban the model by deleting all bans. 57 | */ 58 | public function unban(): void 59 | { 60 | $this->bans()->each(fn ($ban) => $ban->delete()); 61 | } 62 | 63 | /** 64 | * Scope a query to include only models that are currently banned. 65 | */ 66 | public function scopeBanned(Builder $query, bool $banned = true): void 67 | { 68 | $banned 69 | ? $query->whereHas('bans', fn ($query) => $query->notExpired()) 70 | : $this->scopeNotBanned($query); 71 | } 72 | 73 | /** 74 | * Scope a query to include only models that are not currently banned. 75 | */ 76 | public function scopeNotBanned(Builder $query): void 77 | { 78 | $query->whereDoesntHave('bans'); 79 | } 80 | 81 | /** 82 | * Scope a query to include only models with bans having a specific meta key and value. 83 | */ 84 | public function scopeWhereBansMeta(Builder $query, string $key, $value): void 85 | { 86 | $query->whereHas('bans', function ($query) use ($key, $value) { 87 | $query->where('metas->'.$key, $value)->notExpired(); 88 | }); 89 | } 90 | 91 | /** 92 | * Scope a query to include only models with bans created by a specific type. 93 | */ 94 | public function scopeBannedByType(Builder $query, string $className): void 95 | { 96 | $query->whereHas('bans', function ($query) use ($className) { 97 | $query->where('created_by_type', $className)->notExpired(); 98 | }); 99 | } 100 | } 101 | --------------------------------------------------------------------------------