Sync Admin Roles

Roles (or "ranks" as they're called on the hub) are now being pulled down into the local database.
This commit is contained in:
Refringe 2024-06-18 00:41:12 -04:00
parent 47a7121707
commit 5e1c05f49d
Signed by: Refringe
SSH Key Fingerprint: SHA256:t865XsQpfTeqPRBMN2G6+N8wlDjkgUCZF3WGW6O9N/k
9 changed files with 346 additions and 74 deletions

View File

@ -7,6 +7,7 @@ use App\Models\Mod;
use App\Models\ModVersion;
use App\Models\SptVersion;
use App\Models\User;
use App\Models\UserRole;
use Carbon\Carbon;
use CurlHandle;
use Exception;
@ -164,62 +165,50 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
protected function importUsers(): void
{
DB::connection('mysql_hub')
->table('wcf1_user')
->select('userID', 'username', 'email', 'password', 'registrationDate', 'banned', 'banReason', 'banExpires')
->table('wcf1_user as u')
->select('u.userID', 'u.username', 'u.email', 'u.password', 'u.registrationDate', 'u.banned', 'u.banReason', 'u.banExpires', 'u.rankID', 'r.rankTitle')
->leftJoin('wcf1_user_rank as r', 'u.rankID', '=', 'r.rankID')
->chunkById(250, function (Collection $users) {
$insertData = [];
$bannedUsers = [];
$userData = $bannedUsers = $userRanks = [];
foreach ($users as $user) {
$insertData[] = [
'hub_id' => (int) $user->userID,
'name' => $user->username,
'email' => mb_convert_case($user->email, MB_CASE_LOWER, 'UTF-8'),
'password' => $this->cleanPasswordHash($user->password),
'created_at' => $this->cleanRegistrationDate($user->registrationDate),
'updated_at' => now('UTC')->toDateTimeString(),
];
$userData[] = $this->collectUserData($user);
// 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),
];
$bannedUserData = $this->collectBannedUserData($user);
if ($bannedUserData) {
$bannedUsers[] = $bannedUserData;
}
$userRankData = $this->collectUserRankData($user);
if ($userRankData) {
$userRanks[] = $userRankData;
}
}
if (! empty($insertData)) {
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'],
]);
}
}
$this->upsertUsers($userData);
$this->handleBannedUsers($bannedUsers);
$this->handleUserRoles($userRanks);
}, 'userID');
}
protected function collectUserData($user): array
{
return [
'hub_id' => (int) $user->userID,
'name' => $user->username,
'email' => mb_convert_case($user->email, MB_CASE_LOWER, 'UTF-8'),
'password' => $this->cleanPasswordHash($user->password),
'created_at' => $this->cleanRegistrationDate($user->registrationDate),
'updated_at' => now('UTC')->toDateTimeString(),
];
}
/**
* Clean the password hash from the Hub database.
*/
protected function cleanPasswordHash(string $password): string
{
// The hub passwords are hashed sometimes with a prefix of the hash type. We only want the hash.
// The hub passwords sometimes hashed with a prefix of the hash type. We only want the hash.
// If it's not Bcrypt, they'll have to reset their password. Tough luck.
$clean = str_ireplace(['invalid:', 'bcrypt:', 'bcrypt::', 'cryptmd5:', 'cryptmd5::'], '', $password);
@ -242,6 +231,22 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
return $date->toDateTimeString();
}
/**
* Build an array of banned user data ready to be inserted into the local database.
*/
protected function collectBannedUserData($user): ?array
{
if ($user->banned) {
return [
'hub_id' => (int) $user->userID,
'comment' => $user->banReason ?? '',
'expired_at' => $this->cleanUnbannedAtDate($user->banExpires),
];
}
return null;
}
/**
* Clean the banned_at date from the Hub database.
*/
@ -282,6 +287,98 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
}
}
/*
* Build an array of user rank data ready to be inserted into the local database.
*/
protected function collectUserRankData($user): ?array
{
if ($user->rankID && $user->rankTitle) {
return [
'hub_id' => (int) $user->userID,
'title' => $user->rankTitle,
];
}
return null;
}
/**
* Insert or update the users in the local database.
*/
protected function upsertUsers($usersData): void
{
if (! empty($usersData)) {
DB::table('users')->upsert($usersData, ['hub_id'], [
'name',
'email',
'password',
'created_at',
'updated_at',
]);
}
}
/**
* Fetch the hub-banned users from the local database and ban them locally.
*/
protected function handleBannedUsers($bannedUsers): void
{
foreach ($bannedUsers as $bannedUser) {
$user = User::whereHubId($bannedUser['hub_id'])->first();
$user->ban([
'comment' => $bannedUser['comment'],
'expired_at' => $bannedUser['expired_at'],
]);
}
}
/**
* Fetch or create the user ranks in the local database and assign them to the users.
*/
protected function handleUserRoles($userRanks): void
{
foreach ($userRanks as $userRank) {
$roleName = Str::ucfirst(Str::afterLast($userRank['title'], '.'));
$roleData = $this->buildUserRoleData($roleName);
UserRole::upsert($roleData, ['name'], ['name', 'short_name', 'description', 'color_class']);
$userRole = UserRole::whereName($roleData['name'])->first();
$user = User::whereHubId($userRank['hub_id'])->first();
$user->assignRole($userRole);
}
}
/**
* Build the user role data based on the role name.
*/
protected function buildUserRoleData(string $name): array
{
if ($name === 'Administrator') {
return [
'name' => 'Administrator',
'short_name' => 'Admin',
'description' => 'An administrator has full access to the site.',
'color_class' => 'sky',
];
}
if ($name === 'Moderator') {
return [
'name' => 'Moderator',
'short_name' => 'Mod',
'description' => 'A moderator has the ability to moderate user content.',
'color_class' => 'emerald',
];
}
return [
'name' => $name,
'short_name' => '',
'description' => '',
'color_class' => '',
];
}
/**
* Import the licenses from the Hub database to the local database.
*/
@ -568,13 +665,13 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
*/
public function failed(Exception $exception): void
{
// Drop the temporary tables.
// Explicitly 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.
// Close the connections. This should drop the temporary tables as well, but I like to be explicit.
DB::connection('mysql_hub')->disconnect();
DB::disconnect();
}

