diff --git a/app/Community/AppServiceProvider.php b/app/Community/AppServiceProvider.php index 7a023e068b..f537c3ea15 100644 --- a/app/Community/AppServiceProvider.php +++ b/app/Community/AppServiceProvider.php @@ -11,6 +11,7 @@ use App\Community\Commands\SyncForums; use App\Community\Commands\SyncTickets; use App\Community\Commands\SyncUserRelations; +use App\Community\Commands\SyncUserUlids; use App\Community\Components\DeveloperGameStatsTable; use App\Community\Components\ForumRecentActivity; use App\Community\Components\MessageIcon; @@ -55,6 +56,7 @@ public function boot(): void SyncForums::class, SyncTickets::class, SyncUserRelations::class, + SyncUserUlids::class, GenerateAnnualRecap::class, ]); diff --git a/app/Community/Commands/SyncUserUlids.php b/app/Community/Commands/SyncUserUlids.php new file mode 100644 index 0000000000..158329e516 --- /dev/null +++ b/app/Community/Commands/SyncUserUlids.php @@ -0,0 +1,67 @@ +count(); + + if ($total === 0) { + $this->info('No records need ULIDs.'); + + return; + } + + $progressBar = $this->output->createProgressBar($total); + $progressBar->start(); + + User::withTrashed() + ->whereNull('ulid') + ->chunkById(4000, function ($users) use ($progressBar) { + $updates = []; + $milliseconds = 0; + + /** @var User $user */ + foreach ($users as $user) { + $milliseconds += rand(1, 20); + $milliseconds %= 1000; + + $timestamp = $user->Created->clone()->addMilliseconds($milliseconds); + $ulid = (string) Str::ulid($timestamp); + + $updates[] = [ + 'ID' => $user->id, + 'ulid' => $ulid, + ]; + } + + // Perform a batch update for speed. + DB::table('UserAccounts') + ->upsert( + $updates, + ['ID'], + ['ulid'] + ); + + $progressBar->advance(count($updates)); + }); + + $progressBar->finish(); + + $this->newLine(); + $this->info('Done.'); + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index cd09ceed1a..344eafe1c3 100755 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -29,6 +29,7 @@ public function definition(): array { return [ // required + 'ulid' => (string) Str::ulid(), 'User' => $this->fakeUsername(), 'EmailAddress' => fake()->unique()->safeEmail, 'email_verified_at' => now(), diff --git a/database/migrations/2025_01_26_000000_update_useraccounts_table.php b/database/migrations/2025_01_26_000000_update_useraccounts_table.php new file mode 100644 index 0000000000..5e3b570e44 --- /dev/null +++ b/database/migrations/2025_01_26_000000_update_useraccounts_table.php @@ -0,0 +1,23 @@ +ulid('ulid')->after('ID')->unique()->nullable(); + }); + } + + public function down(): void + { + Schema::table('UserAccounts', function (Blueprint $table) { + $table->dropColumn('ulid'); + }); + } +}; diff --git a/public/API/API_GetUserProfile.php b/public/API/API_GetUserProfile.php index 7f193ea760..2efda5440a 100644 --- a/public/API/API_GetUserProfile.php +++ b/public/API/API_GetUserProfile.php @@ -3,9 +3,11 @@ /* * API_GetUserProfile * u : username + * i : ULID * - * string User name of user + * string User non-stable name of user * int ID unique identifier of the user + * string ULID queryable unique identifier of the user * int TotalPoints number of hardcore points the user has * int TotalSoftcorePoints number of softcore points the user has * int TotalTruePoints number of RetroPoints ("white points") the user has @@ -27,10 +29,13 @@ use Illuminate\Support\Facades\Validator; $input = Validator::validate(Arr::wrap(request()->query()), [ - 'u' => ['required', 'min:2', 'max:20', new CtypeAlnum()], + 'u' => ['required_without:i', 'min:2', 'max:20', new CtypeAlnum()], + 'i' => ['required_without:u', 'string', 'size:26'], ]); -$user = User::whereName(request()->query('u'))->first(); +$user = isset($input['i']) + ? User::whereUlid($input['i'])->first() + : User::whereName($input['u'])->first(); if (!$user) { return response()->json([], 404); @@ -38,6 +43,7 @@ return response()->json([ 'User' => $user->display_name, + 'ULID' => $user->ulid, 'UserPic' => sprintf("/UserPic/%s.png", $user->username), 'MemberSince' => $user->created_at->toDateTimeString(), 'RichPresenceMsg' => empty($user->RichPresenceMsg) || $user->RichPresenceMsg === 'Unknown' ? null : $user->RichPresenceMsg, diff --git a/public/request/auth/register.php b/public/request/auth/register.php index 3be153d661..32cc4c7eb0 100644 --- a/public/request/auth/register.php +++ b/public/request/auth/register.php @@ -5,6 +5,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; +use Illuminate\Support\Str; $input = Validator::validate(Arr::wrap(request()->post()), [ 'username' => ValidNewUsername::get(), @@ -41,10 +42,11 @@ } } +$ulid = (string) Str::ulid(); $hashedPassword = Hash::make($pass); -$query = "INSERT INTO UserAccounts (User, display_name, Password, SaltedPass, EmailAddress, Permissions, RAPoints, fbUser, fbPrefs, cookie, appToken, appTokenExpiry, websitePrefs, LastLogin, LastActivityID, Motto, ContribCount, ContribYield, APIKey, APIUses, LastGameID, RichPresenceMsg, RichPresenceMsgDate, ManuallyVerified, UnreadMessageCount, TrueRAPoints, UserWallActive, PasswordResetToken, Untracked, email_backup) -VALUES ( '$username', '$username', '$hashedPassword', '', '$email', 0, 0, 0, 0, '', '', NULL, 127, null, 0, '', 0, 0, '', 0, 0, '', NULL, 0, 0, 0, 1, NULL, false, '$email')"; +$query = "INSERT INTO UserAccounts (ulid, User, display_name, Password, SaltedPass, EmailAddress, Permissions, RAPoints, fbUser, fbPrefs, cookie, appToken, appTokenExpiry, websitePrefs, LastLogin, LastActivityID, Motto, ContribCount, ContribYield, APIKey, APIUses, LastGameID, RichPresenceMsg, RichPresenceMsgDate, ManuallyVerified, UnreadMessageCount, TrueRAPoints, UserWallActive, PasswordResetToken, Untracked, email_backup) +VALUES ('$ulid', '$username', '$username', '$hashedPassword', '', '$email', 0, 0, 0, 0, '', '', NULL, 127, null, 0, '', 0, 0, '', 0, 0, '', NULL, 0, 0, 0, 1, NULL, false, '$email')"; $dbResult = s_mysql_query($query); if (!$dbResult) { diff --git a/tests/Feature/Api/V1/UserProfileTest.php b/tests/Feature/Api/V1/UserProfileTest.php index c413b54da4..cdf6ed1875 100644 --- a/tests/Feature/Api/V1/UserProfileTest.php +++ b/tests/Feature/Api/V1/UserProfileTest.php @@ -18,6 +18,12 @@ public function testItValidates(): void $this->get($this->apiUrl('GetUserProfile')) ->assertJsonValidationErrors([ 'u', + 'i', + ]); + + $this->get($this->apiUrl('GetUserProfile', ['u' => 'username', 'i' => 'ulid'])) + ->assertJsonValidationErrors([ + 'i', // should fail size:26 validation. ]); } @@ -28,7 +34,14 @@ public function testGetUserProfileUnknownUser(): void ->assertJson([]); } - public function testGetUserProfile(): void + public function testGetUserProfileUnknownUlid(): void + { + $this->get($this->apiUrl('GetUserProfile', ['i' => '01HNG49MXJA71KCVG3PXQS5B2C'])) + ->assertNotFound() + ->assertJson([]); + } + + public function testGetUserProfileByUsername(): void { /** @var User $user */ $user = User::factory()->create(); @@ -53,4 +66,31 @@ public function testGetUserProfile(): void 'Motto' => $user->Motto, ]); } + + public function testGetUserProfileByUlid(): void + { + /** @var User $user */ + $user = User::factory()->create(); + + $this->get($this->apiUrl('GetUserProfile', ['i' => $user->ulid])) + ->assertSuccessful() + ->assertJson([ + 'User' => $user->User, + 'ULID' => $user->ulid, + 'UserPic' => sprintf("/UserPic/%s.png", $user->User), + 'MemberSince' => $user->created_at->toDateTimeString(), + 'RichPresenceMsg' => ($user->RichPresenceMsg) ? $user->RichPresenceMsg : null, + 'LastGameID' => $user->LastGameID, + 'ContribCount' => $user->ContribCount, + 'ContribYield' => $user->ContribYield, + 'TotalPoints' => $user->RAPoints, + 'TotalSoftcorePoints' => $user->RASoftcorePoints, + 'TotalTruePoints' => $user->TrueRAPoints, + 'Permissions' => $user->getAttribute('Permissions'), + 'Untracked' => $user->Untracked, + 'ID' => $user->ID, + 'UserWallActive' => $user->UserWallActive, + 'Motto' => $user->Motto, + ]); + } }