User Bans

This commit is contained in:
Refringe 2024-06-16 21:40:00 -04:00
parent be0eecf1e9
commit a3b3971d2f
Signed by: Refringe
SSH Key Fingerprint: SHA256:t865XsQpfTeqPRBMN2G6+N8wlDjkgUCZF3WGW6O9N/k
11 changed files with 346 additions and 43 deletions

View File

@ -18,6 +18,7 @@ LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug
# Due to the hub import script, only MySQL is supported at this time.
DB_CONNECTION=mysql DB_CONNECTION=mysql
DB_HOST=mysql DB_HOST=mysql
DB_PORT=3306 DB_PORT=3306

1
.gitignore vendored
View File

@ -17,3 +17,4 @@ yarn-error.log
/.fleet /.fleet
/.idea /.idea
/.vscode /.vscode
.DS_Store

View File

@ -20,8 +20,8 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use League\HTMLToMarkdown\HtmlConverter; use League\HTMLToMarkdown\HtmlConverter;
use Stevebauman\Purify\Facades\Purify; use Stevebauman\Purify\Facades\Purify;
@ -33,7 +33,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
public function handle(): void public function handle(): void
{ {
// Stream some data locally so that we don't have to keep accessing the Hub's database. Use MySQL temporary // Stream some data locally so that we don't have to keep accessing the Hub's database. Use MySQL temporary
// tables to store the data to save on memory; we don't want this to be a memory hog. // tables to store the data to save on memory; we don't want this to be a hog.
$this->bringFileOptionsLocal(); $this->bringFileOptionsLocal();
$this->bringFileContentLocal(); $this->bringFileContentLocal();
$this->bringFileVersionLabelsLocal(); $this->bringFileVersionLabelsLocal();
@ -60,9 +60,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
*/ */
protected function bringFileOptionsLocal(): void protected function bringFileOptionsLocal(): void
{ {
if (Schema::hasTable('temp_file_option_values')) { DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_option_values');
DB::statement('DROP TABLE temp_file_option_values');
}
DB::statement('CREATE TEMPORARY TABLE temp_file_option_values ( DB::statement('CREATE TEMPORARY TABLE temp_file_option_values (
fileID INT, fileID INT,
optionID INT, optionID INT,
@ -88,9 +86,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
*/ */
protected function bringFileContentLocal(): void protected function bringFileContentLocal(): void
{ {
if (Schema::hasTable('temp_file_content')) { DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_content');
DB::statement('DROP TABLE temp_file_content');
}
DB::statement('CREATE TEMPORARY TABLE temp_file_content ( DB::statement('CREATE TEMPORARY TABLE temp_file_content (
fileID INT, fileID INT,
subject VARCHAR(255), subject VARCHAR(255),
@ -118,9 +114,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
*/ */
protected function bringFileVersionLabelsLocal(): void protected function bringFileVersionLabelsLocal(): void
{ {
if (Schema::hasTable('temp_file_version_labels')) { DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_version_labels');
DB::statement('DROP TABLE temp_file_version_labels');
}
DB::statement('CREATE TEMPORARY TABLE temp_file_version_labels ( DB::statement('CREATE TEMPORARY TABLE temp_file_version_labels (
labelID INT, labelID INT,
objectID INT objectID INT
@ -145,9 +139,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
*/ */
protected function bringFileVersionContentLocal(): void protected function bringFileVersionContentLocal(): void
{ {
if (Schema::hasTable('temp_file_version_content')) { DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_version_content');
DB::statement('DROP TABLE temp_file_version_content');
}
DB::statement('CREATE TEMPORARY TABLE temp_file_version_content ( DB::statement('CREATE TEMPORARY TABLE temp_file_version_content (
versionID INT, versionID INT,
description TEXT description TEXT
@ -173,10 +165,12 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
{ {
DB::connection('mysql_hub') DB::connection('mysql_hub')
->table('wcf1_user') ->table('wcf1_user')
->select('userID', 'username', 'email', 'password', 'registrationDate') ->select('userID', 'username', 'email', 'password', 'registrationDate', 'banned', 'banReason', 'banExpires')
->chunkById(250, function (Collection $users) { ->chunkById(250, function (Collection $users) {
$insertData = []; $insertData = [];
$bannedUsers = [];
foreach ($users as $user) { foreach ($users as $user) {
$insertData[] = [ $insertData[] = [
'hub_id' => (int) $user->userID, 'hub_id' => (int) $user->userID,
@ -186,14 +180,36 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
'created_at' => $this->cleanRegistrationDate($user->registrationDate), 'created_at' => $this->cleanRegistrationDate($user->registrationDate),
'updated_at' => now('UTC')->toDateTimeString(), 'updated_at' => now('UTC')->toDateTimeString(),
]; ];
// If the user is banned, add them to an array of banned users.
if ($user->banned) {
$bannedUsers[] = [
'hub_id' => (int) $user->userID,
'comment' => $user->banReason ?? '',
'expired_at' => $this->cleanUnbannedAtDate($user->banExpires),
];
}
} }
if (! empty($insertData)) { if (! empty($insertData)) {
DB::table('users')->upsert( DB::table('users')->upsert($insertData, ['hub_id'], [
$insertData, 'name',
['hub_id'], 'email',
['name', 'email', 'password', 'created_at', 'updated_at'] 'password',
); 'created_at',
'updated_at',
]);
}
// Ban the users in the new local database.
if (! empty($bannedUsers)) {
foreach ($bannedUsers as $bannedUser) {
$user = User::whereHubId($bannedUser['hub_id'])->first();
$user->ban([
'comment' => $bannedUser['comment'],
'expired_at' => $bannedUser['expired_at'],
]);
}
} }
}, 'userID'); }, 'userID');
} }
@ -207,7 +223,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
// If it's not Bcrypt, they'll have to reset their password. Tough luck. // If it's not Bcrypt, they'll have to reset their password. Tough luck.
$clean = str_ireplace(['invalid:', 'bcrypt:', 'bcrypt::', 'cryptmd5:', 'cryptmd5::'], '', $password); $clean = str_ireplace(['invalid:', 'bcrypt:', 'bcrypt::', 'cryptmd5:', 'cryptmd5::'], '', $password);
// If the password hash starts with $2, it's a valid Bcrypt hash. Otherwise, it's invalid. // At this point, if the password hash starts with $2, it's a valid Bcrypt hash. Otherwise, it's invalid.
return str_starts_with($clean, '$2') ? $clean : ''; return str_starts_with($clean, '$2') ? $clean : '';
} }
@ -226,6 +242,46 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
return $date->toDateTimeString(); return $date->toDateTimeString();
} }
/**
* Clean the banned_at date from the Hub database.
*/
protected function cleanUnbannedAtDate(?string $unbannedAt): ?string
{
// If the input is null, return null
if ($unbannedAt === null) {
return null;
}
// Explicit check for the Unix epoch start date
if (Str::contains($unbannedAt, '1970-01-01')) {
return null;
}
// Use validator to check for a valid date format
$validator = Validator::make(['date' => $unbannedAt], [
'date' => 'date_format:Y-m-d H:i:s',
]);
if ($validator->fails()) {
// If the date format is invalid, return null
return null;
}
// Validate the date using Carbon
try {
$date = Carbon::parse($unbannedAt);
// Additional check to ensure the date is not a default or zero date
if ($date->year == 0 || $date->month == 0 || $date->day == 0) {
return null;
}
return $date->toDateTimeString();
} catch (\Exception $e) {
// If the date is not valid, return null
return null;
}
}
/** /**
* Import the licenses from the Hub database to the local database. * Import the licenses from the Hub database to the local database.
*/ */
@ -512,7 +568,14 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
*/ */
public function failed(Exception $exception): void public function failed(Exception $exception): void
{ {
// Disconnect from the 'mysql_hub' database connection // Drop the temporary tables.
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_option_values');
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_content');
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_version_labels');
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_version_content');
// This *should* drop the temporary tables as well, but I like to be sure.
DB::connection('mysql_hub')->disconnect(); DB::connection('mysql_hub')->disconnect();
DB::disconnect();
} }
} }

View File

@ -11,9 +11,11 @@ use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Jetstream\HasProfilePhoto; use Laravel\Jetstream\HasProfilePhoto;
use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\HasApiTokens;
use Laravel\Scout\Searchable; use Laravel\Scout\Searchable;
use Mchev\Banhammer\Traits\Bannable;
class User extends Authenticatable implements MustVerifyEmail class User extends Authenticatable implements MustVerifyEmail
{ {
use Bannable;
use HasApiTokens; use HasApiTokens;
use HasFactory; use HasFactory;
use HasProfilePhoto; use HasProfilePhoto;
@ -38,14 +40,6 @@ class User extends Authenticatable implements MustVerifyEmail
'profile_photo_url', 'profile_photo_url',
]; ];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
public function mods(): HasMany public function mods(): HasMany
{ {
return $this->hasMany(Mod::class); return $this->hasMany(Mod::class);
@ -63,4 +57,12 @@ class User extends Authenticatable implements MustVerifyEmail
{ {
return ! is_null($this->email_verified_at); return ! is_null($this->email_verified_at);
} }
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
} }

View File

@ -3,6 +3,7 @@
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Configuration\Middleware;
use Mchev\Banhammer\Middleware\IPBanned;
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
->withRouting( ->withRouting(
@ -12,7 +13,7 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware) { ->withMiddleware(function (Middleware $middleware) {
// $middleware->append(IPBanned::class);
}) })
->withExceptions(function (Exceptions $exceptions) { ->withExceptions(function (Exceptions $exceptions) {
// //

View File

@ -22,6 +22,7 @@
"league/flysystem-aws-s3-v3": "3.0", "league/flysystem-aws-s3-v3": "3.0",
"league/html-to-markdown": "^5.1", "league/html-to-markdown": "^5.1",
"livewire/livewire": "^3.0", "livewire/livewire": "^3.0",
"mchev/banhammer": "^2.3",
"meilisearch/meilisearch-php": "^1.8", "meilisearch/meilisearch-php": "^1.8",
"stevebauman/purify": "^6.2" "stevebauman/purify": "^6.2"
}, },

70
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "35e09d2f7ca6d5c8de5ed54e14d13e58", "content-hash": "f9f3d2b898b0a2e762d3c042c4d38f3a",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",
@ -3447,6 +3447,74 @@
], ],
"time": "2024-05-21T13:39:04+00:00" "time": "2024-05-21T13:39:04+00:00"
}, },
{
"name": "mchev/banhammer",
"version": "v2.3.0",
"source": {
"type": "git",
"url": "https://github.com/mchev/banhammer.git",
"reference": "9821abe9a2be60742b43b880091481c2a113d938"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mchev/banhammer/zipball/9821abe9a2be60742b43b880091481c2a113d938",
"reference": "9821abe9a2be60742b43b880091481c2a113d938",
"shasum": ""
},
"require": {
"php": "^8.0"
},
"require-dev": {
"orchestra/testbench": "^7.0|^8.0|^9.0",
"phpunit/phpunit": "^9.5|^10.5|^11.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Mchev\\Banhammer\\BanhammerServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Mchev\\Banhammer\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "mchev",
"email": "martin@pegase.io",
"role": "Developer"
}
],
"description": "Banhammer for Laravel allows you to ban any Model by key and by IP.",
"homepage": "https://github.com/mchev/banhammer",
"keywords": [
"IP",
"ban",
"bannable",
"bans-for-laravel",
"country",
"laravel",
"mchev"
],
"support": {
"issues": "https://github.com/mchev/banhammer/issues",
"source": "https://github.com/mchev/banhammer/tree/v2.3.0"
},
"funding": [
{
"url": "https://github.com/mchev",
"type": "github"
}
],
"time": "2024-05-24T10:09:58+00:00"
},
{ {
"name": "meilisearch/meilisearch-php", "name": "meilisearch/meilisearch-php",
"version": "v1.8.0", "version": "v1.8.0",

95
config/ban.php Normal file
View File

@ -0,0 +1,95 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Table Name
|--------------------------------------------------------------------------
|
| Specify the name of the table created during migration for the ban system.
| This table will store information about banned users.
|
*/
'table' => 'bans',
/*
|--------------------------------------------------------------------------
| Model Name
|--------------------------------------------------------------------------
|
| Specify the model which you want to use as your Ban model.
|
*/
'model' => \Mchev\Banhammer\Models\Ban::class,
/*
|--------------------------------------------------------------------------
| Where to Redirect Banned Users
|--------------------------------------------------------------------------
|
| Define the URL to which users will be redirected when attempting to log in
| after being banned. If not defined, the banned user will be redirected to
| the previous page they tried to access.
|
*/
'fallback_url' => null, // Examples: null (default), "/oops", "/login"
/*
|--------------------------------------------------------------------------
| 403 Message
|--------------------------------------------------------------------------
|
| The message that will be displayed if no fallback URL is defined for banned users.
|
*/
'messages' => [
'user' => 'Your account has been banned.',
'ip' => 'Access from your IP address is restricted.',
'country' => 'Access from your country is restricted.',
],
/*
|--------------------------------------------------------------------------
| Block by Country
|--------------------------------------------------------------------------
|
| Determine whether to block users based on their country. This setting uses
| the value of BANHAMMER_BLOCK_BY_COUNTRY from the environment. Enabling this
| feature may result in up to 45 HTTP requests per minute with the free version
| of https://ip-api.com/.
|
*/
'block_by_country' => env('BANHAMMER_BLOCK_BY_COUNTRY', false),
/*
|--------------------------------------------------------------------------
| List of Blocked Countries
|--------------------------------------------------------------------------
|
| Specify the countries where users will be blocked if 'block_by_country' is true.
| Add country codes to the array to restrict access from those countries.
|
*/
'blocked_countries' => [], // Examples: ['US', 'CA', 'GB', 'FR', 'ES', 'DE']
/*
|--------------------------------------------------------------------------
| Cache Duration for IP Geolocation
|--------------------------------------------------------------------------
|
| This configuration option determines the duration, in minutes, for which
| the IP geolocation data will be stored in the cache. This helps prevent
| excessive requests and enables the middleware to efficiently determine
| whether to block a request based on the user's country.
|
*/
'cache_duration' => 120, // Duration in minutes
];

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create(config('ban.table'), function (Blueprint $table) {
$table->id();
$table->nullableMorphs('bannable');
$table->nullableMorphs('created_by');
$table->text('comment')->nullable();
$table->string('ip', 45)->nullable();
$table->timestamp('expired_at')->nullable();
$table->softDeletes();
$table->timestamps();
$table->index('ip');
$table->index('expired_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists(config('ban.table'));
}
};

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
if (! Schema::hasColumn(config('ban.table'), 'metas')) {
Schema::table(config('ban.table'), function (Blueprint $table) {
$table->json('metas')->nullable();
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (Schema::hasColumn(config('ban.table'), 'metas')) {
Schema::table(config('ban.table'), function (Blueprint $table) {
$table->dropColumn('metas');
});
}
}
};

View File

@ -3,6 +3,8 @@
use App\Http\Controllers\ModController; use App\Http\Controllers\ModController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::middleware(['auth.banned'])->group(function () {
Route::get('/', function () { Route::get('/', function () {
return view('home'); return view('home');
})->name('home'); })->name('home');
@ -17,3 +19,5 @@ Route::middleware(['auth:sanctum', config('jetstream.auth_session'), 'verified']
return view('dashboard'); return view('dashboard');
})->name('dashboard'); })->name('dashboard');
}); });
});