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_LEVEL=debug
# Due to the hub import script, only MySQL is supported at this time.
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306

1
.gitignore vendored
View File

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

View File

@ -20,8 +20,8 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use League\HTMLToMarkdown\HtmlConverter;
use Stevebauman\Purify\Facades\Purify;
@ -33,7 +33,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
public function handle(): void
{
// 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->bringFileContentLocal();
$this->bringFileVersionLabelsLocal();
@ -60,9 +60,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
*/
protected function bringFileOptionsLocal(): void
{
if (Schema::hasTable('temp_file_option_values')) {
DB::statement('DROP TABLE temp_file_option_values');
}
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_option_values');
DB::statement('CREATE TEMPORARY TABLE temp_file_option_values (
fileID INT,
optionID INT,
@ -88,9 +86,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
*/
protected function bringFileContentLocal(): void
{
if (Schema::hasTable('temp_file_content')) {
DB::statement('DROP TABLE temp_file_content');
}
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_content');
DB::statement('CREATE TEMPORARY TABLE temp_file_content (
fileID INT,
subject VARCHAR(255),
@ -118,9 +114,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
*/
protected function bringFileVersionLabelsLocal(): void
{
if (Schema::hasTable('temp_file_version_labels')) {
DB::statement('DROP TABLE temp_file_version_labels');
}
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_version_labels');
DB::statement('CREATE TEMPORARY TABLE temp_file_version_labels (
labelID INT,
objectID INT
@ -145,9 +139,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
*/
protected function bringFileVersionContentLocal(): void
{
if (Schema::hasTable('temp_file_version_content')) {
DB::statement('DROP TABLE temp_file_version_content');
}
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_version_content');
DB::statement('CREATE TEMPORARY TABLE temp_file_version_content (
versionID INT,
description TEXT
@ -173,10 +165,12 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
{
DB::connection('mysql_hub')
->table('wcf1_user')
->select('userID', 'username', 'email', 'password', 'registrationDate')
->select('userID', 'username', 'email', 'password', 'registrationDate', 'banned', 'banReason', 'banExpires')
->chunkById(250, function (Collection $users) {
$insertData = [];
$bannedUsers = [];
foreach ($users as $user) {
$insertData[] = [
'hub_id' => (int) $user->userID,
@ -186,14 +180,36 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
'created_at' => $this->cleanRegistrationDate($user->registrationDate),
'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)) {
DB::table('users')->upsert(
$insertData,
['hub_id'],
['name', 'email', 'password', 'created_at', 'updated_at']
);
DB::table('users')->upsert($insertData, ['hub_id'], [
'name',
'email',
'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');
}
@ -207,7 +223,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
// If it's not Bcrypt, they'll have to reset their password. Tough luck.
$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 : '';
}
@ -226,6 +242,46 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
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.
*/
@ -512,7 +568,14 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
*/
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::disconnect();
}
}

View File

@ -11,9 +11,11 @@ use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Jetstream\HasProfilePhoto;
use Laravel\Sanctum\HasApiTokens;
use Laravel\Scout\Searchable;
use Mchev\Banhammer\Traits\Bannable;
class User extends Authenticatable implements MustVerifyEmail
{
use Bannable;
use HasApiTokens;
use HasFactory;
use HasProfilePhoto;
@ -38,14 +40,6 @@ class User extends Authenticatable implements MustVerifyEmail
'profile_photo_url',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
public function mods(): HasMany
{
return $this->hasMany(Mod::class);
@ -63,4 +57,12 @@ class User extends Authenticatable implements MustVerifyEmail
{
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\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Mchev\Banhammer\Middleware\IPBanned;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
@ -12,7 +13,7 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
//
$middleware->append(IPBanned::class);
})
->withExceptions(function (Exceptions $exceptions) {
//

View File

@ -22,6 +22,7 @@
"league/flysystem-aws-s3-v3": "3.0",
"league/html-to-markdown": "^5.1",
"livewire/livewire": "^3.0",
"mchev/banhammer": "^2.3",
"meilisearch/meilisearch-php": "^1.8",
"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",
"This file is @generated automatically"
],
"content-hash": "35e09d2f7ca6d5c8de5ed54e14d13e58",
"content-hash": "f9f3d2b898b0a2e762d3c042c4d38f3a",
"packages": [
{
"name": "aws/aws-crt-php",
@ -3447,6 +3447,74 @@
],
"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",
"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,17 +3,21 @@
use App\Http\Controllers\ModController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('home');
})->name('home');
Route::middleware(['auth.banned'])->group(function () {
Route::controller(ModController::class)->group(function () {
Route::get('/mods', 'index')->name('mods');
Route::get('/mod/{mod}/{slug}', 'show')->where(['id' => '[0-9]+'])->name('mod.show');
});
Route::get('/', function () {
return view('home');
})->name('home');
Route::controller(ModController::class)->group(function () {
Route::get('/mods', 'index')->name('mods');
Route::get('/mod/{mod}/{slug}', 'show')->where(['id' => '[0-9]+'])->name('mod.show');
});
Route::middleware(['auth:sanctum', config('jetstream.auth_session'), 'verified'])->group(function () {
Route::get('/dashboard', function () {
return view('dashboard');
})->name('dashboard');
});
Route::middleware(['auth:sanctum', config('jetstream.auth_session'), 'verified'])->group(function () {
Route::get('/dashboard', function () {
return view('dashboard');
})->name('dashboard');
});