View File

@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
@ -58,6 +59,18 @@ class User extends Authenticatable implements MustVerifyEmail
return ! is_null($this->email_verified_at);
}
public function assignRole(UserRole $role): bool
{
$this->role()->associate($role);
return $this->save();
}
public function role(): BelongsTo
{
return $this->belongsTo(UserRole::class, 'user_role_id');
}
protected function casts(): array
{
return [

24
app/Models/UserRole.php Normal file
View File

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class UserRole extends Model
{
use HasFactory;
protected $fillable = [
'name',
'short_name',
'description',
'color_class',
];
public function users(): HasMany
{
return $this->hasMany(User::class);
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Nova;
use App\Models\UserRole;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Text;
class UserRoleResource extends Resource
{
public static string $model = UserRole::class;
public static $title = 'name';
public static $search = [
'id', 'name', 'description',
];
public function fields(Request $request): array
{
return [
ID::make()->sortable(),
Text::make('Name')
->sortable()
->rules('required'),
Text::make('Description')
->sortable()
->rules('required'),
];
}
public function cards(Request $request): array
{
return [];
}
public function filters(Request $request): array
{
return [];
}
public function lenses(Request $request): array
{
return [];
}
public function actions(Request $request): array
{
return [];
}
}

View File

@ -2,16 +2,10 @@
namespace Database\Factories;
use App\Models\Team;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Laravel\Jetstream\Features;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
*/
class UserFactory extends Factory
{
/**
@ -20,9 +14,7 @@ class UserFactory extends Factory
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
* Define the user's default state.
*/
public function definition(): array
{
@ -34,13 +26,14 @@ class UserFactory extends Factory
'two_factor_secret' => null,
'two_factor_recovery_codes' => null,
'remember_token' => Str::random(10),
'user_role_id' => null,
'profile_photo_path' => null,
'current_team_id' => null,
];
}
/**
* Indicate that the model's email address should be unverified.
* Indicate that the user's email address should be unverified.
*/
public function unverified(): static
{
@ -48,25 +41,4 @@ class UserFactory extends Factory
'email_verified_at' => null,
]);
}
/**
* Indicate that the user should have a personal team.
*/
public function withPersonalTeam(?callable $callback = null): static
{
if (! Features::hasTeamFeatures()) {
return $this->state([]);
}
return $this->has(
Team::factory()
->state(fn (array $attributes, User $user) => [
'name' => $user->name.'\'s Team',
'user_id' => $user->id,
'personal_team' => true,
])
->when(is_callable($callback), $callback),
'ownedTeams'
);
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Database\Factories;
use App\Models\UserRole;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
class UserRoleFactory extends Factory
{
protected $model = UserRole::class;
public function definition(): array
{
return [
'name' => $this->faker->name(),
'short_name' => $this->faker->word(),
'description' => $this->faker->text(),
'color_class' => $this->faker->randomElement(['sky', 'red', 'green', 'emerald', 'lime']),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];
}
/**
* Define the "administrator" role.
*/
public function administrator(): static
{
return $this->state(fn (array $attributes) => [
'name' => 'Administrator',
'short_name' => 'Admin',
'description' => 'An administrator has full access to the site.',
'color_class' => 'sky',
]);
}
/**
* Define the "moderator" role.
*/
public function moderator(): static
{
return $this->state(fn (array $attributes) => [
'name' => 'Moderator',
'short_name' => 'Mod',
'description' => 'A moderator has the ability to moderate user content.',
'color_class' => 'emerald',
]);
}
}

View File

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('user_roles', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('short_name')->default('');
$table->string('description')->default('');
$table->string('color_class')->default('');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('user_roles');
}
};

View File

@ -0,0 +1,28 @@
<?php
use App\Models\UserRole;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->foreignIdFor(UserRole::class)
->nullable()
->after('remember_token')
->constrained()
->cascadeOnUpdate()
->nullOnDelete();
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropForeign(['user_role_id']);
});
}
};

View File

@ -7,6 +7,7 @@ use App\Models\Mod;
use App\Models\ModVersion;
use App\Models\SptVersion;
use App\Models\User;
use App\Models\UserRole;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
@ -22,6 +23,14 @@ class DatabaseSeeder extends Seeder
// Create some code licenses.
$licenses = License::factory(10)->create();
// Add 5 administrators.
$administrator = UserRole::factory()->administrator()->create();
User::factory(5)->create(['user_role_id' => $administrator->id]);
// Add 10 moderators.
$moderator = UserRole::factory()->moderator()->create();
User::factory(10)->create(['user_role_id' => $moderator->id]);
// Add 100 users.
$users = User::factory(100)->create();