From 5e1c05f49de45f7de4a4fd0fe8f3e1d77e114d93 Mon Sep 17 00:00:00 2001 From: Refringe Date: Tue, 18 Jun 2024 00:41:12 -0400 Subject: [PATCH] Sync Admin Roles Roles (or "ranks" as they're called on the hub) are now being pulled down into the local database. --- app/Jobs/ImportHubData.php | 183 ++++++++++++++---- app/Models/User.php | 13 ++ app/Models/UserRole.php | 24 +++ app/Nova/UserRoleResource.php | 54 ++++++ database/factories/UserFactory.php | 34 +--- database/factories/UserRoleFactory.php | 50 +++++ ...4_06_17_021924_create_user_roles_table.php | 25 +++ ...3510_update_users_table_with_user_role.php | 28 +++ database/seeders/DatabaseSeeder.php | 9 + 9 files changed, 346 insertions(+), 74 deletions(-) create mode 100644 app/Models/UserRole.php create mode 100644 app/Nova/UserRoleResource.php create mode 100644 database/factories/UserRoleFactory.php create mode 100644 database/migrations/2024_06_17_021924_create_user_roles_table.php create mode 100644 database/migrations/2024_06_17_023510_update_users_table_with_user_role.php diff --git a/app/Jobs/ImportHubData.php b/app/Jobs/ImportHubData.php index 26d1200..40c27d3 100644 --- a/app/Jobs/ImportHubData.php +++ b/app/Jobs/ImportHubData.php @@ -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(); } diff --git a/app/Models/User.php b/app/Models/User.php index 7dd6916..1674ff7 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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 [ diff --git a/app/Models/UserRole.php b/app/Models/UserRole.php new file mode 100644 index 0000000..b496c20 --- /dev/null +++ b/app/Models/UserRole.php @@ -0,0 +1,24 @@ +hasMany(User::class); + } +} diff --git a/app/Nova/UserRoleResource.php b/app/Nova/UserRoleResource.php new file mode 100644 index 0000000..52c43c3 --- /dev/null +++ b/app/Nova/UserRoleResource.php @@ -0,0 +1,54 @@ +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 []; + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index ea955ad..eedac39 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -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 + * 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' - ); - } } diff --git a/database/factories/UserRoleFactory.php b/database/factories/UserRoleFactory.php new file mode 100644 index 0000000..0e7d165 --- /dev/null +++ b/database/factories/UserRoleFactory.php @@ -0,0 +1,50 @@ + $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', + ]); + } +} diff --git a/database/migrations/2024_06_17_021924_create_user_roles_table.php b/database/migrations/2024_06_17_021924_create_user_roles_table.php new file mode 100644 index 0000000..86e39c5 --- /dev/null +++ b/database/migrations/2024_06_17_021924_create_user_roles_table.php @@ -0,0 +1,25 @@ +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'); + } +}; diff --git a/database/migrations/2024_06_17_023510_update_users_table_with_user_role.php b/database/migrations/2024_06_17_023510_update_users_table_with_user_role.php new file mode 100644 index 0000000..9daa80b --- /dev/null +++ b/database/migrations/2024_06_17_023510_update_users_table_with_user_role.php @@ -0,0 +1,28 @@ +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']); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index c81ce99..cdc5924 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -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();